The native hook, hooks/_lib.sh, hooks/lib/ai-memory-hook.ps1 and the
generated TypeScript plugins resolve the identity host-side, skip it
when the marker declares project or identity, and are each checked
against the core's shared fixture. New marker key: identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqFKAuGVkuBoewmpA3cx9
/hook, /hook/batch items and /handoff accept identity / identity_src;
only the explicit and git_remote rungs route by identity, anything else
routes by name as before. The path-keyed project cache includes the
identity, so one path holding two repositories on two machines does
not answer for both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqFKAuGVkuBoewmpA3cx9
ai-memory-core gains the repository-identity resolution chain (marker
identity, marker project, upstream/origin remote normalised with
credentials stripped, folder name) and a shared fixture of remote-URL
cases. V70 adds projects.identity / identity_source with a partial
unique index per workspace and no backfill (case-only name clashes
would fail the upgrade). resolve_project_by_identity matches, claims in
place when the caller may write (slice 2's resolve_project_authz on
the same transaction), leaves it unclaimed otherwise, or splits a
different repository into owner-named projects; created projects
record created_by.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqFKAuGVkuBoewmpA3cx9
The example used to ask which document was most relevant, which pulled
index pages to the top. The replacement asks for the page that contains
the answer and keeps the choice-versus-rubric numbers labeled separately.
Without --confirm, purge-project only refused with a generic
"destructive and irreversible" message; the counts of what would be
deleted appeared only after a confirmed run actually deleted them. An
operator purged a project believing it held 0 sessions / 0 pages, and
it actually held 1,063 observations reachable only through a different
project's session (a pre-#871 Windows path-casing split) -- recovered
only from a backup taken a minute earlier.
POST /admin/purge-project now accepts an optional "dry_run": true,
which always wins over "confirm" -- {"confirm": true, "dry_run": true}
still only previews, the same way reclaim-ledger-versions treats its
own dry_run field, so a preview request can never become destructive.
The preview runs the same lookups and counts a confirmed purge uses to
decide what to delete (same 404/409), including two new counts for
rows a purge collaterally deletes or orphans in a DIFFERENT project via
its sessions cascading (collateral_observations_deleted,
collateral_handoffs_denulled -- observations.session_id is ON DELETE
CASCADE and handoffs.from_session_id/accepted_by_session are ON DELETE
SET NULL, neither scoped to the purged project_id). It never issues the
DELETE: PurgeMode::{Commit, Preview} replaces the earlier commit: bool,
and Preview returns right after counting instead of running the delete
and rolling it back, so a preview of a large project doesn't hold the
single-writer actor for as long as a real purge would and starve every
hook capture queued behind it. Preview also skips wiki file removal,
admission webhook dispatch, and both checkpoints; a 200 preview is
therefore not a guarantee the confirmed purge will succeed, since
admission only runs on the confirmed path.
The CLI asks for this preview (bounded to a few seconds, including auth
refresh) before refusing, and prints "Would purge <ws>/<proj>: N pages,
..." plus any collateral-damage counts. A 404/409/403 (or anything else
unexpected) prints the server's own error before the refusal; a plain
400 (older server), a timeout, or an unreachable server fall back
silently to the existing refusal, so no existing script's behavior
changes except gaining information.
Store-level tests cover: preview reports the same counts a confirmed
purge then produces; preview changes nothing (every project-scoped
table's row count, including purged_scopes and audit_log); the incident
shape (an observation stamped into the purged project from a session
that lives elsewhere); and the mirror shape (purging a project
collaterally deletes an observation, and orphans a handoff's session
reference, in a different project). Admin-level tests add the HTTP
equivalents, confirm {"confirm": true, "dry_run": true} deletes
nothing, no webhook dispatch on preview, 404 parity, a 409 on a live
managed-run lease without --force, and root-vs-DB-user-vs-anonymous
auth on the preview payload. CLI tests cover the summary-line wording,
the pure outcome-to-message mapping, and run_preview's HTTP-status
classification against a real local server (400/404/200/connection
failure).
One choice question over the whole candidate list instead of one rubric
score per candidate; choice probability maps to relevance, which is sound
because the reranker leg is sort-only. +14/+21 hit@1 over the rubric shape
on the same 35B/4B backends (live golden set), 3x lower rerank latency,
and the only shape where replay-trained small judges hold their quality.
Scopes the absolute-relevance caveat to consumers that read absolute
values.
Hermes runs a configured hook command through shlex.split with the event JSON on stdin and no shell, so its integration is exec-form (like Zero and ZCode) rather than a .sh bundle: AgentChoice::Hermes (alias hermes-agent) makes install-hooks, setup-agent and finalize-session accept it, and no script subdir is staged.
build_hermes_hooks_yaml emits the ready-to-paste hooks: block for ~/.hermes/config.yaml with the two events ai-memory can act on - pre_tool_call -> pre-tool-use and post_tool_call -> post-tool-use. Their tool_name / tool_input payload is the envelope the router already maps for agent=hermes, which is what gives Hermes sessions tool observations, tool-family titles and capture exclusions.
install-hooks --agent hermes never writes the config file: it is YAML the operator also edits and Hermes gates user hooks behind its own acceptance prompt (hooks_auto_accept), so the block is printed for pasting - the same choice made for Pool. Session lifecycle is deliberately left to the memory-provider plugin, so no hook-driven session-end is installed and a session cannot be closed twice.
Verified against Hermes v0.21.4: the block parses through Hermes own hermes_yaml, _parse_hooks_block registers both events, split_command_line yields an argv that runs the native hook subcommand, and that command spools the event with rc=0. Docs updated in docs/install.md, docs/support-matrix.md and docs/mcp-install.md. Follow-up to #623.
(cherry picked from commit 18155089cd)
Add the online reclaim command to the safety matrix and a
command-by-command section (dry-run default, content gate, --compact
to reclaim bytes), the operator guide AGENTS.md requires for new
destructive lifecycle ops. Follow-up to #927 (#914).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
A named profile (`--profile`, `OMP_PROFILE`, or the legacy `PI_PROFILE`) owns
`~/.omp/profiles/<name>/agent` and ignores `PI_CODING_AGENT_DIR`, as OMP does;
`install-hooks` wrote the extension into `PI_CODING_AGENT_DIR` instead. Profile
names are normalized and refused like OMP does, `PI_CONFIG_DIR` renames the
`~/.omp` root, and on Linux and macOS sessions move to `$XDG_DATA_HOME/omp`
when that directory exists. `install-mcp`, auto-wire, session import,
`backfill`, `doctor` and the `uninstall` sweep follow the same agent dir.
Refs #820
#660 stopped the indexer from superseding an OKF-conformed ledger on every
hook append, but the rows it already wrote stay and nothing removed them:
`compact` deletes nothing, `forget-sweep` only hard-deletes decay
tombstones (only `decay` writes `superseded_at`), and `reindex` loses the
DB-only state. One reported store holds 6,539 versions of 101 live pages,
95% of its rows.
Adds `ai-memory reclaim-ledger-versions` (`POST
/admin/reclaim-ledger-versions`), an online command that deletes only the
superseded versions of paths whose *content* opens with a hook log entry.
Dry run unless `--confirm`; `--drop-latest` also removes each ledger's live
row; `--compact` rebuilds the FTS index and VACUUMs to return the bytes.
The content gate is the load-bearing part and is now one definition:
`ai_memory_core::log_ledger` owns the predicates, `ai_memory_wiki::ledger`
keeps the file-reading half and delegates, and the store decides from a
`pages.body` with no filesystem access. A real page a human named
`log-2026-09.md` keeps its whole version chain. Only `is_latest=0 AND
superseded_at IS NULL` rows are eligible, so decay-owned rows stay with the
sweep that reasons about them.
Derived FTS/entity/vector/link rows go with the page through the existing
ON DELETE CASCADE. The FTS delete trigger is stood down for the bulk delete —
its DDL read back from sqlite_master and re-executed, so it cannot drift
from the schema — and pages_fts is rebuilt wholesale, which is what keeps
this from re-tokenizing tens of gigabytes of ledger body row by row.
Reading the gate needs a bounded prefix, not the whole body: GATE_PREFIX_BYTES
lives with the predicate, shared by the file reader and the store.
Refs #914
ai-memory user grant|revoke|grants, ai-memory project access|grants,
[auth] new_projects_restricted, and the native hook client drops an
event the server refused with 403 instead of retrying it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqFKAuGVkuBoewmpA3cx9
Every database user is stamped as an AuthorizedViewer (never root) and
every surface attaches ScopeResolver::with_project_authz for them: MCP
tools, /api/v1 and the web pages, hook routes and captures. Read-shaped
tools that mutate need write; managed-run, workstream and session-id
entry points authorize the project the id resolves to; message queues
need write to deliver, pop and cancel. Captures into a project the
author may not write are dropped and counted (dropped_unauthorized).
Root-only /admin routes manage access modes and grants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqFKAuGVkuBoewmpA3cx9
Slice 3 on top of slice 2's choke point:
- V69 projects.created_by; resolve_project_authz derives is_creator
from it, so one principal per request is right for every project.
- ScopeResolver::resolve_existing_args[_traced] take a ProjectAccess,
so read-shaped mutations can demand write; resolve_read_args* stay
Read. resolve_many_existing authorizes every scope, and
resolve_write_args records the creator.
- authorize_scope_for and *_guarded free functions for routes that
name a project directly. A refusal names the project and both levels.
- Unscoped reads filtered in SQL before LIMIT with the choke point's
rule (not restricted, creator, or any grant; global scope shared).
- Grant storage: grant/revoke/list, audit_log rows, access-mode
setter, [auth] new_projects_restricted, grants follow a project move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqFKAuGVkuBoewmpA3cx9
Add a server-rendered page under /web that lists the pending
auto-improvement proposals of all projects. The page has a project
filter and a sort. It shows the rationale, the proposed body, and a
warning for proposals that write under _rules/ or replace a page.
The approve and reject buttons post from the browser to the existing
/admin/pending-writes/{id}/approve|reject handlers with the session
cookie and the CSRF header. Admission, audit, attribution, and the
single writer stay the same. ai-memory-web adds no write route.
The page uses the same Capability::Admin decision as /admin, and
serve passes the trusted-proxy setting to it. A non-root session gets
a 403 page. The HTML auth redirect does not send that session to the
change-password form.
Two store reads feed the page. One counts pending proposals per
project, so the total and the project filter include every project.
The other lists proposals, optionally for one project, with a cap of
500 rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwU4vj4tZLYVLR7vEKeh2W
The fixture printed 70,000 bytes one byte per shell loop iteration, which on a loaded machine outran the eval gate's 2s timeout and failed the test as eval_gate_timeout instead of the stdout cap it checks. One zero-padded printf writes the same bytes at once.
The e2e suites picked a free port, released it, and started `serve` on it; a
socket opened in between made the server exit and the test time out. The
shared `start_serve` helper now waits for the server's ready line, restarts on
a fresh port when the bind failed, and fails at once with the server's stderr
on any other exit. The Node plugin harness in `external_capture_ts` retries a
taken port the same way.
The forward-merge combined main's #895 tests (added when build_auto_handoff took
7 args) with release/2.5's #869/#883 turn_checkpoint parameter (the 8th). Update
the two new test callers to pass turn_checkpoint=false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
Forward-merge the 2.4.x fix batch (#877,#891,#893,#888,#820,#885,#886,#884,#890,#894,#895) so main stays a subset of release/2.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
# Conflicts:
# CHANGELOG.md
# crates/ai-memory-cli/src/commands/run.rs
# crates/ai-memory-store/tests/suite/multi_session.rs
Appending the extension to an empty final component fabricated `.md` from a
missing path, which slipped past auto-improve's empty-path rejection and
surfaced as `unsupported_path_prefix` instead of `invalid_path`. Skip the
append when the final component is empty so a missing/invalid path stays empty
and is rejected upstream as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
`ai-memory uninstall` left the `ai-memory run` auto-wire sentinels behind, so
the next managed launch skipped wiring and captured nothing. Removing hooks or
MCP now deletes every sentinel and lists them in the dry-run plan.
`--only mcp|instructions|skills` no longer deletes the stored hook bearer the
still-installed hooks read.
Refs #820