fix(run): recover orphaned native sessions

This commit is contained in:
AkitaOnRails
2026-07-25 14:10:41 -03:00
parent b890f702f0
commit 504a895416
11 changed files with 396 additions and 28 deletions
+7
View File
@@ -46,6 +46,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`access_count` a coarser retention signal. (#239)
### Fixed
- `ai-memory run <harness>` now verifies an ai-memory-injected native resume
target still exists in that harness's read-only session store. A confirmed
orphan starts a fresh native session and repoints the same workstream instead
of retrying the dead id forever. The new wrapper-owned `--fresh` flag forces
the same per-workstream recovery without a resume attempt or adoption prompt;
explicit native resume/session/fork selectors remain authoritative and
cannot be combined with `--fresh`. (#240)
- `install-mcp --client claude-desktop` now detects an MSIX-packaged
Claude Desktop on Windows and writes to its virtualized
`AppData\Local\Packages\Claude_<id>\LocalCache\Roaming\Claude\claude_desktop_config.json`
+9 -3
View File
@@ -71,7 +71,8 @@ priors are at the [bottom](#influences-and-prior-art).
Codex, OpenCode, Pi, Crush, or Kimi Code session for this checkout. On first
explicit use, an interactive launcher can adopt a previous session from the
same checkout; later switches cannot select unrelated native history. Native
arguments pass through unchanged except the wrapper-owned `--yolo`; direct
arguments pass through unchanged except the wrapper-owned `--yolo` and
`--fresh`; direct
commands are unaffected. `kimi-code` and `kimi-cli` are accepted aliases for
the installed `kimi` command.
- **Per-repository capture exclusions.** A nearest-marker `[capture]`
@@ -148,6 +149,9 @@ priors are at the [bottom](#influences-and-prior-art).
# Later, omit the name to resume the newest usable managed session here.
ai-memory run
# Start a new Codex session in the same workstream, keeping portable history.
ai-memory run --fresh codex
```
The first explicit run can offer an existing session from this exact checkout
@@ -155,7 +159,9 @@ priors are at the [bottom](#influences-and-prior-art).
linked to the shared workstream, so an obsolete local session cannot replace
newer cross-harness history. After a normal quit, the next launch waits
briefly if the previous launcher is still finalizing; handled failures release
the workstream immediately. Managed mode currently covers Claude Code, Codex,
the workstream immediately. If a linked native transcript was deleted,
ai-memory detects the orphan before launch and starts fresh; `--fresh` forces
that recovery for one harness. Managed mode currently covers Claude Code, Codex,
OpenCode, Pi, Crush, Kimi Code, and OMP; direct harness launches remain
unchanged. See [Managed cross-harness workstreams](docs/managed-workstreams.md).
- **"Quit at 4 PM, pick up at 9 AM in a different agent."** The
@@ -707,7 +713,7 @@ diagram, crate breakdown, schema notes, and invariants.
|---|---|
| [`docs/install.md`](docs/install.md) | **Installation cookbook.** Every agent CLI, every alternative (curl, source build, no-docker, no-auth), and the server-on-a-different-machine (homelab/LAN) walkthrough. Read after the Quick start if your setup doesn't match the happy path. |
| [`docs/usage.md`](docs/usage.md) | Handoffs, proactive memory queries, slim routing snippet + managed Agent Skills, migration from other memory tools, web UI, raw-wiki inspection, and rules-vs-facts workflow. |
| [`docs/managed-workstreams.md`](docs/managed-workstreams.md) | Optional `ai-memory run` continuity across Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, and OMP: automatic harness selection, native resume, argument forwarding, ledger search, privacy, and recovery. |
| [`docs/managed-workstreams.md`](docs/managed-workstreams.md) | Optional `ai-memory run` continuity across Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, and Grok Build CLI: automatic harness selection, native resume, argument forwarding, ledger search, privacy, and recovery. |
| [`docs/managed-harness-contributions.md`](docs/managed-harness-contributions.md) | Protocol and acceptance bar for contributors adding managed resume, read-only transcript import, and startup context delivery to another harness. |
| [`docs/marker-file.md`](docs/marker-file.md) | `.ai-memory.toml` workspace/project routing for multi-client trees, mono-repos, worktrees, and work/personal separation. |
| [`docs/auto-scope.md`](docs/auto-scope.md) | `[auto_scope]` modes for shared servers: default single-slot routing, session-aware isolation, and multi-user `per_actor` behavior. |
+6 -1
View File
@@ -34,7 +34,8 @@ pub enum Command {
/// Print runtime status (counts, paths, version).
Status(StatusArgs),
/// Launch an agent in an opt-in, cross-harness managed workstream.
/// Every argument after the harness name is forwarded unchanged.
/// Native arguments are forwarded except exact wrapper flags such as
/// `--yolo` and `--fresh`.
Run(RunArgs),
/// Search the complete visible event ledger for a managed workstream.
WorkstreamSearch(WorkstreamSearchArgs),
@@ -189,6 +190,10 @@ pub struct RunArgs {
/// equivalent dangerous-mode option.
#[arg(long)]
pub yolo: bool,
/// Start a new native session in the selected workstream instead of
/// resuming or adopting an existing harness session.
#[arg(long)]
pub fresh: bool,
/// Agent harness to launch. When omitted, continue the newest managed or
/// checkout-local session among the auto-detected harnesses.
#[arg(value_enum)]
+195 -3
View File
@@ -16,7 +16,8 @@ use ai_memory_core::{
use ai_memory_workstream::{
ExportedTranscript, LaunchMode, LaunchPlan, ManagedHarness, NativeSessionCandidate,
allows_native_session_adoption, apply_yolo, build_launch_plan, discover_native_session,
export_transcript, inspect_repository, list_native_sessions, wait_for_transcript_flush,
export_transcript, has_native_session_selector, inspect_repository, list_native_sessions,
native_session_exists, wait_for_transcript_flush,
};
use anyhow::{Context as _, Result, anyhow};
use tokio::process::Command;
@@ -57,6 +58,8 @@ pub async fn run(config: &Config, args: RunArgs) -> Result<i32> {
let automatic_harness = args.harness.is_none();
let mut native_args = args.native_args;
let trailing_yolo = remove_wrapper_yolo(&mut native_args);
let trailing_fresh = remove_wrapper_fresh(&mut native_args);
let force_fresh = args.fresh || trailing_fresh;
if automatic_harness && !native_args.is_empty() {
return Err(anyhow!(
"native harness arguments require an explicit harness; try `ai-memory run codex ...`"
@@ -85,7 +88,7 @@ pub async fn run(config: &Config, args: RunArgs) -> Result<i32> {
let executable = args.executable.map(PathBuf::into_os_string);
ensure_executable_available(provisional_harness, executable.as_deref())?;
let project = resolve_project_name(config, args.project.as_deref())?;
let may_adopt_native_session = args.new_workstream.is_none();
let may_adopt_native_session = args.new_workstream.is_none() && !force_fresh;
let endpoint = ServerEndpoint::from_config_resolving_auth(config).await;
let prepare = PrepareManagedRunRequest {
workspace: args.workspace,
@@ -151,12 +154,29 @@ pub async fn run(config: &Config, args: RunArgs) -> Result<i32> {
};
acquired_try!(ensure_executable_available(harness, executable.as_deref()));
let native_grok_rules = user_supplied_grok_rules(&native_args);
let mut plan = acquired_try!(build_launch_plan(
let (mut plan, orphaned_session) = acquired_try!(build_preflighted_launch_plan(
harness,
executable.clone(),
native_args.clone(),
prepared.native_session_id.as_deref(),
force_fresh,
&home,
&repository.cwd,
));
if let Some(orphaned_session) = orphaned_session {
eprintln!(
"ai-memory: linked {} session {} is missing from its native store; starting fresh and repointing workstream '{}' after the new session is established",
harness.as_str(),
display_session_id(&orphaned_session),
prepared.workstream_name
);
} else if force_fresh && plan.mode == LaunchMode::Session {
eprintln!(
"ai-memory: starting a fresh {} session in workstream '{}'",
harness.as_str(),
prepared.workstream_name
);
}
if automatic_harness
&& prepared.native_session_id.is_none()
&& prepared.may_adopt_existing_session
@@ -534,6 +554,63 @@ fn remove_wrapper_yolo(args: &mut Vec<OsString>) -> bool {
args.len() != before
}
fn remove_wrapper_fresh(args: &mut Vec<OsString>) -> bool {
let before = args.len();
args.retain(|arg| arg != OsStr::new("--fresh"));
args.len() != before
}
fn build_preflighted_launch_plan(
harness: ManagedHarness,
executable: Option<OsString>,
native_args: Vec<OsString>,
linked_session_id: Option<&str>,
force_fresh: bool,
home: &Path,
cwd: &Path,
) -> Result<(LaunchPlan, Option<String>)> {
let explicit_selector = has_native_session_selector(harness, &native_args);
if force_fresh && explicit_selector {
return Err(anyhow!(
"--fresh cannot be combined with a native session, resume, continue, or fork selector"
));
}
let linked_session_id = if force_fresh { None } else { linked_session_id };
let plan = build_launch_plan(
harness,
executable.clone(),
native_args.clone(),
linked_session_id,
)?;
let Some(linked_session_id) = linked_session_id else {
return Ok((plan, None));
};
if explicit_selector || plan.mode != LaunchMode::Session {
return Ok((plan, None));
}
match native_session_exists(
harness,
home,
cwd,
plan.session_dir.as_deref(),
linked_session_id,
) {
Ok(true) => Ok((plan, None)),
Ok(false) => Ok((
build_launch_plan(harness, executable, native_args, None)?,
Some(linked_session_id.to_string()),
)),
Err(error) => {
eprintln!(
"ai-memory: could not verify linked {} session {} ({error}); preserving native resume. Use --fresh to bypass it",
harness.as_str(),
display_session_id(linked_session_id)
);
Ok((plan, None))
}
}
}
fn ensure_executable_available(harness: ManagedHarness, executable: Option<&OsStr>) -> Result<()> {
let program = executable.unwrap_or_else(|| OsStr::new(harness.executable()));
if executable_available(program) {
@@ -1346,6 +1423,121 @@ mod tests {
assert_eq!(args, ["resume", "native-id"].map(OsString::from));
}
#[test]
fn wrapper_fresh_parses_before_or_after_the_harness() {
let cli = Cli::try_parse_from(["ai-memory", "run", "--fresh", "codex"]).unwrap();
let CliCommand::Run(args) = cli.command else {
panic!("expected run command");
};
assert!(args.fresh);
assert!(args.native_args.is_empty());
let mut trailing = ["--fresh", "--model", "opus"].map(OsString::from).to_vec();
assert!(remove_wrapper_fresh(&mut trailing));
assert_eq!(trailing, ["--model", "opus"].map(OsString::from));
}
#[test]
fn missing_linked_session_starts_fresh_but_explicit_selectors_win() {
let temp = tempfile::tempdir().unwrap();
let cwd = temp.path().join("repo");
let session_root = temp.path().join("pi-sessions");
std::fs::create_dir_all(&cwd).unwrap();
std::fs::create_dir_all(&session_root).unwrap();
let transcript = session_root.join("linked.jsonl");
std::fs::write(
&transcript,
format!(
"{}\n",
serde_json::json!({"type":"session","id":"linked","cwd":cwd})
),
)
.unwrap();
let native_args = [
OsString::from("--session-dir"),
session_root.as_os_str().to_os_string(),
]
.to_vec();
let (resumed, orphaned) = build_preflighted_launch_plan(
ManagedHarness::Pi,
None,
native_args.clone(),
Some("linked"),
false,
temp.path(),
&cwd,
)
.unwrap();
assert!(orphaned.is_none());
assert!(resumed.args.iter().any(|arg| arg == "--session"));
assert!(resumed.args.iter().any(|arg| arg == "linked"));
std::fs::remove_file(transcript).unwrap();
let (fresh, orphaned) = build_preflighted_launch_plan(
ManagedHarness::Pi,
None,
native_args.clone(),
Some("linked"),
false,
temp.path(),
&cwd,
)
.unwrap();
assert_eq!(orphaned.as_deref(), Some("linked"));
assert!(fresh.args.iter().any(|arg| arg == "--session-id"));
assert!(!fresh.args.iter().any(|arg| arg == "linked"));
let (explicit, orphaned) = build_preflighted_launch_plan(
ManagedHarness::Pi,
None,
[
native_args,
[OsString::from("--session"), OsString::from("chosen")].to_vec(),
]
.concat(),
Some("linked"),
false,
temp.path(),
&cwd,
)
.unwrap();
assert!(orphaned.is_none());
assert_eq!(explicit.expected_session_id.as_deref(), Some("chosen"));
}
#[test]
fn force_fresh_bypasses_an_existing_link_and_rejects_native_selectors() {
let temp = tempfile::tempdir().unwrap();
let cwd = temp.path().join("repo");
std::fs::create_dir_all(&cwd).unwrap();
let (fresh, orphaned) = build_preflighted_launch_plan(
ManagedHarness::Claude,
None,
Vec::new(),
Some("linked"),
true,
temp.path(),
&cwd,
)
.unwrap();
assert!(orphaned.is_none());
assert!(fresh.args.iter().any(|arg| arg == "--session-id"));
assert!(!fresh.args.iter().any(|arg| arg == "--resume"));
let error = build_preflighted_launch_plan(
ManagedHarness::Claude,
None,
[OsString::from("--resume"), OsString::from("chosen")].to_vec(),
Some("linked"),
true,
temp.path(),
&cwd,
)
.unwrap_err();
assert!(error.to_string().contains("--fresh cannot be combined"));
}
#[tokio::test]
async fn utility_launch_does_not_adopt_a_recent_unrelated_session() {
let temp = tempfile::tempdir().unwrap();
+6 -3
View File
@@ -137,7 +137,7 @@ pub fn build_launch_plan(
.or_else(|| environment_session_dir(harness));
let mut expected = explicit_session_id(harness, &args);
let mode = launch_mode(harness, &args);
if mode == LaunchMode::Session && !has_explicit_session_selector(harness, &args) {
if mode == LaunchMode::Session && !has_native_session_selector(harness, &args) {
match harness {
ManagedHarness::Claude => {
let id = linked_session_id
@@ -275,7 +275,7 @@ pub fn apply_yolo(harness: ManagedHarness, args: &mut Vec<OsString>) {
#[must_use]
pub fn allows_native_session_adoption(harness: ManagedHarness, native_args: &[OsString]) -> bool {
launch_mode(harness, native_args) == LaunchMode::Session
&& !has_explicit_session_selector(harness, native_args)
&& !has_native_session_selector(harness, native_args)
&& !noninteractive_invocation(harness, native_args)
}
@@ -462,7 +462,10 @@ fn launch_mode(harness: ManagedHarness, args: &[OsString]) -> LaunchMode {
}
}
fn has_explicit_session_selector(harness: ManagedHarness, args: &[OsString]) -> bool {
/// Whether the caller supplied a native resume, continue, fork, or session
/// selector. Wrapper recovery must not override an explicit native choice.
#[must_use]
pub fn has_native_session_selector(harness: ManagedHarness, args: &[OsString]) -> bool {
match harness {
ManagedHarness::Claude => has_flag(
args,
+2 -2
View File
@@ -6,10 +6,10 @@ mod transcript;
pub use harness::{
LaunchMode, LaunchPlan, ManagedHarness, allows_native_session_adoption, apply_yolo,
build_launch_plan,
build_launch_plan, has_native_session_selector,
};
pub use repository::{RepositoryIdentity, inspect_repository};
pub use transcript::{
ExportedTranscript, NativeSessionCandidate, discover_native_session, export_transcript,
list_native_sessions, wait_for_transcript_flush,
list_native_sessions, native_session_exists, wait_for_transcript_flush,
};
+99 -3
View File
@@ -167,6 +167,26 @@ pub async fn list_native_sessions(
Ok(sessions)
}
/// Check whether one exact native session still exists in the harness's
/// read-only transcript store. `Ok(false)` means the resume target is
/// definitely absent; store access or schema failures remain errors so callers
/// do not mistake an unreadable store for a deleted session.
pub fn native_session_exists(
harness: ManagedHarness,
home: &Path,
cwd: &Path,
session_dir: Option<&Path>,
native_session_id: &str,
) -> Result<bool> {
if harness == ManagedHarness::OpenCode {
return Ok(opencode_updated(home, session_dir, native_session_id)?.is_some());
}
if harness == ManagedHarness::Crush {
return Ok(crush_updated(cwd, session_dir, native_session_id)?.is_some());
}
Ok(locate_session_file(harness, home, cwd, session_dir, native_session_id)?.is_some())
}
/// Wait briefly for buffered transcript writers to settle before importing.
pub async fn wait_for_transcript_flush(
harness: ManagedHarness,
@@ -2044,10 +2064,19 @@ mod tests {
let sessions = list_native_sessions(harness, temp.path(), &cwd, Some(&root), 8)
.await
.unwrap();
let expected_id = format!("{}-id", harness.as_str());
assert_eq!(sessions.len(), 1, "{} candidates", harness.as_str());
assert_eq!(
sessions[0].native_session_id,
format!("{}-id", harness.as_str())
assert_eq!(sessions[0].native_session_id, expected_id);
assert!(
native_session_exists(harness, temp.path(), &cwd, Some(&root), &expected_id)
.unwrap(),
"{} existing session",
harness.as_str()
);
assert!(
!native_session_exists(harness, temp.path(), &cwd, Some(&root), "missing").unwrap(),
"{} missing session",
harness.as_str()
);
}
}
@@ -2097,6 +2126,26 @@ mod tests {
.collect::<Vec<_>>(),
["newer", "older"]
);
assert!(
native_session_exists(
ManagedHarness::OpenCode,
temp.path(),
&cwd,
Some(&db_root),
"newer"
)
.unwrap()
);
assert!(
!native_session_exists(
ManagedHarness::OpenCode,
temp.path(),
&cwd,
Some(&db_root),
"missing"
)
.unwrap()
);
}
#[tokio::test]
@@ -2143,6 +2192,13 @@ mod tests {
.collect::<Vec<_>>(),
["newer", "older"]
);
assert!(
native_session_exists(ManagedHarness::Crush, temp.path(), &cwd, None, "newer").unwrap()
);
assert!(
!native_session_exists(ManagedHarness::Crush, temp.path(), &cwd, None, "missing")
.unwrap()
);
let first = export_crush(&cwd, None, "newer", None).unwrap();
assert_eq!(first.events.len(), 1);
@@ -2487,6 +2543,26 @@ mod tests {
)
.unwrap();
assert_eq!(found.as_deref(), Some(wire_b.as_path()));
assert!(
native_session_exists(
ManagedHarness::Kimi,
temp.path(),
&cwd,
Some(root.path()),
"session_aaa"
)
.unwrap()
);
assert!(
!native_session_exists(
ManagedHarness::Kimi,
temp.path(),
&cwd,
Some(root.path()),
"missing"
)
.unwrap()
);
}
#[test]
@@ -2788,6 +2864,26 @@ mod tests {
)
.unwrap();
assert_eq!(found.as_deref(), Some(chat_a.as_path()));
assert!(
native_session_exists(
ManagedHarness::Grok,
temp.path(),
&cwd,
Some(root.path()),
"019f-session-aaa"
)
.unwrap()
);
assert!(
!native_session_exists(
ManagedHarness::Grok,
temp.path(),
&cwd,
Some(root.path()),
"missing"
)
.unwrap()
);
assert!(!transcript_file(
ManagedHarness::Grok,
&chat_a.parent().unwrap().join("events.jsonl")
+1 -1
View File
@@ -1177,7 +1177,7 @@ docker run --rm akitaonrails/ai-memory:latest --help # full subcommand tree
| Subcommand | Pattern | What it does |
|---|---|---|
| `serve` | `docker compose up -d` (already done) | Run the HTTP MCP server |
| `run [harness] [args...]` | host wrapper or native binary | Opt into one managed cross-harness workstream; omit the harness to resume the newest usable local session, or name Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, or Grok Build CLI explicitly; exact `--yolo` is wrapper-owned and other native arguments pass through |
| `run [harness] [args...]` | host wrapper or native binary | Opt into one managed cross-harness workstream; omit the harness to resume the newest usable local session, or name Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, or Grok Build CLI explicitly; exact `--yolo` and `--fresh` flags are wrapper-owned and other native arguments pass through |
| `workstream-search [query]` | managed child or thin HTTP client | Search the complete visible managed-workstream ledger; the managed child receives its workstream id automatically |
| `status` | `docker exec` | Counts, paths, derived-index diagnostics, and passive LLM/embedding provider health |
| `search "<query>"` | `docker exec` | Wiki search with FTS5 + graph/vector RRF |
+13 -5
View File
@@ -58,9 +58,10 @@ authoritative for an established workstream.
Implement fresh, resume, and explicit-selector behavior in
`crates/ai-memory-workstream/src/harness.rs`. Preserve every user argument and
its order except the exact wrapper-owned `--yolo` token. An explicit native
selector always wins over ai-memory's linked session. Help, version, login,
doctor, export, and similar utility commands must not receive session flags.
its order except the exact wrapper-owned `--yolo` and `--fresh` tokens. An
explicit native selector always wins over ai-memory's linked session. Help,
version, login, doctor, export, and similar utility commands must not receive
session flags.
Generate a session id only when the native CLI officially accepts a caller
provided id. Otherwise let the harness create the session, then discover it by
@@ -69,6 +70,12 @@ exact checkout and launch time. Do not infer a session from "newest globally."
Map `--yolo` only to a verified native option and avoid duplicates. If the
harness has no equivalent, add no flag and document that fact.
Support wrapper `--fresh` by checking the exact linked id in the native store
before injecting a resume selector. A confirmed missing id starts fresh; an
unreadable or malformed store remains an error rather than being treated as
absence. Reject `--fresh` when the user also supplied a native resume, session,
continue, or fork selector.
## 4. Discover and export read-only
Implement candidate discovery and incremental export in
@@ -128,8 +135,9 @@ precedence, directory renames, or conflicts.
A managed-harness PR should include focused coverage for:
- fresh launch, linked resume, explicit-selector precedence, argv order,
utility passthrough, path overrides, and `--yolo` mapping;
- fresh launch, linked resume, missing-linked-session recovery,
explicit-selector precedence, argv order, utility passthrough, path
overrides, `--yolo` mapping, and wrapper `--fresh`;
- candidate ordering, exact-checkout isolation, timestamp handling, read-only
access, incremental cursors, stable ids, incomplete records, visible record
inclusion, and private record exclusion;
+20 -7
View File
@@ -22,12 +22,14 @@ ai-memory run
```
Everything after the harness name is native argv except the wrapper-owned exact
flag `--yolo`. No `--` separator is needed, and ai-memory does not maintain a
second copy of each harness's option schema. Other wrapper options come first:
flags `--yolo` and `--fresh`. No `--` separator is needed, and ai-memory does
not maintain a second copy of each harness's option schema. Other wrapper
options come first:
```text
ai-memory run [--workspace NAME] [--project NAME]
[--workstream NAME | --new NAME] [--executable PATH] [--yolo]
[--workstream NAME | --new NAME] [--executable PATH]
[--yolo] [--fresh]
[claude|codex|opencode|pi|crush|omp|kimi|grok] [native arguments...]
```
@@ -75,6 +77,15 @@ launches without terminal input skip the chooser and start fresh. A launch that
exits before producing either a native session or portable history does not
consume the later adoption opportunity.
Before adding an ai-memory-owned resume selector, the launcher checks the exact
linked id in the harness's native store without modifying it. If the transcript
was deleted, cleared, or lost with a sandbox overlay, ai-memory starts a fresh
native session and repoints the same workstream when that session is observed.
An unreadable or malformed store is reported but is not mistaken for a missing
session. Use `ai-memory run --fresh <harness>` to deliberately skip the linked
session and the adoption chooser. `--fresh` cannot be combined with a native
resume, continue, session, or fork selector.
## What happens on each run
1. The host client resolves the normal workspace/project scope and a stable
@@ -318,10 +329,12 @@ seeded with the operator's provider configuration. The deterministic phase
also covers first-run adoption, bare-mode selection and empty-directory
failure, wrapper `--yolo`, lease exclusion, Crush context cleanup, a fake-mode
Kimi store/resume/import round trip, and the established-workstream guard
against obsolete sessions. Native session creation, read-only extraction,
cross-harness injection, and returning resume paths are all exercised. Docker
wrapper host execution and remote URL preservation are covered separately by
the `ai-memory-cli` packaging tests. Set
against obsolete sessions. The fake Kimi round trip also deletes the linked
native session and verifies automatic fresh-session recovery and repointing.
Native session creation, read-only extraction, cross-harness injection, and
returning resume paths are all exercised. Docker wrapper host execution and
remote URL preservation are covered separately by the `ai-memory-cli`
packaging tests. Set
`AI_MEMORY_ACCEPTANCE_HARNESSES="kimi-cli codex"` to select a
Kimi-to-Codex-to-Kimi round trip (Kimi aliases normalize to the installed
`kimi` executable), `AI_MEMORY_ACCEPTANCE_DETERMINISTIC_ONLY=1` to skip model
+38
View File
@@ -313,6 +313,8 @@ mapfile -t auto_claude_first <"$TMP/auto-claude-first-argv.log"
exit 1
}
auto_claude_id=${auto_claude_first[1]}
printf '{"sessionId":"%s","cwd":"%s"}\n' "$auto_claude_id" "$REPO" \
>"$AUTO_CLAUDE_HOME/projects/fixture/$auto_claude_id.jsonl"
(
cd "$REPO"
HOME="$AUTO_HOME" CODEX_HOME="$AUTO_CODEX_HOME" \
@@ -436,6 +438,42 @@ jq -e \
exit 1
}
# Deleting the linked native store must heal the established workstream instead
# of feeding the dead id back to Kimi forever.
rm -rf "$kimi_session_dir"
(
cd "$REPO"
KIMI_CODE_HOME="$KIMI_FAKE_HOME" \
AI_MEMORY_ACCEPTANCE_FAKE_MODE=kimi \
AI_MEMORY_ACCEPTANCE_ARGV_LOG="$TMP/kimi-orphan-argv.log" \
AI_MEMORY_ACCEPTANCE_SENTINEL="AMWS-FAKE-KIMI-RECOVERED" \
"$BIN" --data-dir "$DATA" run --workstream edge-kimi --executable "$FAKE" \
kimi >"$LOGS/edge-kimi-orphan.log" 2>&1
)
if grep -q . "$TMP/kimi-orphan-argv.log"; then
printf 'orphaned kimi session was not replaced by a fresh launch\n' >&2
cat "$TMP/kimi-orphan-argv.log" >&2
exit 1
fi
grep -q "linked kimi session .* is missing from its native store" \
"$LOGS/edge-kimi-orphan.log"
kimi_recovered_dir=$(find "$KIMI_FAKE_HOME/sessions" -mindepth 2 -maxdepth 2 -type d -print -quit)
[ -n "$kimi_recovered_dir" ] || {
printf 'orphan recovery did not create a replacement kimi session\n' >&2
exit 1
}
kimi_recovered_id=$(basename "$kimi_recovered_dir")
[ "$kimi_recovered_id" != "$kimi_session_id" ] || {
printf 'orphan recovery reused the deleted kimi session id\n' >&2
exit 1
}
kimi_current_id=$(sqlite3 "$DATA/db/memory.sqlite" \
"SELECT native_session_id FROM workstream_native_sessions WHERE workstream_id = x'${kimi_ws_hex}' AND agent_kind = 'kimi-code' AND is_current = 1;")
[ "$kimi_current_id" = "$kimi_recovered_id" ] || {
printf 'orphan recovery did not repoint the kimi workstream\n' >&2
exit 1
}
# Grok fake-mode fixture: the fake grok honors the wrapper's `--session-id`
# (fresh) and `--resume <id>` (returning) selectors, writes the native store
# layout ($GROK_HOME/sessions/<bucket>/<id>/summary.json plus