workspace is a required .ai-memory.toml key, so the host hook forwards
&workspace=<name> on every event under any marker's tree, including events
that never left the session's own workspace. overrides_permit_sticky
disqualified sticky mid-session routing on that presence alone, before any
project-provenance check ran, so [routing] mid_session = "sticky" was
silently inert for every marker-covered install (#976).
find_session_scope now runs before the sticky-permit gate, and a
workspace_override is resolved once against the session's own workspace via
the existing no-create lookup_existing_workspace (workspace_override_is_rescope).
The same workspace is not a rescope and falls through to the existing
project-provenance logic unchanged; a different or unresolvable workspace
still fails closed and disqualifies sticky, exactly as before.
Patch release from the 2.4.x line. Fixes: sanitize observation titles before
truncating (#982/#980), export-okf interoperability (#979/#960), the prior
#967-#973 batch, and more. Docs: TLS-interception recipe (#954), Cheaper
Inference provider doc (#981), OpenRouter and backup docs (#949/#950, recorded
as documentation of already-shipped behavior). No new capabilities → patch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
- ids.rs: #944 and #921 each hoisted SessionId::from_native; keep one
definition (git auto-merged both into a duplicate).
- render_shared.rs: align the generated-TS shell flag with the native
capture_policy determination (shell = non-file && name != web_search)
after #966 added search_web/manage_task/manage_subagents to the non-file
list — the two front doors must agree.
- mount.rs / admin_repair_session_times.rs: set the WebMountSpec
trusted_proxy_identity and AdminState contradiction_band_min/max fields
that main added, in the release/2.5-side test constructors that predated
them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
#944 adds a finalize-on-exit entry point that resolves a native session; per
invariant #16 and the security-boundary rule, record that own_native_session
refuses a discovered/concurrent session and the test that proves it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
ai-memory's reqwest client is built with rustls-tls-native-roots, so it trusts
the OS store and honors SSL_CERT_FILE/SSL_CERT_DIR. Document how to give the
container a combined CA bundle (public roots + the interception root) so
outbound LLM/embedding calls stop failing with UnknownIssuer behind a
corporate MITM appliance or inspecting antivirus. Cross-referenced from
deploy.md's provider-failures troubleshooting bullet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
The sustained_per_session_isolation stress test reported early driver
exits as 'cross-actor leaks'. On Windows, slow process_envelope latency
saturates the 1024-slot ingest semaphore under the 4-driver loop, causing
hook to return 429 Too Many Requests — a correct server backpressure
signal, not a data-isolation bug. The test now separates real leaks
(non-429 early exits) from rate-limited exits and asserts only on the
former. On Windows, rate-limited-only runs print a note instead of
failing, matching the existing throughput-floor reporting pattern.
(cherry picked from commit 4f96413c20)
The submitted text stated a specific '15–60% less' range the vendor site does
not substantiate; reword to a claim we can stand behind (below list price)
without quoting an unverified figure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
The sanitizer boundary (row 7, invariant #6) gained enforcing code in #982:
Sanitized::new now scrubs the title before applying the 80-char display cap.
Per the mandatory security-boundary rule, record the adversarial tests that
would fail if the guard were removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
`title_hint` used to be cut to 80 chars in ai-memory-hooks::payload before
the sanitizer ever ran, so a secret straddling that cutoff was often left as
a fragment too short to match a built-in or [sanitize] extra_patterns rule.
The unmatched remainder landed in observations.title, its FTS index, and
every surface that renders titles (session pages, briefings, handoffs,
search) in clear text, even though the same observation's body was
correctly scrubbed first.
title_hint construction now only takes the first line, keeping it fully
untruncated; Sanitized::new scrubs the title and then applies the 80-char
display cap (moved to ai_memory_core::sanitize::truncate_for_title), the
same scrub-then-truncate order the body already used. synth.rs's
session-title truncation (already-sanitized text) now calls the same
canonical function instead of a local duplicate.
Fixes#980.
The OKF actor grammar (§5.1) requires <producer>/<version> for an
agent or process:<id> for an automated process; the bare agent name
(e.g. claude-code) conform_frontmatter stamped into sources[].author
matches neither, so strict validators warned on every exported source
(issue #960, follow-up comment). We don't track a per-harness semantic
version, so process:<agent> is the honest choice rather than a
fabricated version.
This changes the default sources[].author value for every newly
written page from now on; already-written pages are not retroactively
rewritten, matching the project's stated philosophy of minimal OKF
migrations.
Also corrects docs/okf.md's field-mapping table: the title row claimed
it was "already written by every producer", which was never true and
is now export-scoped per the previous commit; the description row now
notes the abstract fallback and that it is still export-only beyond
the write-time summary fallback; the sources row reflects the new
process:<agent> format.
A generic OKF consumer has no idea what [[decisions/b.md]] means: §6.1
defines links as standard Markdown links (issue #960 item 3). Add
rewrite_local_wikilinks to ai-memory-wiki's markdown module, reusing
the same fenced/inline-code skip logic extract_links already relies
on (factored the inline-code span detection into a shared helper so
the two can never disagree about what counts as code). A local
wikilink, or one scoped to the exporting page's own project, becomes
a relative Markdown link computed from the page's own directory depth
in the bundle; a cross-project or cross-workspace wikilink has no
Markdown equivalent and ships untouched as literal [[...]] text.
build_okf_bundle_file threads the project name through to wire this
into the same export-only augmentation pass that backfills
title/description.
Exported pages carried neither `title` nor `description`, even though
`derive_title` already knows the title (H1 heading, else path stem)
and §4.1/§8 recommend both (issue #960 item 2). build_okf_bundle_file
now re-serializes each page through a new augment_page_for_export
step: fill a missing/empty `title` from derive_title, and a missing
description from `summary`, else `abstract`, per the issue's own
suggested fallback order. An explicit title/description already
present is left untouched, and the augmentation only ever changes the
bundle's copy — the on-disk wiki file is read once and never rewritten.
export-okf's generated index.md had a sentence ("Concept files live in
these directories:") outside the §8 list structure. §11.3 ("follows
the structure in §8") reads as rejecting prose there, and an
independent validator rejected a real bundle on this line alone
(issue #960 item 1). Fold the context into the heading instead of
dropping information, and keep the directory listing shape as-is.
- Add dedicated antigravity module in ai-memory-hooks to read tool execution outputs from .system_generated/steps/<stepIdx>/output.txt.
- Enforce whitelist for output-eligible tools (run_command, view_file, list_dir, find_by_name, grep_search, search_web, manage_task, manage_subagents).
- Strictly preserve code capture for mutation tools (write_to_file, replace_file_content) by bypassing output.txt enrichment.
- Cap output read at 2048 bytes with symlink/FIFO guards, UTF-8 boundary safety, and trailing whitespace trim.
- Support both artifactDirectoryPath and transcriptPath resolution with home directory expansion.
- Add tool mappings and CommandLine support to capture policy in both Rust and generated TypeScript (render_shared.rs).
- Enrich payload in client-side hook spooling and server-side envelope parsing.
Command Code, Kiro CLI (v2 and v3) and Antigravity CLI have no native
session-end hook, so their sessions stayed open until a manual
`ai-memory finalize-session`. When `ai-memory run` launched one of them, it
now finalizes the run's own session after the harness exits: the one named
on the command line or chosen before the spawn, or the one its child linked
under AI_MEMORY_RUN_ID in this checkout, never a discovered one.
SessionId::from_native moves the hooks router's native-id mapping into core
so hook POSTs and the lookup share one key. finalize_session::run is split
into finalize() and report printing so the run reports on stderr. The
harness's exit code is kept; finalizing is bounded by a timeout, Ctrl-C
skips it, and a failure prints the exact finalize-session command.
The lookup matches the ended session too (as --reopen does), because these
harnesses keep capturing under the same id after a resume; a re-end with
nothing new is a no-op on the server.
Closes#941
The watcher's reconcile pass only ever handled create/modify events, so a
page whose file vanished from disk stayed indexed forever until an operator
ran `ai-memory delete-page` (#929). A prior attempt at a hard-delete
reconcile-diff (merged as #958's embed half only) was reviewed as unsafe:
three false-positive classes (indexed-but-unwalked reserved paths, a
walk/list race, and partial walks on NotFound) could each make a live page
look "deleted".
This adds the safer design proposed on the issue instead: a new opt-in,
default-off `[maintenance] reconcile_tombstones_deleted_pages` flag lets the
watcher soft-tombstone (`is_latest = 0` + `superseded_at`, mirroring decay
eviction) a page only after its file has been missing on two consecutive
reconcile passes, with the DB-side candidate snapshot taken before the walk
runs and re-verified live immediately before acting, reserved/pending/session
paths filtered out of candidacy entirely, partial walks skipped for that
pass, and a circuit breaker that refuses a whole scope when more than
max(3, 50%) of its candidates look missing at once (or when a complete walk
finds nothing at all).
The tombstone is picked up by the SAME aged-tombstone hard-delete sweep decay
eviction uses, not exempted from it: what actually protects a false positive
is that `upsert_page`'s insert path now re-links a fresh write at a
tombstoned path onto that chain via `supersedes` and clears the old row's
`superseded_at`, turning it into an ordinary protected supersession-chain
member instead of an orphan the sweep would otherwise destroy. It never runs
the blocking admission gate, but does fire-and-forget any non-blocking
observer/mirror webhook. `sessions/*.md` pages are excluded from candidacy
entirely, since a same-workspace `move-session` re-home can leave one with a
correct row and no file (a separate, pre-existing bug, not fixed here). With
the flag off (the default), behavior is unchanged.
(cherry picked from commit 321e76c439)
DATA_HANDLING.md said there is no built-in retention expiry, but the daily
forget sweep tombstones cold episodic pages (below [decay] cold_threshold)
and hard-deletes them after hard_delete_after_days, and TTL (expires_at) pages
expire too — both on by default. Describe the actual [decay]/TTL behavior and
what stays (semantic/procedural/pinned, raw observations).
Closes#972
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
The forward-merge's CHANGELOG rebuild took the [2.4.0]+ tail from release/2.5,
but the frozen-section CI check requires the released half (## [2.4.1] down) to
match origin/main byte-for-byte. Since main ⊆ release/2.5 now, main's released
history is authoritative — replaced the released half with main's verbatim.
[Unreleased] (all 2.5 features + merged main fixes) is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
Review follow-up for #973. `extract` runs for every tool event on the native
hook and the server, but shell commands were tokenized inside it, so users
with no `[capture]` policy paid for words they never read. `shell_command` now
only validates and borrows the command, and the words are split in
`match_command`'s caller under an active policy. The generated TypeScript
integrations do the same. Behavior is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review follow-up for #973. `metadata_protocol_is_legal` now admits an
invalid-marker, non-file metadata-only body only when it claims no paths and
an extracted command, the only shape a client produces. Anything else is
refused instead of being echoed back with an arbitrary claim.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review follow-up for #973.
- The server's fallback for an unparseable `_ai_memory_capture` marker now
strips any event a direct invalid-policy inspection would strip, so a shell
event no longer keeps its command and output.
- An invalid marker now strips every command-running tool, even when its
command is missing or unparseable; `web_search` is still kept.
- An argv element is kept whole as a path candidate only up to 256 chars. A
longer element is a `bash -lc` script whose tokens are still checked;
charging the blob against every pattern exhausted the match budget and
silently dropped innocuous events.
The generated TypeScript integrations mirror both native changes, with Node
runtime checks. Docs and changelog note that an older server drops the new
metadata-only shell event.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The main->release/2.5 forward-merge combined main's #919 (occurred_at field on
NewObservation/NewSession + a 4th end_admitted_session arg) and #926 (build_request
existing_titles arg) with release/2.5 test constructors that predate them. Adds
occurred_at: None to the affected literals, the missing args, and fixes doctor.rs
to import main's build_launch_plan (relocated_session_dir) after adopting the
injected session_dir_for closure. Preserves invariant #8 (embed_page_version uses
model_identity) and the AI_MEMORY_CAPTURE_OWNER gate in the generated TS capture policy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
Two gaps left over from #946's shell-command `ignore_paths` matching, in
both the native hook and the generated TypeScript integrations:
- An argument vector was joined with spaces and re-tokenized, so a path
element with spaces (`["cat", "private notes/x.md"]`) was split apart
and never matched, and one element's stray quote swallowed every later
element. Each element now counts as one word as given and is also
tokenized on its own, so `bash -lc "<script>"` still reads the script.
- An invalid `.ai-memory.toml` kept shell commands with their full
output while file tools failed closed to metadata-only. A shell call
that carries a command is now metadata-only too; a non-file tool with
no command (`web_search`) keeps today's behavior.
The server admits a metadata-only `non-file` protocol only under an
invalid marker, and its re-inspection strips an older client's
invalid-marker shell keep. An older server drops the new form.
The shared shell fixture gains argv vectors (spaced path, stray quote,
`bash -lc` script, split-words control) that both matchers run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Slice 4 (#925) identity routing was merged with its design-doc section
deferred pending the always-on vs opt-in review. Decision: always-on in
2.5.0 (opt-in would leave the same-basename grant hole open). Documents the
V70 no-backfill migration, lexical normalization, resolution order, and the
four-front-door parity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm