- Adds HandoffAcceptStatus enum ('claimed' | 'consumed_by_hook' | 'none_pending')
returned alongside 'handoff' in memory_handoff_accept (#988, split from #920).
- Adds ReaderPool::handoff_claimed_by_live_session to verify if the caller's
live session already received the handoff via SessionStart hook.
- Reports 'consumed_by_hook' only when the caller forwards its session id
(e.g., Claude Code with --session-aware or OpenCode 2) and matches the
scope, live session, and owner filters.
- Reports 'none_pending' when no handoff was pending or for static clients
where the hook claim cannot be confirmed for that exact session.
- Updates documentation, routing skill, ARCHITECTURE.md, security boundaries (row 4g),
and CHANGELOG.md.
Adds a hermes skill-root family to install-skills (project .hermes/skills,
global ~/.hermes/skills) and the matching hermes entry in
memory_install_self_routing's target_hints. Hermes needs no special-casing:
both roots fall out of base.join(agent_dir).join(SKILLS_DIR), the same as the
other agent families. Completes Hermes routing support alongside its lifecycle
hooks (#933).
Closes#942
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
Shell tools (Bash, shell, execute_bash, terminal, ...) were classified as
non-file and always kept under an active capture policy, so a command
such as `cat docs/adr/*.md` stored an ignored file's full text in the
post-tool-use observation body.
The native hook now tokenizes the command line lexically, resolves each
path-like argument against the event cwd, and drops the event when one
matches an ignore pattern or is a glob that can reach one. Protocol
fields stay unchanged for non-file tools, so server re-inspection keeps
agreeing and no server change is needed.
The marker-file reference documents the lexical limits and how to
exclude large tool results that Claude Code saves and re-reads from
another path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A multi-page batch writes each update at a path the model chose. When
that path named an existing pinned page, apply_batch replaced its body
and wrote the new version with pinned = 0, because neither the batch
loop nor the wiki write path looks at the page being replaced. Pinned
pages are documented as immutable to automation.
Skip such updates with a warning, next to the existing invariant-slot
skip. _slots/ pages are pinned automatically and keep their own
state/invariant regime, so they are not skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hbGY9oRywtM3Nz2NcXxUF
Sessions imported by `backfill` before it carried per-event timestamps all
have started_at/ended_at on the import day, which flattens every
time-ordered view of that history. This adds an admin repair for sessions
already imported; it is independent of the companion fix to `backfill`
itself (#919).
The repair runs online, through the server's single writer, because it does
not need direct disk access (invariant 9). POST /admin/repair-session-times
validates a caller-supplied batch of (session_id, started_at, ended_at)
candidates in one transaction, scoped to (workspace, project) like
purge-session. It only rewrites a row whose current started_at postdates
the candidate's own transcript end, the bug's signature, so a correctly
hook-captured session or a re-run is a no-op (`not_flattened` /
`unchanged`). Without confirm it is a real dry run: the write runs and
rolls back. Out-of-scope sessions are reported not-found and left
untouched; negative, inverted or future times are refused; an open session
never gets an end time; and a confirmed batch writes one audit_log row with
every repaired session's before/after times.
`ai-memory repair-backfill-timestamps` is the thin client. It reuses
backfill's session discovery and the workstream transcript reader, takes
each session's earliest and latest event time, resolves native ids with the
new `SessionId::from_native` (now shared with the hook router instead of a
private copy), chunks the batch at the server's cap, and prints each
repaired session's old and new times.
The accessory app ships the ai-memory binary and hooks tree, starts the
existing LaunchAgent, and opens /web, status, config, and logs. Durable
data stays in Application Support so replacing the .app is an update.
A blank `session_id` in `memory_consolidate` is now the same request as an
omitted one: it resolves the latest completed session of the resolved project,
the default `memory_read_session_observations` already applies to a blank id.
A malformed id answers `-32602` instead of `-32603`, the code
`memory_auto_improve` already uses for the same argument; its message is
unchanged.
Scope-resolution failures over MCP map `is_bad_request()`/`is_not_found()` to
`invalid_params`, the same split the web route applies with its 400/404; only a
missing writer handle or an underlying store failure stays an internal error.
Consolidation LLM calls retry a transient provider failure (429, any 5xx, a
transport timeout or connect failure) twice, two seconds apart, the bounded
policy `bootstrap` already applies to its chunks. Deterministic failures are
still reported on the first attempt.
An omitted `session_id`, or an explicit `null`, failed MCP parameter
deserialization with `-32602` and the message `missing field session_id`
before the handler ever ran, making this the only tool in the cluster with no
natural default.
`ConsolidateArgs::session_id` is now `Option<String>` with
`#[serde(default)]`, and the handler resolves an omitted id to the latest
completed session of the resolved project via
`latest_completed_session_for_project` — the same default
`memory_auto_improve` and `memory_read_session_observations` already use. A
project with no completed session fails with `no completed session in
<scope>; pass session_id to consolidate a specific session`. An explicit id
keeps the previous behaviour unchanged, including the existing error for an
empty string.
Docs and the tool description now state the omitted-field behaviour, and the
CHANGELOG records it under [Unreleased].
Add directed, claim-once cross-project messaging so an agent in one project
can hand a self-contained request to an agent in another project without
pulling that project's context into its own session. This is the one place
ai-memory deliberately crosses per-project isolation, so the crossing is
explicit and bidirectionally scoped: a project only ever sees mail addressed
TO it (inbox) or sent FROM it (outbox).
- Schema: V64 agent_messages table (pending/claimed/cancelled), pin -> 64.
- Core: AgentMessage/NewAgentMessage/MessageClaim/MessageBox/MessageState +
UNTRUSTED_MESSAGE_NOTICE.
- Store: insert_message (inbox depth cap), pop_message (claim-once via the two
state='pending' guards, mirroring accept_handoff), cancel_messages;
list_messages + pending_message_count; pending_message_count on
BriefingSnapshot.
- MCP: memory_message_send / _list / _pop / _cancel (surface 19 -> 23), added
to MCP_TOOL_NAMES, DETAILED_ROUTING_TOOL_NAMES, read/write classification,
MEMORY_INSTRUCTIONS, SNIPPET managed skills (new ai-memory-messaging skill),
and AdmissionOp (MessageSend/Pop/Cancel).
- CLI + REST: ai-memory message send|list|pop|cancel over new /admin/messages*
routes.
- Hooks: non-consuming, count-only on-start inbox notice in the /handoff block.
Security (anti prompt-injection into live harnesses): a popped message is
untrusted cross-project input. Bodies are secret-scrubbed and size-capped on
send (both MCP and REST paths), fenced with a security notice and sender
provenance on pop, never auto-injected (the on-start notice carries only a
count), recipient lookup fails closed, and inbox depth is bounded.
Docs: docs/agent-messaging.md, ARCHITECTURE (schema/tools/count/CLI), usage.md,
CHANGELOG, AGENTS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
The routing snippet distinguishes a reviewed decision record in the
repository (an ADR directory, a Keep the Why context/ tree) from a
harness-local memory store: decisions go there under the project's
convention, ai-memory keeps recall, handoffs and session history and does
not duplicate the record as a page. AGENTS.md managed block regenerated
to match. docs/usage.md: the ADR section becomes "Repo-native decision
records" and names both shapes; it and docs/marker-file.md say to list
the directory in [capture] ignore_paths, and why.
The managed routing snippet directs agents to write durable project rules
into "the project's canonical agent instruction file", and both the CLI
hint and the docs steer that file toward AGENTS.md. Claude Code loads
CLAUDE.md and does not read AGENTS.md, so a project that follows the
recommendation without a bare `@AGENTS.md` import line in CLAUDE.md keeps
its canonical rules out of context at session start. They stay reachable,
since an agent acting on a prose "read AGENTS.md" pointer opens the file,
which makes adherence contingent on the agent choosing to read them.
State the precondition in SNIPPET_BODY, mirror it beside the
`--target AGENTS.md` guidance in docs/install.md and docs/usage.md, and
switch this repository's own CLAUDE.md from a prose pointer to the import.
The committed AGENTS.md managed block is regenerated to match SNIPPET_BODY,
as committed_agents_md_matches_snippet_body requires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE9rQpsjqNK5mmpqmggL7
Grok, Zero, and MCP-only clients discard SessionStart stdout, so the only
way to see a pending baton was memory_handoff_accept, which claims it.
List is owner-filtered and read-only; accept now takes an optional
handoff_id so the inspected row is claimed exactly once.
Forward-merge of the nine PRs that landed on main (the 2.0.4 batch: #642 auth
stale-bearer, #646/#640 LoginLimiter, #644 Cursor attribution, #650/#647
reindex manifest, #638 MCP routing, #652 CI docs, #645 dev-loop/build) into the
2.1 feature train, so release/2.1 carries every fix before 2.1.0 is cut.
Conflicts resolved:
- crates/ai-memory-wiki/src/wiki.rs: 2.1's per-page write lock (page_locks,
#607) and main's manifested_scopes memo (#650) are independent additions to
the same struct/imports/constructor — kept both; imports merged to
{HashMap, HashSet}.
- CHANGELOG.md: [Unreleased] now carries 2.1's ### Added features above main's
### Changed + ### Fixed (the 2.0.4 fixes), Keep-a-Changelog order, single
[2.0.3] section preserved.
- crates/ai-memory-llm/tests/extra_headers_on_the_wire.rs (2.1's #606 test)
relocated into tests/suite/ and declared in mod.rs to satisfy #645's
one-test-binary-per-crate harness convention (caught by the repo_layout guard).
fmt, clippy -D warnings, llm harness, and the repo_layout guard all green.
* feat(auth): add browser sessions and API credentials
* fix(auth): preserve 1.x compatibility paths
* fix(changelog): preserve released sections
* docs(auth): tie mirror triggers to 2.0 cutover
* docs(store): correct mirror migration version
* docs(changelog): link admin console PR
* fix(migrations): renumber human_auth/api_credentials to V54/V55
Main gained V52__purged_sessions_tombstones and V53__page_embed_failures
after this branch was opened, so the merge produced four migration files
across two version numbers. Git saw no conflict - the filenames differ -
and the collision only surfaces at runtime:
UNIQUE constraint failed: refinery_schema_history.version
on a fresh database, so the merged tree could not open a store at all.
Renumbered V52__human_auth -> V54 and V53__api_credentials -> V55, and
shifted the version numbers the tests pin: `run_to`/`open_to` targets, the
`schema_version` assertions, the rollback assertion, and the test names.
The pre-migration fixture point moves 51 -> 53 because main's V52/V53
create unrelated tables (purged_sessions, page_embed_failures) and touch
neither `users` nor any table these migrations rewrite.
Migration content is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
---------
Co-authored-by: AkitaOnRails <fabioakita@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Add `POST /admin/move-session` and `ai-memory move-session`. The single
form names a session id, the batch form `from_project` (every session of
that scope, one store transaction each, stopping at the first refusal and
reporting `moved`/`total`). The destination is resolved with the scope
helpers (404 unless `create`), a same-scope target is 422, and the guards
refuse with 409 unless `force`: an open session, a `pending`/`running`
consolidation job, and, for the batch, the hook router's active source
project. Without `confirm` the writer runs the move with `commit = false`
and the response is the exact dry run (`dry_run: true`); the CLI prints
the summary and the command to apply. With `confirm` the handler
checkpoints, calls `Wiki::move_session_page` (admission, file move, SQL
re-stamp, file rollback), backfills scope manifests and checkpoints
again. `PagePathTaken` and an existing destination page file map to 409.
The report carries per-table counts, the page disposition
(`moved` | `regenerated` | `none`), the untouched `cwd`, and a
`cwd_warning` when its basename is not the destination project.
Docs: lifecycle-ops (safety matrix, command section, operator workflow),
usage, admission-webhooks (`move_session` op), README/ARCHITECTURE command
lists, CHANGELOG.
Add first-party lifecycle capture for the AWS Kiro CLI (#355). The
binary ships two agent engines with incompatible hook surfaces, so each
is selected explicitly and never guessed:
- `install-hooks --agent kiro-cli` merges five flat camelCase entries
(agentSpawn, userPromptSubmit, preToolUse, postToolUse, stop) into
every existing v2 agent config under ~/.kiro/agents/. Entries carry no
matcher key (absent = every tool; empty string matches nothing) and no
type key (the v2 Hook schema has neither), and agentSpawn raises
max_output_size to 64 KiB so an injected handoff + brief is not
truncated. The v2 engine has no global hook surface and the built-in
default agent has no file on disk, so the installer updates existing
configs and bails with guidance when none exist instead of inventing
an agent that would never be active.
- `install-hooks --agent kiro-cli-v3` writes the standalone versioned
hooks file ~/.kiro/hooks/ai-memory.json (PascalCase triggers, command
actions, timeout in seconds) for the early-access v3 engine, never
touching other files in the hooks directory and preserving
non-ai-memory entries inside ours.
Both surfaces honor $KIRO_HOME (verified against kiro-cli 2.16.0),
share one hooks/kiro-cli script bundle, uninstall cleanly, and keep
capture fail-open: hooks always exit 0 and print nothing on capture
paths, because exit code 2 blocks the tool call and session-start /
user-prompt stdout is added to the agent context on both engines. For
the same reason the native `ai-memory hook` command suppresses its `{}`
protocol line for kiro-cli and prints a fetched session-start handoff
raw (no hookSpecificOutput envelope — Kiro documents none).
Kiro tool payloads (tool_name/tool_input, tool_response.success) join
the capture policy with fixture vectors, including fs_read's batched
operations path extraction; fs_read/fs_write/execute_bash join the tool
family map. The v3 stdin payload shape is not publicly documented and
extraction stays verified on the v2 shape only (documented as such).
Also add `?flavor=bedrock` as an alias of the Moonshot root-schema
flattening: Kiro talks to Amazon Bedrock's Converse API, which rejects
root-level anyOf/oneOf/allOf in tool schemas, so a manually configured
~/.kiro/settings/mcp.json can point at …/mcp?flavor=bedrock (#351).
Contracts verified against kiro-cli 2.16.0 (binary surface probed
locally), kiro.dev's v2/v3 hook references, and the shipping HookTrigger
implementation in aws/amazon-q-developer-cli.
Closes#355.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJvj7D7czyZzqk4pDXgLxY