נראה שזו המערכת שלך
Linux
- x86_64ai-memory-linux-x86_64.tar.gz15.2 MBchecksum מסוג SHA-256
- ARM64ai-memory-linux-aarch64.tar.gz14.3 MBchecksum מסוג SHA-256
מחלצים, מריצים ./ai-memory init ואז serve.
מפתחים
שוחררה ב-21 בספטמבר 2026. קובץ בינארי עצמאי אחד לכל פלטפורמה, ולצידו checksum מסוג SHA-256. ברישיון MIT.
נראה שזו המערכת שלך
מחלצים, מריצים ./ai-memory init ואז serve.
נראה שזו המערכת שלך
כולל launchd agent. אם ההורדה נעשתה בדפדפן, צריך קודם להסיר את דגל ה-quarantine.
נראה שזו המערכת שלך
ניסיוני. המסלול הנתמך הוא WSL2 עם ה-build של Linux.
sha256sum -c ai-memory-linux-x86_64.tar.gz.sha256 # macOS: shasum -a 256 -c ...
אחרי החילוץ, עוברים אל ההתקנה המהירה. היא לוקחת שלוש פקודות.
mise use -g github:akitaonrails/ai-memory
Linux ו-macOS. מאמת את ה-checksum בשבילך. גרסאות חדשות יכולות להופיע באיחור של יום בערך.
yay -S ai-memory-bin # בנוי מראש
yay -S ai-memory # מקוד מקור
מתקין יחידות systemd לשירות משתמש או לשירות מערכת.
docker pull akitaonrails/ai-memory:latest
Multi-arch: linux/amd64 ו-linux/arm64, כך ש-Apple Silicon לא צריך אמולציה.
git clone https://github.com/akitaonrails/ai-memory
cd ai-memory && cargo build --release --workspace
דורש את ה-toolchain של Rust בגרסה שמקובעת ב-rust-toolchain.toml.
מחשבי לקוח שרק שולחים hooks לשרת מרוחק לא צריכים את הקובץ הבינארי המלא. הגרסה כוללת גם את ai-memory-install-hooks וסקריפטי wrapper ל-Docker ב-Linux, macOS ו-Windows. כולם מופיעים בדף הגרסה.
12 הגרסאות האחרונות. מאז 2.0 הפרויקט עובד לפי semantic versioning. החלק הזה באנגלית כי הוא מגיע ישירות מהפרויקט.
rmcp to 2.x (2.2.0), resolving three MCP transport advisories:
GHSA-9pj6-vhgr-3mwh (unauthenticated Streamable-HTTP session-table leak /
DoS), GHSA-33f5-2c5q-wgwj (missing OAuth resource-field validation), and
GHSA-9g45-5xwm-f3wc (custom headers leaking to cross-origin redirect
targets). Behavior-preserving: the only source change is the
rmcp::model::Content → ContentBlock rename (imported under the prior
name), the feature set is unchanged, and the 23-tool MCP surface is
unaffected. (#794)companions/ai-memory-macos) that bundles the
ai-memory binary and hooks/ tree, governs the existing LaunchAgent, and
opens the built-in web UI, ai-memory status, config.toml, the data
directory, and logs. Durable memory stays in
~/Library/Application Support/ai-memory; replacing the .app does not
rewrite it. Documented as a README quick-start, an
install.md path, a cookbook
recipe, and docs/macos.md Scenario D. (#809)[dream] enabled
flag is set AND a provider AND an embedder are configured — a provider-less
store keeps the zero-LLM A3 path untouched (invariant #13). It never deletes
a source (invariant #16): the highest-retention member is rewritten and every
merged-away member is superseded with a merge-note stub pointing at it, so the
full pre-merge body stays reachable via the supersession chain + git and
restore-page recovers it; page_evidence (reconsolidation +
b2_dream:<id>) records which members fed each merge (the hallucinated-merge
guard). The rewrite routes through the existing gated apply path
(preflight_admission(Consolidate) → Wiki::apply_batch, single-writer actor,
invariant #2) with dry_run first (a dry run returns the plan and calls
neither the LLM nor the writer), and uses JSON-schema structured output only
(invariant #7). Scheduling (B3) runs the pass only after a configurable idle
window with no client activity and cancels it the moment the operator
returns (a cheap cancellation flag polled between clusters), bounded to a
capped number of clusters per run (invariant #5). Work is ordered
surprisal-first (B4): most-novel clusters — those farthest from the nearest
existing page — first. Every run returns an observable DreamReport (clusters
considered, merged, pages rewritten/superseded, skipped, cancelled) so a bad
run is never silent. New [dream] config section; no new migration (reuses
page_evidence + supersession); no new MCP tool (still 23) (#816).page_evidence substrate
(design-memory-aging.md bucket B1 / design-hindsight-borrowings.md §3): a
read-time, zero-LLM confidence derived per page version from its
evidence — distinct supporting sessions (breadth, not raw count), recency of
the newest sighting, and live contradicts count — bounded to
[0.0, 0.95]. It is exposed inertly everywhere it helps diagnosis:
memory_query(explain=true) now reports confidence and evidence_count
per hit (and belief_factor when folding is on), and memory_status reports
the project's evidence_rows count — none of which changes ranking. It can
optionally be folded into ranking authority as one more bounded factor
inside the existing [0.55, 1.50] clamp (never a new multiplier tower) via
the new [retrieval] belief_authority_weight config key, which defaults to
0.0 (OFF) so upgrades rank byte-identically. The anti-entrenchment guards
are baked in: breadth weighting by distinct sessions, recency shading, a
hard confidence cap, and — the caller-side guard for invariant #16 — a
supersession always wins regardless of evidence (a superseded version's
stale evidence never boosts it, and confidence never gates whether a write or
correction takes). Turning the authority factor on is gated on a positive
R2 delta (retrieval-triple / QA), not yet performed (#815).memory_lint
(design-memory-aging.md bucket A5): the lint pass now flags likely-conflicting
pages by cosine-similarity band. Cold knowledge pages (semantic / procedural)
whose already-stored embeddings sit in the 0.4–0.75 cosine-similarity band —
"same topic, but not a near-duplicate", the shape of a likely contradiction
(a pair ≥ 0.75 is A3 dedup territory; < 0.4 is unrelated) — get an advisory
contradiction lint finding naming both pages, with timestamp-based
resolution advice (the newer page supersedes on a timestamp basis; reconcile).
It is fully zero generative LLM: it reads only existing embeddings and
cosine (invariant #13), so with no embedder configured — or no embeddings for
the configured (provider, model, dim) triple — it is a clean no-op, not an
error, and never a provider call. It runs on the user-invoked memory_lint
(MCP and admin) and is advisory-only and non-destructive: it emits a
finding and never deletes, edits, or supersedes a page (invariant #16), and
never persists an edge (the links table's contradicts edges are
body-derived and rewritten on every page write, so a programmatic edge would
be silently wiped) — hence no new migration, and no new MCP tool (still
23). The scan is bounded: one embeddings load over the already-bounded cold
set, capped page and finding counts, deterministic ordering (invariant #2)
(#814).minPts = 2) and collapse each cluster to one survivor — the highest-retention
member, its body the extractive union of the cluster's keep-tokens, so every
member's durable facts survive — superseding the other members with a merge
note that points at the survivor. It runs only over the bounded cold-episodic
candidate set the sweep already materialises (never O(N²) over the whole
corpus), is opt-in and off by default via [decay] dedup_cold_clusters
(a false default), and fully zero generative LLM: it reads only
already-stored embeddings, so with no embedder configured — or no embeddings
for the configured (provider, model, dim) triple — it is a clean no-op, not
an error. The eps is clamped to a conservative ceiling ([decay] dedup_max_eps,
default cosine distance ≈ 0.15) so it errs toward NOT merging. Non-destructive
and reversible: no source is ever hard-deleted — every merged-away member
stays reachable via the supersession chain and git history and is recoverable
with restore-page (invariant #16) — and the merge provenance is recorded in
page_evidence. Every run reports its collapses in the SweepReport. Reuses
existing tables: no new migration, and no new MCP tool (still 23). Ships
opt-in/off; the R2 recall no-regression proof is the gate before any future
default-on (#812).apply_batch.
It is advisory and non-destructive — a skipped page is not consolidated,
never deleted (invariant #16) — and opt-in / off by default via
[auto_improve.scheduler.experience_entropy_filter] (a false default with
conservative, validated thresholds tuned so a terse-but-informative note with
a file path and an error code is KEPT), so an upgrade changes no consolidation
output until an operator opts in. Every run surfaces the skip count in the
experience report warnings. No schema change and no new MCP tool (still 23)
(#812).abstract:, an L1 first-paragraph
summary, and an L2 regex-mined keep-token set (file paths, URLs, inline-code
spans, error codes, UPPER_SNAKE constants and long identifiers), and
dropping the prose body. Tier-down beats eviction because the durable facts
survive while the expensive, low-signal prose does not. It is opt-in and
off by default via [decay] compact_cold_episodic (a false default, so an
upgrade changes nothing until an operator opts in), fully zero-LLM (regex
only), and reversible and non-destructive: the rewrite goes through the
wiki layer, so the full pre-compaction body stays reachable in git history and
the supersession chain and is recoverable with restore-page. A new V65
migration adds a nullable pages.compacted_at marker (additive ADD COLUMN,
no backfill; populated lazily by the sweep from a compacted: true frontmatter
mirror) so the sweep and the curator tell a deliberately-short compacted page
from a cold one — a compacted page is never re-compacted, re-evicted, or
re-reported as cold. Only unpinned episodic pages compact; pinned/semantic/
procedural pages are never touched. Every run reports what it compacted in the
SweepReport. Ships opt-in/off; the R2 recall no-regression proof is the gate
before any future default-on. No new MCP tool (still 23) (#808).[decay.half_life_days] config table, replacing the single global λ. Each
key (working / episodic / semantic / procedural) is a half-life in
days, converted internally to λ = ln(2) / days, so an operator can keep
episodic session history longer and working-tier scratch shorter (the
mcp-memory-service 365/180/90/30 shape). An omitted key falls back to the
scalar [decay] lambda, so the default (no table) is byte-identical to the
previous single-λ behaviour — an upgrade changes no score and mass-evicts
nothing on the first post-upgrade sweep. Pure math + config: no new column,
no migration, and no new MCP tool (still 23) (#807).memory_read_page (a direct by-path/by-query read), its
include_related link-graph walk (the walked neighbours, not just the seed),
and memory_explore (the pages it surfaces — rules, slots, recent, pinned,
settled) now bump access_count + last_accessed_at exactly as
memory_query and memory_recent already do. A page a human opens directly,
or one the graph surfaces, is used and now resists decay like a search hit.
Reuses the sanctioned reinforcement path: fire-and-forget on the single-writer
actor, throttled to ≤1 per (page, operator) per minute, and FTS-exempt. It is
strictly additive — reinforcement only raises retention scores, never blocks,
never touches the response payloads, and adds no new MCP tool (still 23)
(#798).reasoning argument on
memory_query (its answer path) and memory_explore (borrowed from
Honcho's reasoning-effort ladder; targets the 2.4 line). The knob is a schema
enum minimal (default) / low / medium / high / max; an unknown value
is rejected. Because the provider-neutral ChatRequest carries no per-request
reasoning/effort field (the provider-level reasoning_effort is fixed at
construction from config), the tier maps honestly to a per-tier max-token
budget scaled off each path's base budget (answer 2 000, explore 16 000):
minimal = 1x, low = 1.5x, medium = 2x, high = 3x, max = 4x — a
higher tier gives the model more room to reason before its output is
truncated. It only tunes the answer path: reasoning is inert unless the LLM
path actually runs (answer: true with a provider, or memory_explore with a
provider), so the zero-LLM default path is untouched. Omitting reasoning, or
passing minimal, is byte-identical to before. No new MCP tool (still 23)
(#783).memory_query: an opt-in, off-by-default answer
argument (borrowed from Honcho's dialectic endpoint; targets the 2.4 line).
When answer: true AND the server has an LLM provider configured, the query
synthesizes a concise, cited natural-language answer over the top retrieved
hits and attaches it as answer: { text, citations }, where citations are
the page paths the answer drew from (JSON-schema structured output, grounded
strictly in the retrieved snippets). When answer: true but no provider is
configured, the normal hits are returned plus a short answer_unavailable
note rather than an error. With answer omitted/false (the default), no LLM
provider is accessed and the response is byte-identical to before, so the
zero-LLM default path is untouched. Applies to the normal single-project /
scopes search; global and as_of queries ignore it. Honest caveat: the
feature is new and its answer quality is not yet eval-validated — treat the
synthesized answer as a convenience over the same hits and still open the
cited pages before acting (#782).memory_query gained an opt-in pin_first argument and
memory_briefing now carries a bounded pinned list (default off/absent;
targets the 2.4 line). Pinned pages previously earned only a small post-RRF
authority bump; they were never surfaced ahead of the search, and the
briefing never listed them by the pinned column. With pin_first: true, a
single-project memory_query prepends the project's bounded pinned latest
pages (newest first, cap 10) ahead of the fused hits, deduped by page id so a
pinned page that also matches the query appears once (marked pinned: true),
and re-truncates to the requested limit; scopes, global, and as_of
queries ignore it. A project-scoped memory_briefing snapshot now includes a
bounded pinned list of pinned latest pages (distinct from the _slots/
path-prefixed slots) so SessionStart hot-context can show standing context.
Both are backed by the new ReaderPool::list_pinned_pages; default off/empty
is byte-identical to the previous query ordering and briefing shape (#780).memory_read_page gained an opt-in related-pages graph walk (default false;
targets the 2.4 line). Passing include_related: true adds a related array
of the pages reachable from the read page through the link graph — a bounded
breadth-first walk that reuses the single-hop link primitive per node,
following both outgoing links and incoming back-links out to related_depth
hops (default 1, hard-capped at 3). Each entry carries its
path/title/kind/workspace/project plus the hop depth and edge direction
(link/backlink) it was reached by; the walk is cross-project aware,
dedup- and cycle-safe via a global visited set, and bounded by a total-node
cap. Default-off behaviour is byte-identical to the previous single-page
response (no related field) (#775).memory_query gained an opt-in include_superseded argument (default false;
targets the 2.4 line). When set, project and explicit-scope searches also
return superseded (older) page versions across the FTS/entity/vector/graph
streams, each hit labelled superseded: true so callers can tell historical
versions from the current one; the current version is never marked. Default-off
behaviour is byte-identical to the previous latest-only retrieval, and
global=true and as_of time-travel are unaffected (#773).memory_status now reports which project answered: a scope object with
workspace, project, and resolved_by (explicit, session,
shared_slot, startup_seed, default, or default_after_mismatch). An
unscoped call from a static MCP client, whose transport session id is not a
lifecycle-hook session id, returned plausible counts for a project it never
named, with nothing in the response to question them; resolved_by now makes
that visible. The server also logs a warning whenever an unscoped MCP read is
resolved by the startup seed or by the default after a session mismatch,
rather than by the caller's own hook session (#757, #774).AI_MEMORY_LLM_MODEL=gpt-5-mini for the openai-oauth
provider in docs/llm-providers.md and docs/install.md. The Codex/ChatGPT
backend only accepts a small server-defined set of model ids and rejects
others (including gpt-5-mini) with a deterministic 400; the docs now advise
leaving the provider default (gpt-5.5) for openai-oauth/codex, keep
claude-haiku-4-5 for anthropic-oauth, and qualify gpt-5-mini for
copilot as unverified. (#831)docs/llm-providers.md now covers the opencode LLM provider, which has
shipped since 1.x but was missing from the recommended-defaults table:
OPENCODE_API_KEY as the only credential, Go as the default endpoint, Zen
via AI_MEMORY_LLM_BASE_URL, the built-in default model, per-catalogue
model ids, and which model goes through the Responses endpoint (#763).docs/benchmarks/): local embeddings add
+0.149 hit@5 / +0.254 recall@10 over zero-LLM FTS, with a clean
baseline-vs-baseline determinism check and no default-ranking regression
vs 2.3.x (the 2.4 features are opt-in / off by default).auto_improve review no longer removes its session from
the queue permanently. The scheduler claims a session before reviewing it,
and the candidate query excludes any session that holds a claim — but nothing
ever released one, so a review that failed (a hung provider call, or a
proposal the reviewer could not stage) left a claim with no run row and that
session was skipped by every later tick. The state was silent: the tick
reported errors=1 once and clean runs from then on, and the only exit was a
hand-written DELETE. A claim now records the failure and releases, so the
next tick retries it, and parks after 3 attempts with the last error kept so a
deterministic failure stops costing a review every tick instead of vanishing.
The tick summary counts parked separately from errors. (#833)sessions/ pages from its own
recent-page context so those slots go to durable pages (decisions/,
gotchas/, _rules/, …) it might otherwise re-propose. Session pages are
never valid proposal targets and previously dominated the recency-ordered
list, crowding durable knowledge out of the reviewer's view. The exclusion is
scoped to the reviewer only — the SessionStart briefing and memory_briefing
still include session pages. docs/auto-improvement-loop.md now documents
that only _rules//procedures/ page bodies reach the reviewer and that the
recent-page list is recency-ordered, with configurable patchable prefixes and
embedding-nearest dedup noted as deferred future work. (#834)Create whose target page
already exists, or an Update/patch whose target is missing, previously
aborted the staging transaction, dropping every sibling proposal and the run
row over one probabilistic LLM mislabel. Those two cases now skip just the
offending proposal (reported as skipped, like a pending-target collision)
and keep the rest of the run. Two proposals in one run targeting the same
path remain a hard error, and a create-on-existing is never coerced to an
update (the page could be pinned). (#832)bin/ai-memory.ps1) now forwards the same
provider credentials and host-config env vars as the POSIX wrapper into the
helper container. A host-exported GEMINI_API_KEY / GOOGLE_API_KEY,
Copilot token, OPENCODE_API_KEY, CLAUDE_CONFIG_DIR, or
AI_MEMORY_WORKSTREAM_ID previously never reached Config::load, so
llm-test, Copilot auth, OpenCode, and a relocated Claude config all
reported "not configured" on native Windows Docker Desktop even though the
same export worked through bin/ai-memory. The POSIX wrapper also now
forwards OPENCODE_API_KEY. (#803)Config::load now treats Windows %USERPROFILE% (then dirs::home_dir) as
the operator home when AI_MEMORY_HOME and $HOME are unset. Native
Windows often has no HOME, so the #103 catch-all guard — skip a stored
repo_path equal to the user profile so it cannot prefix-match every
project beneath it — was inert there and a home-directory project could
swallow unrelated cwds. (#804)C:\Users\…\repo
vs c:\users\…\repo missed the pending auto-handoff and could mint a
fragment project. Unix paths stay case-sensitive. (#806)Ctrl+C at the native-session chooser leaving the launcher alive and
renewing its workstream lease. Cancelling now releases the acquired run and
exits without waiting for Enter or linking a native session (#795).C:\Users\…\.ssh,
.aws, .kube, .gnupg, .config\gcloud). The previous patterns required
a POSIX / separator, so a captured tool result that echoed a native
Windows path stored the profile directory and key file name verbatim. (#805){"db_password":"..."}
was stored verbatim while the identical YAML form was redacted, and JSON is
the shape most captured tool payloads arrive in. The same rule now also
accepts an auth scheme word before the value, so
Authorization: Basic <base64> (which carries user:password) is redacted
like the Bearer form already was, and covers two unprefixed names the
generic env rule missed: Azure AccountKey= and npm _authToken=. (#800)ai-memory read-page and ai-memory search, where
an escape rewrites the screen or the window title and a bidi override
reverses what the reader sees; a NUL additionally made the markdown file
binary, costing it grep and git diffs. Tabs, newlines and carriage returns
are kept. (#800)reindex skips such a pair instead of failing the whole rebuild, reports the
count, and logs each one. (#799)memory_consolidate accepts an omitted session_id. Omitting the field (or
sending null) no longer fails deserialization with missing field session_id; the tool consolidates the latest completed session in the
resolved project — the same default memory_auto_improve and
memory_read_session_observations already use. Pass an explicit UUID to
target a specific session, and dry_run=true for the cheap admission
preflight. A project with no completed session now fails as
no completed session in <scope> instead of a deserialization error.429, any
5xx, a transport timeout or connect failure) is retried twice, two seconds
apart, before the failure is reported — the same bounded policy bootstrap
already applies to its chunks. Deterministic failures (auth, schema, a
malformed-request 4xx, unparseable or truncated output) are still reported
on the first attempt, since retrying them only burns another call.CLI commands no longer fail at startup when an [[llm_fallbacks]] profile's
api_key_env variable is absent from the invoking shell. Config::load
validated every fallback credential eagerly, so read-only commands such as
ai-memory status exited with llm_fallbacks[0].api_key_env=... is set but the environment variable is missing or empty even when the running server
had the key injected by its service wrapper, which pushed operators to export
provider keys in every shell. The missing credential is now enforced where it
is needed: ai-memory serve still refuses to start without it, and building
the LLM chain still fails rather than silently dropping the fallback. Every
other profile check (provider, model, base URL) still runs at load for every
command (#762).
backfill --dry-run recorded a completed attempt and suppressed the next
automatic import. Planning now leaves the backfill sentinel untouched, even
for populated projects or an opted-out automatic invocation (#785).
ai-memory serve no longer hard-fails to take its single-instance lock on a
transient error under load. Acquiring the serve lock now retries open and
try_lock_exclusive a few times with a short (~25ms) backoff when they hit a
transient failure (EMFILE/ENFILE fd exhaustion, EINTR), mirroring
acquire_drain_lock. A genuinely contended lock (WouldBlock, another server
holds it) is never retried and still refuses startup immediately. The
serve-lock tests also assert with the concrete errno so any remaining
environmental flake is diagnosable rather than silent (#745).
GitHub Copilot completion requests now select the model-advertised API
endpoint from /models: existing Chat Completions remains preferred when
available, while Responses-only models use /responses. Responses requests
preserve strict JSON Schema structured-output constraints and report empty,
refused, or rejected output without silently downgrading the contract. A model
the /models catalogue does not enumerate (enterprise/custom deployments,
aliases, a model newer than the list) falls back to Chat Completions with a
warning instead of erroring, matching the graceful fallback already used when
/models is unavailable. As a related behavior change, a Copilot chat
completion that returns empty content now reports UnexpectedShape rather
than yielding an empty string. (#761)
The V62 page-ingestion-window migration no longer runs its backfill inside a
single migration transaction, which on a large store ran for hours and grew
the WAL to roughly the size of the database with no progress. V62 is now
DDL-only (the two columns plus their index); the window backfill moved to a
chunked, resumable, WAL-bounded boot-path step that processes pages in bounded
batches, checkpoints the WAL (TRUNCATE) between each, and logs progress. The
end state is byte-identical to the original V62, the step resumes rather than
restarts if interrupted, and it is a fast no-op on a store that already applied
the original V62. Because that reshape changes the migration's checksum, the
runner now intentionally tolerates a divergent checksum on an already-applied
migration (abort_divergent = false) so correctly-migrated stores still open;
the schema-ahead guard (abort_missing) is unchanged (#776).
Page writes now refuse git-reserved and non-portable page paths (a .git
component or an 8.3 git~1..git~4 alias, Windows-reserved names and
characters) on every write funnel, including MCP memory_write_page and
consolidation apply_batch; reads of already-stored pages stay tolerant so
a bad row never breaks a listing. The git-reserved check is byte-safe and no
longer panics on a 5-byte multibyte path component (#781).
The generated OpenCode and OpenCode 2 plugins now forward a subagent session's
parentID as the agent_id marker, so [capture] drop_subagent_captures can
recognize and drop OpenCode subagent sessions. Previously both plugins emitted
only title/projectID on session.created, so the marker never reached the
server and the opt-in was a silent no-op for OpenCode. Root sessions (no
parentID) stay unmarked (#755).
Scope-resolution failures over MCP now answer with invalid params
(-32602) instead of an opaque internal error (-32603), the same split the
web route applies with its 400/404: a malformed scope argument, or a
workspace/project name that does not resolve, is caller input, while a
missing writer handle or an underlying store failure stays internal. The
messages are unchanged.
memory_consolidate treats a blank session_id ("" or whitespace) exactly
like an omitted one — the resolved project's latest completed session — and a
malformed id now fails as invalid params, the code memory_auto_improve
already uses for the same argument.
backfill returned success even when imports failed, and --quiet hid
their diagnostics. It now reports errors on stderr, includes failure counts
in the human summary, and exits nonzero after emitting its report (#786).
DATA_HANDLING.md, docs/sso.md, and docs/airgapped-install.md,
consolidating existing data-flow, OIDC, and offline-install facts from
SECURITY.md/docs/install.md/docs/local-embeddings.md into the shape
enterprise security/legal review typically asks for before approving a dev
tool. No behavior change; a small addendum to SECURITY.md's reporting
section adds a fallback contact path for reporters who can't use GitHub's
private-advisory flow.export-okf no longer refuses to export any project that has captured an
observation. The bundle walk skipped the reserved names index.md and
log.md, but not the rotated hook ledger log-YYYY-MM.md the server itself
appends to — which carries no frontmatter, so okf::is_conformant failed it
and aborted the whole export with 422 page log-YYYY-MM.md is not OKF-conformant; run the server once to migrate before exporting. Migrating
could not help: the OKF migration deliberately skips ledgers (#669), so the
file the export demanded be conformed stays frontmatter-less forever. The
ledger is raw capture rather than a concept file, so it is now dropped from
the bundle exactly as log.md already was, sharing the migration scan's
content gate — a prose page that happens to be named log-2026-09.md still
ships and still has to declare a type. (#748)ai-memory run <harness> now auto-installs that harness's ai-memory hooks and
MCP server the first time it launches the harness, if they are not already
wired — so managed launch (the recommended way to start a harness) captures
and can query memory without a separate install-hooks / install-mcp step.
It is idempotent and one-time per harness + binary version (a per-agent
sentinel under <data_dir>/autowire-state/), preserves any unrelated user
config the installers touch, runs before the child spawns so the harness picks
up the fresh hooks, and is best-effort (a failure warns and the launch still
proceeds). Harnesses without installer support (e.g. Crush) are skipped
cleanly, and Pi wires hooks but has no MCP client to write. Opt out with
ai-memory run --no-autowire, AI_MEMORY_RUN_AUTOWIRE=false, or
run_autowire = false. Documented as the preferred launch path ("if in
doubt, run with ai-memory")./hook —
the same ingress live capture uses — so the history becomes real sessions and
observations that consolidate into pages and are searchable via memory_query,
sanitized and bounded on the server exactly like live capture and
attributed to the original harness. It only ever bootstraps an empty project
(never overwrites an established one; live capture from install-time forward
and backfill of before-install history do not overlap), is hard-capped (newest
25 sessions, 50k events), and runs detached so session start is never blocked.
New ai-memory backfill
subcommand runs it by hand (--dry-run, --force, --session, --json).
Opt out with AI_MEMORY_BACKFILL_ON_START=false / backfill_on_start = false.ai-memory doctor command: a capture-coverage check that, for the current
project, compares every known harness's local (on-disk) native session store
against what the server actually captured (GET /admin/sessions/by-agent) and
warns when a harness ran here recently but has zero captured sessions — the
silent "its hook was never installed" gap — printing the exact
install-hooks --agent <name> --apply to fix it. Read-only on the local side;
supports --json, --since-days, and explicit --workspace/--project.docs/cookbook.md cheat sheet ("I want to do X" → how:
recall, durable rules, importing a knowledge base and reading a specific
document, two agents working together), linked from the README docs table, to
make it clearer what ai-memory does and how to use it (#726).install-hooks --agent codex --capture-assistant
now captures the assistant's final message on Stop, the same double opt-in
(client flag + server capture_assistant = true) and sanitize/bound pipeline
as Claude Code. Codex's Stop payload carries last_assistant_message
(verified on codex-cli 0.154.0); the installer previously refused the flag for
Codex and now bakes it on a native hook platform. Only (Codex, Stop) is added
— Codex has no SubagentStop (#743).memory_message_send / memory_message_list / memory_message_pop
/ memory_message_cancel, bringing the surface to 23) plus ai-memory message send|list|pop|cancel CLI subcommands and /admin/messages* routes, backed by
the new agent_messages table (migration V64). Messages are addressed to a
project (any session there can pop; claim-once); the sender can retract a
pending message; an unknown recipient fails closed; each inbox is depth-capped.
A popped message is treated as untrusted cross-project input — secret-scrubbed
and size-capped on send, fenced with a security notice and sender provenance on
pop, and never auto-injected into context. See docs/agent-messaging.md.memory_briefing reports pending_message_count.codex LLM provider that follows the Codex CLI account
selected by CODEX_HOME, reloads its read-only auth.json credentials before
each operation, and delegates expired-token recovery to
codex app-server --stdio (#716).install-mcp --flavor <moonshot|bedrock|gemini> pins the tool-schema dialect
in the URL written into a client's config. The installer already picks one for
the clients whose upstream is fixed — Kimi Code is always Moonshot, Kiro always
Bedrock — but a client that fronts several models cannot be pinned by its name
alone, and there was no installer path to ?flavor=gemini for any of them. A
Command Code or OpenCode install routed to Vertex needed the marker added by
hand or every model call 400'd at tools/list; the same client on a non-Google
model does not, which is why this is an explicit choice rather than another
per-client default. vertex is accepted as an alias. uninstall now matches
every marker on every client, so an entry pinned this way is still removed
(#735).AI_MEMORY_EMBEDDING_PROVIDER=copilot adds GitHub Copilot as an embedding
provider, reusing the copilot LLM provider's OAuth login and GitHub-token
exchange (no separate API key). Defaults to text-embedding-3-small,
1536-dim; calls Copilot's /embeddings endpoint following the
OpenAI-compatible contract Copilot documents for chat (#739).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
never read from and that the container user frequently cannot write — so the
persist step failed with Permission denied and left the operator with no
hooks and no capture at all. The installer now falls back to embedding the
credential inline in the rendered hook config (the pre-#552 behavior), warns
that the token is then readable in that file and how to restore the secure
on-disk path (a writable host data dir, e.g.
AI_MEMORY_DATA_DIR=$HOME/.local/share/ai-memory for the docker wrapper, or
a native binary), and completes the install so capture keeps working.rustls 0.23.40 → 0.23.45 for RUSTSEC-2026-0285,
in which a TLS 1.3 handshake message that follows a key-changing message in
the same record can be accepted at the wrong encryption level. rustls is a
direct dependency — it installs the process-wide crypto provider the MCP
bridge needs for HTTPS — and is also the TLS implementation every outbound
HTTPS call resolves to through reqwest, so the advisory failed cargo audit
and cargo deny check on every open pull request that inherited main's
lockfile, including ones that change no Rust at all. Cargo.toml already
allows compatible 0.23 patch releases, so this is a lockfile-only change
needing no manifest or public-surface edit; the same resolution moves
rustls-webpki 0.103.13 → 0.103.15 and nothing else (#731).ai-memory hook session-id state file
(<data_dir>/hook-state/<agent>-session-id), introduced for Devin in #178,
now also covers ZCode. ZCode's hook payloads do not reliably carry a session
id, it fires Stop at the end of every turn, and it has no SessionEnd
event — without a persisted id, each turn with an id-less payload opened a
fresh server-side session that nothing ever closed (observed in production:
4 sessions with NULL ended_at and 398 observations stuck in episodic).
ZCode events without a native id now share one stable stored id per agent
session; events carrying a native id are passed through untouched. The stored
id is cleared by finalize-session --agent zcode (which now also removes the
state file) or overwritten by the next session-start; Stop never clears
it. Other agents are unaffected: payloads that carry a session id short-circuit
the state file exactly as before.memory_feedback's signal (FeedbackKind) JSON schema now declares a
top-level type: "string", so strict function-calling gateways (Moonshot/Kimi
and other schema validators) accept the tools/list surface instead of
rejecting the enum for a missing type (#735, #741).install-hooks --agent cursor and install-hooks --agent claude-code are
applied. Cursor also runs the commands in Claude Code's settings, and since
2.1.0 that copy is re-attributed to cursor by its cursor_version, so it
landed in the same session as the native event. The --agent claude-code
hook now drops a Cursor-marked payload when ~/.cursor/hooks.json already
registers an ai-memory --agent cursor hook; without Cursor's own hooks the
Claude Code path keeps capturing Cursor sessions as before (#721).install-hooks --agent cursor --apply now warns about existing
~/.cursor/hooks.json entries that mention ai-memory but are not ai-memory
hook entries, such as a pre-2.1.0 shim that injected cwd. Those entries
are kept beside the native ones, so every event would otherwise be captured
twice without any sign (#721).ai_memory_json_field appended to a string that grows to the whole
value, so reading a multi-megabyte entry — one large tool result is enough —
took minutes of CPU, and a drain pass reads every entry three times (url,
body, token) while one detached pass starts behind every delivery that
succeeds. Once an outage had filled the spool, the passes accumulated faster
than they retired and saturated the machine. The value is now bounded by a
single regex pass over the JSON string grammar, unescaped with gsub over
whole segments and written straight to stdout, for identical output and exit
codes. A 2.2 MB entry of grep output — the shape that caused the incident,
where every literal backslash is an escaped pair — goes from 732 s to 5.5 s
under the awk macOS ships and from 9-10 s to about 1 s under mawk and gawk
(#727).ingest_key before its initial
delivery and preserves that key when spooling the event, preventing a replay
from creating a duplicate observation when the server committed the first
request but its response was lost (#729).install-hooks reapply is idempotent again on Windows for agents whose
native command uses an underscore executable name (ai_memory): the hook
ownership predicate recognized only the hyphenated ai-memory, so a reapply
reported Updated and failed to dedup its own prior Antigravity/Cursor/Kimi
Code entries. It now matches both forms while still requiring the full
hook --event … --agent … --server-url … argv signature (#740).ai_memory_json_string now escapes every JSON
control character (U+0000–U+001F) as \u00XX, not just backslash, quote, tab
and CR. A replayed tool result carrying an ANSI colour escape (0x1b) reached
stdout bare, so the managed-workstream SessionStart packet — and any handoff
whose summary held a control byte — was rejected as invalid JSON and the
resuming session started with no context. The escaping is linear and reuses
the same BusyBox replacement-doubling probe as the four existing escapes
(#732).<data_dir>/hook-spool/ in the same on-disk contract ai-memory hook-drain
reads (same filename shape, same SpoolEntry JSON, same 0600/0700 modes,
tmp+rename), with an ingest_key minted once at spool time so a shell drain
and a concurrent hook-drain cannot double-ingest; the backlog is flushed
behind the next delivery that succeeds, detached from the hook so the agent
never waits. A 4xx stays a permanent rejection and is not retried. This is
the durability the generated TypeScript integrations got in #580, for the
path the docker deploy installs. The PowerShell bundle is unchanged (#719).POST /hook now hard-timeouts at 200 ms
rather than 500 ms, which is the budget invariant 5 documents for a script
hook. A real loopback round trip runs about 0.3 ms, so a local install is
unaffected; against a remote server the tighter ceiling is safe only because
a missed window now spools instead of dropping (above). The handoff GET keeps
its 1 s: it is fed synchronously to the agent's context, and truncating an
almost-ready handoff costs more than it saves (#719).ai-memory restore no longer rejects the GNU-sparse SQLite entry that
ai-memory backup itself produces. tar::Builder's default sparse
detection archives db/memory.sqlite as a GNU-sparse entry (header type
S) whenever the SQLite snapshot has real holes on disk, which
validate_restore_entry's is_file()/is_dir() check rejected as an
"unsupported entry type" — a verified, valid backup could not be restored,
with no recovery path but hand-editing the archive. tar already expands
GNU-sparse blocks to their full logical content while iterating entries, so
restore now accepts the type and unpacks it exactly like a regular file
(#718).explain now names the typed edge a graph-stream neighbour
was reached by: memory_query(explain=true) reports graph_via.edge =
causes / fixes / contradicts (omitted for a plain references link), so
it is visible why a page surfaced through the link graph. Retrieval ranking
is unchanged — this is explanation only; typed-edge weighting and contradicts
capping are deferred behind the eval harness (docs/design-hindsight-borrowings.md P3).pages.valid_from / valid_to, V62) and
a second as_of stream: memory_query(as_of=T) now fuses the entity
timeline with version-filtered full-text search over the page versions
alive at T, using the default path's RRF (k=60) plus the same bounded
authority adjustment — current-index relevance over knowledge valid at T. This
answers entity-less pages and paraphrased audit questions ("which
database were we on during the outage?") that the entity-only lookup
missed. explain=true reports both streams (streams_active: ["entity", "fts"]) with per-stream ranks and RRF contributions.
Vector, graph, and the raw-observation fallback stay out of audit mode;
the default (no as_of) path is unchanged. Every retire path
(supersede, decay, reorg graveyard, move-regenerate) closes both grains
in the same transaction, and the V62 backfill closes successor-less
retirements at the decay marker, existing entity-link close, or
updated_at fallback, preserving recorded reorg/move history.
Phase B world-time stays deferred. (#656)[retrieval] section with two opt-in ranking signals, both off by default
so unconfigured stores rank exactly as before. query_intent routes
lexically session-recall queries ("上次 / 之前那次…的会话 / last time /
yesterday …") past the default session-page authority penalty
(×0.77 combined kind/tier), with session_recall_bonus (default 0.25)
sizing the lift; abstract_vectors adds a fifth RRF stream over the new
page_abstract_embeddings table (V61) — the L0 layer: a page's
frontmatter abstract: line is embedded on its own, at write time when an
embedder is attached and by the same backfill as the body otherwise
(a one-line summary embeds far more sharply than a
multi-thousand-character body). Measured on a 138-query golden set over a
production two-year wiki (FTS5 + entity + vector + graph, mis-tei
Qwen3-Embedding-8B): hit@1 0.609 → 0.746 (+22%), NDCG@10 0.782 → 0.879
(+12%) with both enabled; every unrouted category keeps its exact
baseline ordering. Cross-checked on an independently built 99-query
golden set against the same store: hit@1 0.556 → 0.626, NDCG@10
0.712 → 0.779 (+9.4%). Both signals are visible per hit in
memory_query(explain=true) (intent, intent_boost,
abstract_rank, rrf.abstract). (#672)memory_handoff_list lets no-stdout and MCP-only clients inspect open
project handoffs without claiming or expiring them, then
memory_handoff_accept can claim that exact handoff_id once. Grok,
Zero, and other clients that discard SessionStart stdout no longer have
to recover a baton by blindly accepting the latest row. Listing is
owner-filtered like accept (own plus shared; root-only any_owner);
it is not a second claim path, and Grok SessionStart still does not
fetch /handoff. (#664)install-instructions --compact writes a slimmer managed routing block for
projects that already have the detailed ai-memory Agent Skills installed. The
compact block keeps the same start/end markers (so refresh/uninstall still
find it), the untrusted-history security scaffold, and the cross-harness
memory-of-record guidance; full_block and the default remain unchanged.
(#675)memory_install_self_routing accepts compact: Option<bool> so agent-driven
refreshes of a compact-installed file preserve the compact routing block
instead of rewriting it back to full. (#685)openai-compat provider now sends OpenRouter's app-attribution
headers (HTTP-Referer, X-Title) by default when its base URL points
at openrouter.ai, so ai-memory's usage shows up on OpenRouter's app
leaderboard. An explicit AI_MEMORY_LLM_HEADERS entry for either header
still wins, and a non-OpenRouter compat endpoint (Ollama, vLLM, LM
Studio) never receives them. (#686)ai-memory run now accepts any claude*-prefixed harness name
(claude-corp, claude-personal, ...), all resolving to the same
ManagedHarness::Claude/AgentKind::ClaudeCode — no new session store,
migration, or agent kind. This is for callers juggling more than one
Claude account (e.g. Corporate and Personal): name each account's PATH
wrapper script claude-<account> and pair it with --executable claude-<account> (bare names already resolve through PATH) so
ai-memory run claude-corp --executable claude-corp and ai-memory run claude-personal --executable claude-personal each launch the right
binary while reading clearly in shell history (#687).install-mcp --client muse registers ai-memory with Meta's Muse Code,
merging a native streamable-HTTP entry with bearer headers into the
snake_case mcp_servers map of ~/.config/muse/settings.json and
preserving sibling servers. The writer adds the mandatory
"schema_version": 1 when it is absent — without that key every muse
command fails at startup with malformed settings file — and never
rewrites an existing value, so a future schema is not downgraded. The entry
sets "mode": "optional" because Muse defaults it to required, which
aborts the whole run when the memory server is unreachable. MCP-only:
Muse documents a lifecycle hook surface, but the output contract of its
SessionStart event is not specified, so capture and managed workstreams
are not claimed. Skills need no extra step — Muse reads ~/.agents/skills,
which install-skills already writes (#659).docs/design-hindsight-borrowings.md
§3): a new append-only page_evidence table (V63,
(page_id, source_kind, source_id, created_at), source_kind one of
session/observation/feedback/reconsolidation) records what
produced or reaffirmed each page version, written in the same
transaction as the page upsert. Rule-based and zero-LLM — the
consolidator cites the session(s) it drew on for both the single-page
and batch write paths, so the substrate populates on the default path
with no provider configured. hybrid_search_explained (and therefore
memory_query(explain=true)) now reports evidence_count per hit,
batch-fetched once after fusion; the default (non-explained) path and
ranking are unchanged — evidence is inert data and an explain field
this release, not a ranking input. The confidence-into-PageAuthority
step described in the design doc is deferred behind the planned R2
retrieval eval.docs/design-hindsight-borrowings.md
§5): memory_briefing accepts an opt-in settled_first: bool (default
false, unchanged briefing shape). When true, the snapshot's new
settled array leads with the project's highest-standing rule/decision
pages — up to 8, ordered by page_evidence count (P2) then recency — so an
agent can boot from settled answers instead of re-deriving them. Pure SQL,
no LLM call; every other briefing field, and the default settled_first: false path, are byte-for-byte unchanged.[REDACTED:<kind>]
(e.g. [REDACTED:github_token], [REDACTED:jwt], [REDACTED:env_secret],
[REDACTED:custom] for operator patterns) — instead of a bare [REDACTED],
so a later reader knows what kind of secret was present without it leaking.
Redaction stays complete and idempotent; the trust boundary is unchanged.
Note: this changes the durable sanitized string, so pages written before the
upgrade keep [REDACTED] while new writes carry the label (see
docs/design-hindsight-borrowings.md P1).updated_at still held creation
time. Clarified current-index ranking and snapshot-based rollback. (#682)context/ tree) from a
harness-local memory store: decisions go into the repo's record under its
convention, ai-memory keeps recall, handoffs and session history and does not
duplicate the record as a page. docs/usage.md ("Repo-native decision
records") and docs/marker-file.md say to list such a directory in
[capture] ignore_paths, and why (#700).CLAUDE.md and
does not read AGENTS.md: a project whose canonical instruction file is
AGENTS.md needs a bare @AGENTS.md import line in CLAUDE.md, or the rules
written there are absent from context at session start and reach the agent only
if it opens the file. docs/install.md and docs/usage.md carry the same note
beside the --target AGENTS.md guidance, and this repository's own CLAUDE.md
now uses the import instead of a prose pointer (#680).purge-session whose page-file cleanup failed was undone by the next
watcher pass. The cleanup failure is reported in files_failed and leaves
the database rows deleted while sessions/<id>.md is still on disk; nothing
on the reindex path consulted the purged_sessions tombstone, so the
reconcile tick 30 seconds later indexed the leftover file and the purged
session's body was searchable again. The wiki reindex now skips a page whose
session is tombstoned, the way it already skips a tombstoned scope, loading
the tombstones once per directory per pass rather than once per page. The
pass reports them as skipped_purged_sessions (#701).bin/ai-memory) now forwards GEMINI_API_KEY and
GOOGLE_API_KEY into the container. Every other provider credential was on
the -e forwarding allowlist, but these two were missing, so
AI_MEMORY_LLM_PROVIDER=gemini (or the gemini embedder) reached the server
while its key did not — the process then failed with provider not configured: GEMINI_API_KEY or GOOGLE_API_KEY even though the operator had
exported it (#698).serve no longer re-archives the whole data directory on every boot once the
pre-migration backup receipt's archive has been deleted and auto-improve
_pending/ sidecars exist. The OKF conformance scan that feeds the backup
gate flagged those staging sidecars (which carry no frontmatter and are never
migrated — SQLite owns their approval state) as nonconformant, so it kept
falling through to a full archive. The scan now skips the project-root
_pending/ subtree — a nested notes/_pending/ page still migrates — matching
the watcher indexer and the existing ledger skip (#695, same class as #669).LLM_BASE_URL in the environment no longer redirects providers that
talk to a fixed vendor endpoint. The variable is a cross-tool convention an
operator exports once for a local Ollama, and ai-memory fed it to every
provider: a gemini server then POSTed to
http://localhost:11434/v1beta/models/<model>:generateContent and got Ollama's
plain-text 404 page not found, surfacing as
502 Bad Gateway: {"error":"provider error 404: 404 page not found"} on
bootstrap and consolidation. It now reaches only the dialects whose endpoint
the operator supplies anyway — openai-compat, which has none without it, and
opencode, whose Zen catalogue is an override — and is ignored elsewhere with
a startup warn! naming the provider and the URL. An explicit llm_base_url
(or AI_MEMORY_LLM_BASE_URL) still configures any provider, so proxying a
vendor endpoint on purpose is unchanged. ai-memory llm-test resolves the base
URL the same way, so it reproduces what serve will do instead of inheriting
the same ambient override (#691).POST /admin/bootstrap and the auto-improve routes serialized the error into JSON
and told the log nothing, so an upstream provider failure could break every
bootstrap while the log showed only the run starting — and a scheduled
auto-improve tick, which has no client to print the body, failed with no
operator-visible trace at all. A 5xx now emits a warn! naming the status,
the operation, and the error. A 4xx stays quiet: the caller was told and the
caller was at fault, so logging those would let any client fill the log at
will (#692).purge-session took the wiki mutation guard only for the file cleanup, after
the database deletion had already committed. A watcher reindex could reinsert
the still-present page in that gap and keep only the row, and a concurrent page
write could lose its file to the cleanup. The purge now holds the guard across
both steps, so reindexes, page writes and wiki moves finish before it starts
and wait until it is done (#696, follow-up to #653).memory_status, memory_briefing, and every other
unscoped read then resolved through the baked default scope and answered
zero counts through the success path, with nothing in the log to
distinguish "scope unresolved" from "project genuinely empty". serve now
seeds a read-side fallback at startup from the most recently active project
recorded in the database, bounded by the same TTL as a per-key entry, so a
keyed miss right after a restart degrades to real data. The seed serves
reads only: keyed per-actor entries are never reconstructed, an unscoped
write resolves exactly where it did before, and the first hook event
supersedes the seed — so pre-publish and eviction reads keep degrading
exactly as they do today (#678).PKGBUILD now builds and tests on constrained AUR
builders. Release LTO was disabled (options=('!debug' '!lto')) so the
final link no longer gets OOM-killed on low-memory build hosts, and the
check() step pins CARGO_HOME to the registry build() already
populated before it repoints HOME at an empty test home, so the
--frozen test run can resolve the packages it fetched instead of
failing offline (#677).server returned 404 Not Found: <body>,
so a failure gave no clue which endpoint answered. The message now leads
with the request method and path: GET /admin/open-sessions: server returned 404 Not Found: <body>. The error keeps the path only, so a
token in the URL userinfo or query string never reaches a log line. (#679)session_shutdown for pi/omp, session_end
for OpenClaw) with a synchronous, fire-and-forget handler. The host tears
down the runtime as soon as a synchronous handler returns, killing the
in-flight session-end POST before it completes, so the server never
learned the session had ended: sessions.ended_at stayed NULL, and no
summary or handoff was produced. The handlers are now async and await a
bounded (2s) drain of the pending request(s) before returning, mirroring
the joinable dispose-drain already used by the OpenCode integration (#676).bin/ai-memory container wrapper now matches multi-architecture image
manifests against the host's platform architecture, eliminating a
false-positive "a newer image is available on Docker Hub" warning on x86_64
and Podman. Additionally, emit_docker_run_script now preserves volume mount
modes (such as :Z on SELinux/Podman environments) and filters transient
runtime environment variables (HOSTNAME, container=podman). (#673)ai-memory serve now stops on Ctrl-C and on SIGTERM, on both transports.
The stdio transport listened for no signal at all, and the HTTP transport
listened for SIGINT alone — so SIGTERM, what docker stop, docker compose down and systemctl stop send, reached no handler on either. What that
cost depended on whether the server was PID 1. In the container it is (the
image's ENTRYPOINT is exec form, with no init shim), and for PID 1 the
kernel discards a signal whose handler is not installed: the signal was not
merely unhandled, it was invisible, so docker stop sat out its whole grace
period and ended in SIGKILL, docker kill was the only way out, and Ctrl-C
on stdio did nothing at all. Everywhere else — under the native systemd
unit, or a plain ai-memory serve in a terminal — the process is not PID 1,
so the same signal fell through to the kernel's default disposition and
killed it instantly instead, with no drain at all: the durable SessionEnd
consolidation worker was cut off mid-flight rather than drained. Both
transports now listen for SIGINT and SIGTERM, log which one arrived, and
bound the drain at five seconds so a stateful or SSE MCP client holding a
connection open cannot stall the exit — a stop that used to be instant and
unclean now takes up to those five seconds and drains. The listeners are
installed before the transport starts, so a signal arriving during a slow
boot — migrations, the pre-migration archive — is handled rather than lost,
and no container init shim (tini, docker run --init) is needed for the
server to stop as PID 1 (#699)..ai-memory.toml marker whose only content is a [capture]
section (e.g. one that just sets ignore_paths) no longer resets scope
resolution to default / basename. The native binary, the POSIX shell
hooks, and the generated TypeScript integrations all used to resolve
workspace/project and the other forwarded root-level settings
(project_strategy, drop_subagent_captures, [recall] default_global,
[briefing] keys) from the single nearest marker — the same one that
decided [capture]/ignore_paths — so a capture-only marker in a
subdirectory silently shadowed an ancestor marker's declared
workspace/project and captures landed in the wrong scope. Scope and the
other forwarded settings now resolve from the nearest marker that
declares more than [capture], while [capture]/ignore_paths keeps
reading the nearest marker unchanged — a marker that declares nothing
else is scope/settings-transparent. Applies to the native ai-memory
binary, the POSIX shell hooks (hooks/_lib.sh), and the generated
TypeScript integrations for every adapter that resolves scope client-side
(OpenCode, OpenCode2, pi, OMP, OpenClaw); outputs remain byte-identical
otherwise. (#668)causes / fixes / contradicts schema as
single-page consolidation and carry non-empty relations into wiki
frontmatter and the link index, instead of silently discarding them. (#667)log.md / log-YYYY-MM.md carrying YAML frontmatter as an ordinary page,
but the OKF v0.2 migration stamps frontmatter on every .md under wiki/,
ledgers included. A migrated store therefore indexed its ledgers, and each
hook append_event superseded them: one pages row per appended line,
holding the whole multi-megabyte ledger body. One affected store reached
6,539 page versions and 14 GB from 101 live pages within four days of
migrating. The check now reads past the frontmatter fence and classifies on
the first body line, so a real page that happens to be named log.md is
still indexed. (#660)install-hooks --apply --capture-mode allowlist no longer captures
repositories with no .ai-memory.toml marker on the five generated
TypeScript integrations (pi, omp, opencode, opencode2,
openclaw). The native ai-memory hook path already gated every
lifecycle event on marker presence before spooling or sending anything
(repository_admits_capture in ai-memory-hooks::capture_policy), but
the generated adapters POST to <server>/hook directly and had only
ported the ignore_paths denylist logic into their shared
capturePolicy — never the allowlist admit gate — so an unmarked
repository was still fully captured while the CLI printed that the
protection was active. The shared template now bakes the selected
--capture-mode into a CAPTURE_MODE constant and gates on
marker presence (not on marker configuration state, so a marker with
an empty [capture] section still opts a repository in) before any
per-event disposition runs. The install-time "enforced"/"NOT in force"
messaging is corrected to match: a script-fallback install is now the
only path flagged as unenforced. (#661)markdown::parse stripped the mark before looking
for the frontmatter fence, but the no-frontmatter path returned the untouched
input as the body, so the mark stayed in front of the first line. Two things
followed from that: derive_title no longer read the leading # as a
heading, so the page was indexed under its filename instead of its title, and
the OKF v0.2 file pass (which conforms frontmatter and leaves the body alone)
re-emitted the mark after the closing --- fence, where it is a stray
zero-width no-break space rather than a byte-order mark. Both paths now drop
the leading BOM, which is what the frontmatter path already did. (#663)serve no longer re-archives the whole data dir on every start once a
monthly log ledger exists. The OKF conformance migration's
nonconformant_files scan flagged every frontmatter-less log-YYYY-MM.md
/ log.md event ledger as a pre-OKF page (okf::is_conformant requires a
type key and a ledger has none), so the pre-migration backup gate saw a
non-empty pending list and took a full tar.gz snapshot on every boot —
the flip side of #660, which taught only the watcher's indexer to skip
ledgers by content. The migration scan now shares that same content-gated
check (a reserved-looking filename is excluded only when its first body
line is a hook log entry), moved into a shared ledger module so both
call sites stay in sync; a page literally named log-2026-09.md whose
body is prose is still migrated. (#669)sessions/<id>.md page for a
session that logged no real work. is_lifecycle_only_session treated a
session as skippable only when every observation was SessionStart or
SessionEnd, so a single Stop observation — which OpenCode fires for
purely internal work like branch-naming, alongside session.created +
session.idle with no user prompt and no tool use — was enough to make
the session look substantive and get a wiki page synthesized for it,
flooding the wiki with no-op session pages. "Substantive" is now defined
positively instead of negatively: a session counts as real work only if
it contains a UserPrompt, PreToolUse, or PostToolUse observation
(is_ephemeral_session in ai-memory-hooks). The atomic store-side
check (end_lifecycle_only_session_in_tx in ai-memory-store) moved to
the same positive kind IN ('user-prompt', 'pre-tool-use', 'post-tool-use') test so the two stay in agreement, and the
PreCompact/PostCompaction checkpoint path gates on the same test rather
than only observations.is_empty(). Provider-agnostic: this fixes the
class for any harness that fires lifecycle-only Stop/Notification
events, not just OpenCode. (#662)[[llm_fallbacks]] in
config.toml configures one or more additional providers, each with its
own provider, model, optional base_url, and optional api_key_env
(the environment-variable name holding that profile's key — never the
key itself in config). The primary provider (llm_provider) always runs
first; fallbacks run only after a transient failure
(LlmError::is_transient(): 429, 5xx, timeout, connection error) advances
past the current candidate, in declaration order, with the original
request, JSON schema, and logical operation id preserved on every
attempt. A deterministic failure (400/401/403/404/422, an unsupported
schema, or a malformed response) still stops immediately — the same
policy a single provider already had. Each candidate carries a 30s
in-memory circuit: a transient failure opens it, a success closes it,
and a restart clears all circuit state. Config::load validates every
profile and resolves its credential once, at startup — a missing/empty
provider or model, an unknown provider, or a RequiredApiKey provider
with no resolved api_key_env fails startup rather than leaving a
latent fallback that only fails once the primary is already down; a
provider with a native credential source (OpenAI OAuth, Copilot) needs
no api_key_env. GET /admin/status (and ai-memory status) now
reports each candidate's provider/model label, last-selected/last
success/error state, and circuit-open-until timestamp alongside the
existing top-level LLM provider/model fields, which are unchanged for
every existing single-provider deployment.bootstrap --resume recovers an interrupted run from durable per-chunk
progress instead of re-paying for every LLM call (#621). Each chunk's pages
are recorded (keyed by a fingerprint of the pruned sources + chunk budget, so
a re-run after new commits re-chunks and starts fresh) and --resume seeds
them and skips those chunks; the wiki write stays atomic (nothing lands until
the whole run completes) and progress is cleared on success. It adopts only
the contiguous prefix of completed chunks and re-runs from the first gap
or unreadable row (#635), so a resumed run can never diverge from a clean one
by seeding a later chunk with context an earlier, still-missing chunk never
produced. Complements the transient-retry from 2.0.3 (#617): retry reduces
how often you need resume; resume covers the crash / restart case.opencode2) support.
install-mcp --client opencode2 merges the V2 mcp.servers remote entry
(no enabled field, oauth: false for header credentials);
install-hooks --agent opencode2 writes an ai-memory-opencode2.ts
plugin in the beta { id, setup } shape (no runtime dependency:
Plugin.define is identity), with handoff injection through the
context hook and the same session.* lifecycle coverage as v1;
ai-memory run opencode2 launches through the opencode2 binary and
imports the beta session_v2/session_message transcript. The beta
shares v1's config dir, session store, and agent kind, so no store
migration ships. Verified live against beta-18999 (hook capture,
handoff loop, managed launch/resume/import, opencode↔opencode2
session resume). Two beta caveats are documented: back up
~/.local/share/opencode first (the beta has migrated the shared
database in place before), and move its background service off port
49374 when ai-memory serves there. Managed ledger-delta
acknowledgement does not fire through the shared background service
(redelivery, never loss); see the managed-workstreams notes (#622).install-hooks --agent codex now wires Codex's SessionEnd hook, so a
finished Codex session gets the same automatic end-of-session summary
and cross-agent handoff Claude Code already gets. Codex shipped a
first-class SessionEnd event in Codex CLI 0.145.0 (openai/codex#33895);
the hooks/codex/session-end.{sh,ps1} scripts already existed and only
needed adding to the Codex hook profile. On older Codex the event key is
inert; ai-memory finalize-session --agent codex remains the fallback.
merge_codex_payload no longer strips a SessionEnd key on every apply
— a third-party SessionEnd hook is preserved alongside ours. ([#604])AI_MEMORY_LLM_HEADERS (llm_headers in config.toml) attaches extra
HTTP headers to every LLM chat request, for gateways that require a
caller-identifying header. Entries are Name=Value or Name: Value,
comma separated in the env var. Parsed and validated once at the
configuration boundary, so a malformed entry fails at startup rather than
on the first consolidation pass, and providers consume typed header
material instead of operator strings. Headers ai-memory sets itself
(authorization, content-type, x-api-key, x-goog-api-key,
anthropic-version, anthropic-beta, openai-beta, host,
content-length) are refused: reqwest appends rather than replaces, so a
duplicate would break the request instead of overriding it. Values are
marked sensitive on the wire and never logged — the Debug output carries
header names only. ([#606])User-Agent: ai-memory/<version>.
reqwest sends no user agent unless one is configured, so provider
requests previously arrived anonymous, and gateways that require callers to
identify themselves reported ai-memory as an unknown client. An
AI_MEMORY_LLM_HEADERS entry for user-agent overrides it. The Copilot
provider is unchanged: it keeps GitHubCopilotChat/<version>, the
editor-plugin agent GitHub's Copilot API expects. ([#606])x-opencode-session and the opencode user agent, both shipped in 2.0.2,
are now operator-overridable like any other header: an
AI_MEMORY_LLM_HEADERS entry for either name takes precedence over the
provider's default. The default remains 2.0.2's — one LlmOperationId per
logical operation, stable across retries and the strict/tolerant fallback —
so nothing changes unless an operator asks for it. Override the session
header to tell several ai-memory instances apart in OpenCode's metrics.
([#606])[briefing] max_chars now bounds the whole
rendered brief, and its floor rose from 500 to 1500 chars. The scaffold the
brief may never drop — the security notice plus both untrusted-history
markers and the closing instruction — costs 832 chars by itself, so any
budget under that could only ever have been honoured by dropping the
boundary that marks the brief as untrusted. A project whose marker sets a
smaller max_chars now gets 1500. (#657)TAILWIND_BUILD=1 cargo build -p ai-memory-web
regenerates it, so no command needs TAILWIND_SKIP=1 any more. CI
regenerates the bundle on Linux and fails if the committed
static/tailwind.css is stale, a check that did not exist before.cargo t (everyday, skips slow/stress modules) and
cargo tf (everything) aliases over cargo-nextest, integration tests that
compile into each crate's own test harness from tests/suite/ (78 test
binaries down to 11 in the everyday loop; only the CLI keeps a separate one,
and the evals harness builds only under --workspace), a dev profile that
keeps only line tables, and an opt-in pre-push hook that runs the full tier.
bin/release and the documented gate also run git diff --check. Measured:
workspace edit-to-result ~380s to ~150s on macOS; the warm everyday test run
28s to 19s on a 32-thread Windows box.bin/ai-memory container wrapper now runs on Podman without Docker
(#636). It auto-selects the engine (AI_MEMORY_DOCKER override → docker →
podman), defaults to the fully-qualified docker.io/akitaonrails/ai-memory
image so Podman's non-interactive short-name resolution does not fail, and
preserves the selected engine in the standalone-container recovery script
ai-memory upgrade emits instead of hard-coding docker.gpt-5.6-luna requests to OpenCode Go's
Responses endpoint. Luna is not served through Chat Completions, where plain
and structured ai-memory calls returned HTTP 500 (#618).AI_MEMORY_LLM_BASE_URL now works with AI_MEMORY_LLM_PROVIDER=opencode.
The provider hardcoded OpenCode's Go endpoint and the factory dropped
the configured base URL without a word, so Zen's general catalogue at
https://opencode.ai/zen/v1 was unreachable through it — Zen and Go are
separate products, not two spellings of one. Go remains the default, so
existing setups are unaffected; an override keeps the x-opencode-session
default and the user agent, since both endpoints correlate requests the
same way. Model ids are per catalogue, so set AI_MEMORY_LLM_MODEL
explicitly when overriding. OPENCODE_ZEN_BASE_URL is deprecated in
favour of OPENCODE_GO_BASE_URL: the constant named Zen but has always
held Go's URL. It keeps its value, so code compiled against it is
unaffected. ([#606])purge-session leaving the live sessions/<id>.md wiki file behind
after deleting the session's SQLite rows. The admin endpoint now removes the
scoped page file after the DB purge commits, reports actual cleanup through
files_deleted / files_failed, and marks purge_session observer webhooks
with partial_failure: true when file cleanup fails, so a later wiki reindex
cannot resurrect the purged session. (#653)[briefing] max_chars is now the size of the brief the agent actually
receives. Page bodies were budgeted, but every section rendered after
them escaped the accounting: the truncation notice, the crowded-out core
page list, the recently-updated pointers, the closing untrusted-history
fence and the agent footer. The escape pass that neutralises an untrusted
<!-- ai-memory:untrusted-history --> marker inside a page body then
lengthened the text by a further 6 chars per marker, after the count. A
4000-char budget rendered 6577 chars, and the 500-char floor rendered
3054 — six times what the operator asked for, on every opted-in session
start. Bodies are now served first, the pointer sections are sized
against what the bodies left (the omitted list degrading to a bare count
when not even the paths fit), and the mandatory scaffold is reserved
before a single body char is spent. The regression test asserting this
was named render_session_brief_enforces_budget but only checked the
brief's contents, never its length. (#657)AI_MEMORY_AUTH_TOKEN received 401 Unauthorized even though the server
reported auth=false; invalid Bearers remain rejected whenever static or
human authentication is enabled. (#639)default/scratch bucket.
Cursor sends the workspace directory only as workspace_roots — its
sessionStart / sessionEnd payloads carry no cwd key at all, and its
tool events send cwd: "" — so cwd resolution produced nothing and the
server fell back to its default project for every Cursor event. Both the
native ai-memory hook path and the POSIX/PowerShell hook scripts now read
workspace_roots (alongside Antigravity's workspacePaths) and treat an
empty cwd as absent rather than as an answer.claude-code. The Cursor CLI
also runs the hook commands declared in Claude Code's
~/.claude/settings.json, which install-hooks --agent claude-code
hardcoded to --agent claude-code, so a Cursor-driven session was stored
with agent_kind = claude-code. Hook payloads carrying Cursor's
cursor_version marker are now attributed to cursor regardless of the
?agent= the hook command declared._meta.md) were written at startup and on
the rename/move admin paths, so a session in a checkout the server had not
seen before produced a scope directory with pages but no manifest. Stop the
server in that window and reindex could not rebuild that tree — the one
situation where an operator most needs the rebuild to work. The manifest is
now written with the scope's first page, one store lookup per scope per
process, byte-identical to what the backfill writes so restarts still do not
churn the wiki's git history. The startup backfill is unchanged and remains
the repair path for trees written by older releases.ai-memory reindex now names the exact missing or unreadable scope
_meta.md path instead of collapsing the filesystem error to a bare No such file or directory (os error 2). This makes the existing startup-backfill
workaround discoverable when a scope was first created during the server's
last run. (#643)workspace + project values from .ai-memory.toml or
operator configuration on every project-scoped call, preventing another
session's last active project from capturing reads or writes; global searches
and global preference writes retain their scope-free argument rules (#372).reindex (#607, data-layer audit follow-up, item 2). purge_project /
delete_workspace commit the DB deletion first and remove on-disk files
afterward, best-effort; a crash or failure in that window left the markdown
directory (_meta.md included) on disk with no row, and reindex rebuilt
the scope from that manifest — silently undoing the purge. Each purge now
writes a purged_scopes tombstone in the same transaction as the deletion
(a whole-workspace delete uses a zeroblob(16) sentinel project id), and
reindex_all skips any tombstoned scope instead of recreating it. The
inert files are left for a later purge or manual cleanup; reindex stays
non-destructive.(workspace, project, path) could interleave their
file-rename and DB-upsert and transiently leave the on-disk markdown
disagreeing with the DB is_latest row (self-healing on reindex, no data
loss). A per-path async lock now serializes same-path writers while
different paths still proceed concurrently; batches acquire their paths in a
fixed global order so they cannot deadlock.cargo install --git snippet now build with
--locked (#628). Without it cargo install re-resolves dependencies and
ignores the committed Cargo.lock, so a source install could silently pull
a different schemars than the one tested — a plausible source of
build-to-build structured-output schema drift. A unit guard also asserts the
strict-schema normalizer keeps required equal to properties (and closes
open maps), catching that drift class at build time rather than in
production._pending/auto-improve/ sidecar no longer renders a status: line
(#624). It was written once at staging and could never change from
pending — no code path updates or garbage-collects the sidecar — so it
contradicted design principle 14 (SQLite owns approval status) and misled a
human (or a second agent) reading the file. Status lives in
ai-memory pending-writes list / SQLite; the sidecar is a staging-time
snapshot.ai-memory status and the pre-migration backup log now report the data
directory's filesystem free space (#629). The OKF-migration backup gate
proves the archive is writable but said nothing about disk headroom — an
operator whose 447 MB safety archive left 77 MB free had the store fail to
extend its WAL eight minutes later, unlogged, for hours. This is pure signal
(no threshold, no refusal): status shows filesystem free: beside the
storage figures, and the "pre-migration backup verified" line now carries
dest_free_bytes, so the same number is visible right before a migration.Store::open has already advanced the DB schema, so the archived db/ was
already at 2.x and a 1.x binary refused it — the documented rollback was
impossible. The snapshot now runs in the boot path before Store::open,
gated to the real 1.x→2.0 upgrade (a fresh install or already-migrated store
takes nothing, unchanged) and idempotent; the OKF migration reuses that
archive instead of taking a second one. An integration test asserts the
archived DB reflects the pre-migration state.relations) now actually work on OpenAI-family providers
(#630). ConsolidatedPage.relations was an open map
(BTreeMap<String, Vec<String>>), which the strict structured-output
normalizer closes to additionalProperties: false — OpenAI strict mode
cannot express arbitrary-key maps — so the model was structurally unable to
emit any causes/fixes/contradicts edge on openai, openai-oauth,
copilot, opencode, or openai-compat; the 2.0 typed-edges feature was
silently dead there. relations is now a fixed three-field object
(causes/fixes/contradicts), which strict mode expresses and which
serializes to the identical relations: frontmatter — no migration, no
downstream change. A schema guard pins the fixed shape.finalize-session --agent hermes (and claude-desktop, crush, other)
is accepted again (#623). These agents are captured and stored, but
finalize-session reused the install-oriented agent enum — which is
deliberately limited to agents with a first-party installer — so it rejected
them at parse time and their sessions could never be closed, summarised, or
consolidated. finalize-session is agent-agnostic, so --agent now accepts
any agent the store recognises (a genuine typo is still rejected);
install-hooks/setup-agent stay limited. A drift guard keeps the two sets
from diverging again.--agent open-code, omp, pi,
openclaw) authenticate again under install-hooks --apply. Since #552
the bearer is deliberately omitted from the rendered file and persisted to
the 0600 <data_dir>/auth-token file, but only the native hook runtimes
learned to read it back: the generated TS adapters kept rendering
TOKEN = null with no runtime resolution, so every request they make
(hook capture, spool drain, handoff fetch) goes out unauthenticated and is
rejected by a Bearer-enabled server. The templates now render a
resolveToken() helper (static embed, then AI_MEMORY_AUTH_TOKEN, then
the auth-token file) used by authHeaders() and the spool writer.
fetchHandoff() in the same adapters also ignored response.ok, so a
401 error body could be injected into model context as if it were a
handoff; non-OK responses now return undefined like other failures.bootstrap now retries a chunk's LLM call on a transient error before
giving up, instead of letting one blip discard the whole multi-chunk run
(#617). A provider 5xx/429 or a transport timeout/connect failure on
chunk n previously aborted the run and threw away chunks 1..n-1 — the
reporter's own runs died repeatedly to provider 520s and connection
resets. Each chunk now gets up to 3 short, fixed-delay attempts;
deterministic failures (auth, schema, a 4xx, malformed JSON) are not
retried. The retry is deliberately short and bounded, not tenacity-style
escalating backoff (cognee #2840). A durable --resume for the rarer
crash/hard-failure case remains tracked separately in #617.bootstrap no longer aborts the whole multi-chunk run when one chunk
returns no pages key (#614). Later chunks are told which paths
earlier ones wrote, so a model that judges the material already
covered legitimately answers with a rationale and no pages; because
Anthropic's tool_use schema does not enforce required fields the way
OpenAI's strict: true does, that answer reached serde and failed
deserialisation with missing field 'pages'. Since pages are only
written after every chunk completes, the error discarded the work of
all preceding chunks. pages now defaults to empty.install-hooks --agent antigravity-cli on native Windows now emits
an unquoted hook command (#611). The native-binary rendering wrapped
the executable, --data-dir, and --server-url in double quotes, but
Antigravity runs the string through cmd /c "<string>" without
stripping inner quotes, so "C:\…\ai-memory.exe" was "not
recognized", every hook failed, and the cp850 (non-UTF-8) error text
aborted the whole agy session. Antigravity's command is now rendered
bare; other Windows agents keep their quotes.index.md into orphan directories
(dirs with a valid-UUID name but no projects row — near-empty shells left
by older history), and the watcher then warned once per file per 30s
reconciliation pass indefinitely — hundreds of identical lines that buried
real warnings, including, on one host, a 22-hour sqlite: disk I/O error.
Reconcile now checks the project scope once per directory and skips an
unresolvable one wholesale at debug, rather than retrying its pages; if
the row later appears the directory indexes normally on the next pass. The
same orphan skip now also guards the real-time directory-event path
(reindex_project_dir), not just the periodic pass (#616).[[wikilinks]] use — instead
of resolving against the web mount's <base href>, and a namespace
(directory) path such as .../p/_lint/ now lists the pages under it
rather than returning 404. External links, anchors, and dangerous
schemes are untouched by the rewrite.title no longer read back titleless
or trip a bogus duplicate-title lint (#599). Auto-improve could store
title: "" while the page had a proper # H1; now memory_read_page
derives the title from the H1 (then the path stem) when the stored one
is blank, the duplicate-title lint ignores blank titles instead of
collapsing them into one Multiple pages share title "" finding, and
the consolidator derives a title from the body H1 at write time so new
pages never store an empty one. The DB title column (search) was
already correct; this aligns the frontmatter-title readers with it
without rewriting stored pages.fetch_model) now bounds a stalled
connection instead of hanging startup forever (#602): a 30s connect
timeout and a generous 600s overall ceiling (large model files, so
not the inference clients' flat 120s that would fail a slow download).invalid object specified … class=Tree error that left the
server unable to boot (#594). The wiki commit re-hashed files through
libgit2's index, whose stat cache could trust a cached blob OID absent
from the object database (a store carried across libgit2/git versions,
or an interrupted earlier operation) — write_tree then aborted and,
because the migration commits through that path, every restart failed.
The commit now clears the index before staging so every file is
re-hashed from the working tree. (The Docker runtime ships no git,
so the fix is in-library, not a CLI fallback.)Bearer keyword or an
UPPER_SNAKE_TOKEN= shape. The bearer rule requires the literal keyword,
and the generic env-var rule requires [A-Z][A-Z0-9_]*_TOKEN, which never
matches a kebab-case header name. X-Amz-Security-Token (AWS SigV4),
X-Api-Key, Private-Token (GitLab), and Ocp-Apim-Subscription-Key
(Azure) all fall in that gap, and tool output echoing a curl invocation
is a common way they reach capture. A key or token suffix on its own
does not imply a secret, so it must be qualified by an auth word:
Idempotency-Key, Continuation-Token and storage partition keys are
left intact and stay readable in captured output.install-hooks --agent zcode --apply no longer reports a hooks block
ZCode has thrown away as no-op … (already up to date) (#600). ZCode
validates hooks.events strictly and rejects the whole block, including
ai-memory's own entries, over a single key it does not recognize, so
capture never ran (hookCount: 0). Apply only ever merged the six event
keys it writes, so a key it does not write was neither inspected nor
reported, the file kept matching byte for byte, and the report claimed
the install was current. Apply now also withdraws ai-memory's own
entries from event keys it no longer writes, wherever they sit, and
reports what it found: a warning naming any key it withdrew from, and a
note for a key left unable to run anything, both naming the config file.
Changed output: an unchanged ZCode config with such a key now reports
no-op … (unchanged; see the notes below) instead of already up to date, so a redirected stdout no longer carries the reassurance without
the cause. Event keys are never deleted and hooks ai-memory did not write
are never removed, so a key holding someone else's hook is reported, not
touched.User-Agent: ai-memory/<version> and a
per-operation x-opencode-session header. Session consolidation and
auto-improvement reuse the captured ai-memory session ID, while retries and
structured-output fallback keep the same ID instead of appearing as unrelated
requests (#608).as_of time-travel queries and the entity retrieval stream now work
on real stores. Both read the entity index, which was populated only
from an LLM consolidator's entities: frontmatter — absent on the
bulk of a mature store (bootstrapped pages predate it; stable pages
are never re-consolidated), so the index sat empty and as_of
returned nothing. Entities are now also derived from each page's
tags: (the nouns a page is already labelled with) at write time, and
a one-shot idempotent backfill runs on the first start after upgrading
to populate the index for existing pages from their frontmatter —
opening each entity-link window at the page version's own created_at
so historical as_of is correct. Broad tags don't distort ranking
(the entity stream is inverse-frequency weighted), and a store already
written through the current path is a no-op. See docs/temporal.md..serve.lock
single-instance lock and its .serve.lock.holder sidecar. On Windows
the exclusive LockFileEx is mandatory, so the same serve process
that runs the migration also holds .serve.lock and could not read it
back while creating the backup — the walk aborted with os error 33
and the server crash-looped on every 2.0.x upgrade from a 1.x store
(#593). Linux/macOS were immune because POSIX locks are advisory.ai-memory.ps1) no longer crashes with
git.exe : fatal: not a git repository … NativeCommandError when run
outside a git repository — which broke ai-memory status and every
non-repo invocation (#591). The wrapper's script-global
$ErrorActionPreference = 'Stop' turned the repo-root probe's
redirected native stderr into a terminating error under Windows
PowerShell 5.1; the probe now runs under a localized
SilentlyContinue, gates on $LASTEXITCODE, and restores the
preference, falling back to the working directory as before. The
unredirected Docker invocations were never affected.relations: frontmatter values (relation keys and targets)
are now length-bounded before being echoed into warning logs. On a
shared server one caller's page is parsed by a process whose logs
others read; a crafted or oversized relation key/target could bloat or
pollute the log. Bounded to 200 bytes (UTF-8-safe), matching the
bounding every other untrusted-content sink already uses. Found by a
data-layer security audit of the store/wiki crates.חינם ובקוד פתוח, ברישיון MIT. בלי חשבון ובלי מפתח API.