fix(install-hooks)+docs: preserve capture-assistant on re-apply; 2.3 release-audit doc fixes

Post-audit of release/2.3 (v2.2.2..HEAD) before the 2.3.0 release.

Fix (S1, the one code finding): install-hooks re-applied with no
--capture-assistant flag silently stripped an existing assistant-capture
opt-in — most visibly through the new `ai-memory run` auto-wire, which always
re-applies without the flag. install-hooks now preserves an already-baked
--capture-assistant on a bare apply (Claude Code + Codex, via the same
existing-config read that prompt-capture already uses); an explicit flag still
forces it on. Regression tests: bare re-apply preserves the opt-in; a fresh
install without the flag stays off. (The underlying non-preservation is also
latent on main and can be backported to a 2.2.x patch if desired.)

Docs (release-readiness):
- README: add the agent-messaging.md row to the Docs table (new feature + 4 MCP
  tools shipped without a README entry); note run-as-preferred/auto-wire.
- ARCHITECTURE config reference: document capture_assistant, backfill_on_start,
  run_autowire + their AI_MEMORY_* env overrides.
- install.md: add an "Upgrading to 2.3.0" note (the two default-on behaviors +
  opt-outs) and a forward-only/backup-before-rollback note.
- design-boot-backfill.md: reconcile the now-answered "Open questions" with the
  as-built implementation (path B, on-by-default, capped).
- design-rules-promotion.md: drop the stale "(2.1)" target label (untargeted,
  still unimplemented).
- CHANGELOG: add the install-hooks preservation Fixed entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
This commit is contained in:
AkitaOnRails
2026-09-16 12:28:07 -03:00
co-authored by Claude Opus 4.8
parent da8d07dcc9
commit 9c2e793503
7 changed files with 194 additions and 15 deletions
+6
View File
@@ -91,6 +91,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
OpenAI-compatible contract Copilot documents for chat (#739).
### Fixed
- `install-hooks --apply` now preserves an existing `--capture-assistant` opt-in
on a bare re-apply (one with no `--capture-assistant` flag). Previously a
refresh without the flag silently stripped assistant capture; this most
affected the new `ai-memory run` auto-wire, which always re-applies without the
flag. The flag still explicitly enables it; an unset flag now keeps whatever is
already installed (Claude Code and Codex).
- `install-hooks --apply` no longer aborts the entire install when the hook
bearer token cannot be persisted under the data dir. This bit the docker
wrapper, where `data_dir` is `/data` — a container volume the host hooks
+2 -1
View File
@@ -343,7 +343,8 @@ diagram, crate breakdown, schema notes, and invariants.
| [`docs/cookbook.md`](docs/cookbook.md) | **Task-oriented cheat sheet.** "I want to do X" → how: recall prior work, keep a rule a project must follow, import an existing knowledge base (OKF norms/specs) and have a project read a specific document, and get two agents/repos working together. Start here if you're unsure what ai-memory can do for you. |
| [`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, OpenCode 2 beta, Pi, Crush, Kimi Code, Command Code, Kiro CLI v2/v3, OMP, Grok Build CLI, and Antigravity CLI: 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, OpenCode 2 beta, Pi, Crush, Kimi Code, Command Code, Kiro CLI v2/v3, OMP, Grok Build CLI, and Antigravity CLI: automatic harness selection, native resume, argument forwarding, ledger search, privacy, and recovery. The preferred way to launch — it auto-installs a harness's hooks + MCP on first run. |
| [`docs/agent-messaging.md`](docs/agent-messaging.md) | Cross-project agent-to-agent messaging: a directed, claim-once inbox/queue so an agent in one project can hand a self-contained request to an agent in another, plus the on-start "you have mail" notice. Four `memory_message_*` MCP tools + `ai-memory message` CLI. |
| [`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. |
@@ -496,6 +496,20 @@ pub fn run(config: &Config, mut args: InstallHooksArgs) -> Result<()> {
break cross-agent continuity."
);
}
// Preserve an existing `--capture-assistant` opt-in on a bare re-apply. There
// is no negative flag, so a re-run without `--capture-assistant` (notably the
// `ai-memory run` auto-wire, which always passes it off) must NOT silently
// downgrade a user who had enabled assistant capture. An explicit
// `--capture-assistant` still forces it on; this only fills in the unset case
// from what is already installed. Gated to the agents where assistant capture
// is allowed (Claude Code, Codex), whose configs share the nested-hooks shape.
if args.apply
&& !args.capture_assistant
&& capture_assistant_allowed(args.agent)
&& existing_capture_assistant_opt_in(&args)
{
args.capture_assistant = true;
}
if args.apply {
// #446: settle the capture failure mode before any agent-specific
// work, and say which mode is in force. A protection the operator
@@ -871,6 +885,54 @@ fn baked_claude_prompt_capture(existing: &str) -> Option<bool> {
)
}
/// Whether the currently-installed config for this agent already bakes the
/// `--capture-assistant` opt-in. Used to preserve that opt-in across a bare
/// re-apply (e.g. `ai-memory run` auto-wire) instead of dropping it. Only the
/// agents where assistant capture is allowed (Claude Code, Codex) reach here,
/// and both write the nested-hooks JSON shape.
fn existing_capture_assistant_opt_in(args: &InstallHooksArgs) -> bool {
existing_agent_config(args)
.as_deref()
.and_then(baked_capture_assistant)
.unwrap_or(false)
}
/// `Some(true|false)` when `existing` is a recognizable ai-memory install and
/// whether its Stop command carries `--capture-assistant`; `None` when it is not
/// an ai-memory install at all (so there is nothing to preserve).
fn baked_capture_assistant(existing: &str) -> Option<bool> {
let document: serde_json::Value = serde_json::from_str(existing).ok()?;
let hooks = document.get("hooks")?.as_object()?;
let mut saw_ai_memory = false;
let mut has_marker = false;
for entries in hooks.values().filter_map(serde_json::Value::as_array) {
for entry in entries {
// Nested (`hooks:[{command}]`) or flat (`{command}`) shape.
let nested = entry.get("hooks").and_then(serde_json::Value::as_array);
let commands: Vec<&str> = match nested {
Some(inner) => inner
.iter()
.filter_map(|e| e.get("command").and_then(|c| c.as_str()))
.collect(),
None => entry
.get("command")
.and_then(|c| c.as_str())
.into_iter()
.collect(),
};
for command in commands {
if hook_command_is_ours(command) {
saw_ai_memory = true;
if command.contains("--capture-assistant") {
has_marker = true;
}
}
}
}
}
saw_ai_memory.then_some(has_marker)
}
/// Read the config file `--apply` will update for the selected agent.
fn existing_agent_config(args: &InstallHooksArgs) -> Option<String> {
let path = if let Some(path) = &args.config_file {
@@ -7965,6 +8027,73 @@ model = "gpt-5"
);
}
fn claude_apply_args(
settings: &std::path::Path,
hooks_dir: &std::path::Path,
capture_assistant: bool,
) -> InstallHooksArgs {
InstallHooksArgs {
agent: AgentChoice::ClaudeCode,
apply: true,
capture_assistant,
server_url: Some("http://127.0.0.1:49374".to_string()),
config_file: Some(settings.to_path_buf()),
hooks_dir: Some(hooks_dir.to_path_buf()),
..default_hook_args()
}
}
/// Regression for the auto-wire capture-downgrade (post-audit S1): a bare
/// `--apply` without `--capture-assistant` — exactly what `ai-memory run`
/// auto-wire issues — must PRESERVE a user's existing assistant-capture
/// opt-in, not silently strip it.
#[test]
fn a_bare_reapply_preserves_an_existing_capture_assistant_opt_in() {
let home = TempDir::new().unwrap();
let cfg_dir = TempDir::new().unwrap();
let settings = cfg_dir.path().join("settings.json");
std::fs::write(&settings, "{}").unwrap();
let config = crate::config::Config::load(None, Some(home.path().to_path_buf())).unwrap();
let hooks_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../hooks");
run(&config, claude_apply_args(&settings, &hooks_dir, true)).expect("first install");
assert!(
std::fs::read_to_string(&settings)
.unwrap()
.contains("--capture-assistant"),
"the explicit opt-in must be baked on the first install"
);
// A bare re-apply (the auto-wire shape: capture_assistant = false).
run(&config, claude_apply_args(&settings, &hooks_dir, false)).expect("bare re-apply");
assert!(
std::fs::read_to_string(&settings)
.unwrap()
.contains("--capture-assistant"),
"a bare re-apply must preserve the existing --capture-assistant opt-in"
);
}
/// The preserve logic must not false-positive: a fresh install without the
/// flag stays off.
#[test]
fn a_fresh_install_without_the_flag_leaves_assistant_capture_off() {
let home = TempDir::new().unwrap();
let cfg_dir = TempDir::new().unwrap();
let settings = cfg_dir.path().join("settings.json");
std::fs::write(&settings, "{}").unwrap();
let config = crate::config::Config::load(None, Some(home.path().to_path_buf())).unwrap();
let hooks_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../hooks");
run(&config, claude_apply_args(&settings, &hooks_dir, false)).expect("fresh install");
assert!(
!std::fs::read_to_string(&settings)
.unwrap()
.contains("--capture-assistant"),
"a fresh install without the flag must not enable assistant capture"
);
}
#[test]
fn hook_source_candidates_include_native_package_dir() {
let candidates = hook_source_candidates(
+16
View File
@@ -550,6 +550,22 @@ prefixed `AI_MEMORY_*`.
bind = "127.0.0.1:49374"
log_level = "info"
# Capture / launch UX (all default-on where noted). Each has an AI_MEMORY_* env
# override (AI_MEMORY_CAPTURE_ASSISTANT / AI_MEMORY_BACKFILL_ON_START /
# AI_MEMORY_RUN_AUTOWIRE).
capture_assistant = false # server-side opt-in: honor a Claude Code / Codex
# client's sanitized assistant-final-message marker
# on Stop (#196). Client half is baked separately by
# `install-hooks --capture-assistant`.
backfill_on_start = true # on first SessionStart in a brand-new (empty) project,
# import that project's existing local harness history
# once so hooks-mid-project isn't amnesiac. Only ever
# bootstraps an empty project; hard-capped. `ai-memory
# backfill` runs it by hand.
run_autowire = true # `ai-memory run <harness>` auto-installs that harness's
# hooks + MCP on first launch if missing (idempotent,
# one-time per harness+version). Also `--no-autowire`.
[decay] # M8 retention params
lambda = 0.02 # ↓ to forget less aggressively
sigma = 0.6 # ↑ to reward query-hits more
+15 -10
View File
@@ -218,17 +218,22 @@ cross-link them — doctor is the ongoing check, backfill is the one-time bootst
- **Multi-session**: two concurrent boots → exactly one import (lease claim-once), the
other no-ops — the invariant-#16 concurrency shape, proven at integration level.
## Open questions for the plan
## Open questions — resolved as built
1. **Ingest path A vs B** — confirm the lifecycle-free workstream-import endpoint is worth
adding versus an observation replay. (Recommend A for the cursor.)
2. **Shared-server default** — auto-on everywhere, or auto-on only in the single-operator
posture with explicit `ai-memory backfill` required on multi-user servers?
3. **Notice delivery** — Claude discards SessionStart stdout; reuse the same
"deliver on first user prompt" path the hot-context block already uses for Kimi/Claude?
4. **Consolidation quality** — should a first pass gate on a small eval that backfilled
pre-hook ledgers produce pages of comparable quality to forward capture before making
full-project bootstrap the default, or ship behind the cap and iterate?
These were the pre-implementation questions; the shipped feature (see the **As-built
note** at the top) resolved each:
1. **Ingest path A vs B** — resolved to **B** (replay through `/hook`). A live smoke proved
path A populated the workstream continuity ledger, not the searchable memory pipeline
(observation count stayed zero), so it was abandoned.
2. **Shared-server default** — shipped **on by default** with `AI_MEMORY_BACKFILL_ON_START=false`
/ `--no`-style opt-out; the emptiness gate makes it safe on any posture (it only ever
bootstraps an empty project).
3. **Notice delivery** — the automatic path runs detached and silent (like Claude Code's
own auto-memory); the summary shows on a manual `ai-memory backfill`. Surfacing it in
the next session's on-start context remains a possible follow-up.
4. **Consolidation quality** — shipped **behind the hard cap** (newest 25 sessions / 50k
events) and iterating; a dedicated eval remains a follow-up.
## Semver
+4 -3
View File
@@ -1,7 +1,8 @@
# Design: Promoting Memory into Governed AGENTS.md Rules (2.1)
# Design: Promoting Memory into Governed AGENTS.md Rules
*Status: design, targeting the `release/2.1` line. Not implemented yet — this
is the balance research the feature needs before code.*
*Status: design proposal — not implemented. (Originally scoped against the 2.1
line; that and 2.2 shipped without it, so it is un-targeted and still open —
this is the balance research the feature needs before code.)*
## 1. The idea, and the trap
+22 -1
View File
@@ -2376,7 +2376,28 @@ the wrapper requires `<url>.sha256` unless
When the upgraded server starts, it applies SQLite schema migrations and
pending wiki-structure migrations automatically. No manual database
reset or wiki rewrite is required for normal upgrades.
reset or wiki rewrite is required for normal upgrades. Migrations are
forward-only: after a newer version has applied its schema, an older binary
will refuse to open that data dir (it fails closed rather than risk
corruption), so take a `ai-memory backup` before upgrading if you might need
to roll back to the previous version.
### Upgrading to 2.3.0
2.3.0 is a normal forward upgrade (the only new migration, `V64`, just adds the
cross-project `agent_messages` table — nothing existing is altered or removed).
Two capture/UX conveniences are **on by default**; both are additive and
non-destructive, but worth knowing about for your first session after upgrading:
- **First `ai-memory run <harness>` auto-installs that harness's hooks + MCP** if
they were not already wired (idempotent, one-time per harness; it preserves
your existing hook config, including a `--capture-assistant` opt-in). Disable
with `ai-memory run --no-autowire` or `AI_MEMORY_RUN_AUTOWIRE=false`.
- **The first session in a brand-new (empty) project imports that project's
existing local harness history once**, so installing ai-memory mid-project is
not amnesiac. It only ever runs on an empty project (never touches one that
already has captured memory) and is hard-capped. Disable with
`AI_MEMORY_BACKFILL_ON_START=false`; run it by hand with `ai-memory backfill`.
If the server runs on another host, `ai-memory upgrade` refreshes only
the local wrapper, local image, and local hook scripts. Redeploy the