mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
fix(run): recover orphaned native sessions
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user