本文へスキップ
メニュー

プロダクト

ソリューション

連携

開発者

言語

開発者

ai-memory v2.4.0をダウンロード

2026年9月21日リリース。プラットフォームごとに自己完結型のバイナリが1つあり、それぞれにSHA-256チェックサムが付いています。MITライセンスです。

バイナリ

ダウンロードを検証
sha256sum -c ai-memory-linux-x86_64.tar.gz.sha256     # macOS: shasum -a 256 -c ...

展開したらクイックセットアップに進んでください。コマンド3つで終わります。

パッケージマネージャーとコンテナ

mise

mise
mise use -g github:akitaonrails/ai-memory

LinuxとmacOS。チェックサムは自動で検証されます。新しいリリースが反映されるまで1日ほどかかることがあります。

Arch Linux (AUR)

Arch Linux (AUR)
yay -S ai-memory-bin    # ビルド済み
yay -S ai-memory        # ソースから

ユーザーサービスまたはシステムサービス用のsystemdユニットをインストールします。

DockerまたはPodman

DockerまたはPodman
docker pull akitaonrails/ai-memory:latest

マルチアーキテクチャ対応。linux/amd64とlinux/arm64があるので、Apple Siliconでもエミュレーションは不要です。

ソースから

ソースから
git clone https://github.com/akitaonrails/ai-memory
cd ai-memory && cargo build --release --workspace

rust-toolchain.tomlで固定されたRustツールチェーンが必要です。

リモートサーバーにフックを送るだけのクライアントマシンには、フルバイナリは要りません。リリースにはai-memory-install-hooksと、Linux、macOS、Windows向けのDocker用ラッパースクリプトも含まれます。一覧はリリースページにあります。

変更履歴

直近12件のリリースです。2.0以降はセマンティックバージョニングに従っています。 この部分はプロジェクトからそのまま取り込んでいるため、英語のままです。

2.4.0追加15件修正11件セキュリティ1件

Security

  • Bumped 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::ContentContentBlock rename (imported under the prior name), the feature set is unchanged, and the 23-tool MCP surface is unaffected. (#794)

Added

  • macOS menu bar companion (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)
  • LLM "dream" pass — cross-session rewrite/merge of cold clusters, scheduled on idle (design-memory-aging.md buckets B2/B3/B4). Where A3 collapses near-duplicate cold clusters extractively (zero-LLM, keep-token union), the dream pass hands each cold cluster to the configured provider to be rewritten into ONE coherent page. It is opt-in LLM, OFF by default, and gated on an R2 number before it may default on: it runs only when the new [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).
  • Belief-strength confidence over the 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).
  • Zero-LLM contradiction detection surfaced through 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).
  • Cold-cluster dedup of near-duplicate episodic pages (design-memory-aging.md bucket A3): the forget-sweep can now cluster near-duplicate cold episodic pages by embedding (cosine-distance DBSCAN with an adaptive k-distance eps, 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).
  • Entropy / boilerplate pre-filter before consolidation (design-memory-aging.md bucket A4): a pure, zero-LLM Shannon-entropy + boilerplate gate that skips low-information session pages (near-empty, whitespace, single-character, or highly-repetitive boilerplate) from the cross-session experience consolidation pass before they reach the LLM prompt, the eval gate, or 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).
  • Extractive tier-down of cold episodic pages (design-memory-aging.md bucket A2): instead of evicting a cold episodic page, the forget-sweep can now compact it — keeping the L0 frontmatter 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).
  • Per-tier retention half-life curves (design-memory-aging.md bucket A1): the forget-sweep's decay rate can now be tuned per memory tier via an opt-in [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).
  • Access reinforcement on the remaining read paths (design-memory-aging.md bucket C1): 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 tier on the LLM synthesis paths: an opt-in 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).
  • Dialectic answer on 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).
  • "Pin before search": 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).

Docs

  • Stopped recommending 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).
  • Refreshed the LongMemEval-S retrieval benchmarks on the 2.4 tree and populated the full-dataset R2 A/B (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).

Fixed

  • A failed scheduled 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)
  • The auto-improve reviewer now excludes 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)
  • Auto-improve proposal staging no longer discards an entire run when one proposal is a create/update misclassification. A 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)
  • The Windows Docker wrapper (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)
  • Automatic handoff selection and cwd-prefix project matching now treat Windows drive-letter and UNC paths as case-insensitive. A Linux server (the Docker Desktop helper) comparing host cwds from Explorer, Git, and PowerShell previously required a byte-exact match, so C:\Users\…\repo vs c:\users\…\repo missed the pending auto-handoff and could mint a fragment project. Unix paths stay case-sensitive. (#806)
  • Fixed 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).
  • The privacy strip now redacts Windows credential paths (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)
  • The privacy strip now redacts secrets written in JSON. The quote before a value put it outside the value character class, so {"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)
  • Terminal escape sequences, NUL and bidi override characters are stripped from captured text instead of being stored. Page bodies and observations are replayed to a terminal by 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)
  • A page write is refused when another live page in the same project differs from it only by case or Unicode normalization. Such a pair is one file on macOS (APFS) and Windows (NTFS), so creating the second silently overwrote the first page's file while the index kept both rows, so reads for either path then returned the survivor's body, and the wiki watcher superseded the overwritten row, losing the original content from disk and index alike. The refusal names both paths, applies on every platform (the wiki is synced between them), and leaves supersedes of an existing path untouched. reindex skips such a pair instead of failing the whole rebuild, reports the count, and logs each one. (#799)
2.3.2変更2件修正10件

Changed

  • 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.
  • A consolidation LLM call that fails on a transient provider 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.

Fixed

  • 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).

2.3.1追加1件修正1件

Added

  • 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.

Fixed

  • 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)
2.3.0追加10件修正2件

Added

  • 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").
  • Boot-time backfill of pre-hook local history, on by default. When a project's ai-memory store is brand new (empty), the SessionStart hook triggers a one-time, bounded import of that project's existing local harness transcripts so installing hooks mid-project no longer starts amnesiac about the very session you are resuming. It replays each local transcript through /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.
  • New 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.
  • New task-oriented 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).
  • Codex assistant-final-turn capture: 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).
  • Cross-project agent messaging: a directed, claim-once inbox/queue so an agent in one project can hand a self-contained request to an agent in another project without pulling that project's context into its own session. Four new MCP tools (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.
  • The session-start "hot context" block now appends a non-consuming inbox notice (a static count only — never message text) when a project has pending cross-project mail, and memory_briefing reports pending_message_count.
  • Added an independent 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).

Fixed

  • install-hooks --apply now preserves an existing --capture-assistant opt-in on a bare re-apply (one with no --capture-assistant flag). Previously a refresh without the flag silently stripped assistant capture; this most affected the new ai-memory run auto-wire, which always re-applies without the flag. The flag still explicitly enables it; an unset flag now keeps whatever is already installed (Claude Code and Codex).
  • install-hooks --apply no longer aborts the entire install when the hook bearer token cannot be persisted under the data dir. This bit the docker wrapper, where data_dir is /data — a container volume the host hooks 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.
2.2.2修正8件セキュリティ1件

Security

  • Updated 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).

Fixed

  • The native 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).
  • Cursor lifecycle events are no longer stored twice on a host where both 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).
  • The POSIX shell hook bundle's spool reader no longer builds the decoded value in memory. 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).
  • The POSIX shell hook bundle now assigns an 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).
  • The POSIX shell hook bundle's 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).
2.2.1修正3件

Fixed

  • The POSIX shell hook bundle no longer drops a lifecycle event when the server is unreachable or answers 5xx. A failed delivery is written to <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).
  • The POSIX shell hook bundle's 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).
2.2.0追加11件変更1件修正1件

Added

  • Retrieval 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).
  • Page-grain ingestion windows (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)
  • The 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).
  • Belief-strength evidence substrate (P2, 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.
  • Standing-answer boot surfacing (P4, 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.

Changed

  • The privacy sanitizer now redacts to a typed marker[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).

Fixed

  • Preserved recorded entity-link retirement timestamps when backfilling page ingestion windows for historical reorg/move-regenerate pages, preventing empty page windows when updated_at still held creation time. Clarified current-index ranking and snapshot-based rollback. (#682)
2.1.2変更2件修正14件

Changed

  • The managed routing snippet distinguishes a reviewed decision record kept in the repository (an ADR directory, a Keep the Why 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).
  • The managed routing snippet now states that Claude Code loads 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).

Fixed

  • A 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).
  • The Docker wrapper (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).
  • A bare 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).
  • Admin requests that fail for a reason the server owns are now logged server-side instead of existing only in the response body. 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).
  • Fixed empty native Codex tool observations by recognizing its top-level tool fields and preserving safe tool-family/call-ID metadata plus bounded, sanitized responses for recognized tools. Unknown tools and capture-excluded file operations retained their existing content restrictions; buffering, retry idempotency, and Stop/SessionEnd semantics were preserved. Corrected the install guide's outdated claim that Codex lacks native SessionEnd (#697).
  • Reads no longer report an empty project after the daemon restarts mid-session. The active-project pointer lives in process memory, so a restart — the one the packages' own post-upgrade note tells you to run — dropped it, and 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).
  • The from-source AUR 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).
  • CLI server errors now name the request. A non-2xx response from the configured server printed only 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)
  • Wiki auto-commits stage what the wiki wrote instead of walking the whole tree, keep the repository open between commits, and no longer drop the commit when another session is writing a file at the same time. A session end now costs what it wrote, not the size of the wiki, and the git history no longer silently misses snapshots under concurrent sessions (#674).
  • The generated pi/omp and OpenClaw TypeScript integrations registered their session-end lifecycle event (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).
  • The 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).
2.1.1変更1件修正8件

Changed

  • The managed routing snippet now states that ai-memory is the cross-harness memory of record: when the surrounding harness has its own local memory feature, durable project facts should be captured in ai-memory rather than duplicated in a harness-local store that other agents cannot see (#671).

Fixed

  • A nested .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)
  • Wiki auto-commits no longer re-hash the whole tree. Since the #594 guard, every commit cleared the git index and re-read every page, so a session end cost the size of the wiki and grew with it; the LongMemEval harness saw its ingest rate fall from 141 to 50 session ends a minute as the tree grew. Staging now goes through libgit2's stat cache and re-hashes the tree only when the tree write fails, which is the #594 case and is now covered by a test that removes a blob from the object store. Commits on one wiki are also serialized: two session ends at once used to collide on the index lock, and the losing snapshot was dropped with a warning. (#665)
  • Preserved typed relation edges in multi-page consolidation. Batch updates now include the same closed 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)
  • OKF-conformed event ledgers are skipped by the indexer again, so a migrated store stops growing without bound. The reserved-file check treated any 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)
  • A UTF-8 BOM on a hand-edited wiki page that has no frontmatter no longer rides into the page body. 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)
  • SessionEnd no longer writes an ephemeral 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)
2.1.0追加5件変更5件修正12件セキュリティ2件

Added

  • Ordered LLM provider fallback chains (#648). [[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.
  • Added first-party OpenCode 2.0 beta (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])

Changed

  • Every LLM chat request now sends 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])
  • The session-start brief's [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)
  • The build is self-contained on every platform: the vendored web stylesheet is now the default and 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.
  • Developer loop: 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.

Fixed

  • The bin/ai-memory container wrapper now runs on Podman without Docker (#636). It auto-selects the engine (AI_MEMORY_DOCKER override → dockerpodman), 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.
  • The OpenCode provider now sends 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])
  • Fixed 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)
  • Authentication-disabled HTTP servers now ignore stale or unexpected Bearer headers and preserve anonymous access. Previously, a client retaining an old 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)
  • Cursor sessions no longer land in the default 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.
  • Cursor sessions are no longer attributed to 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.
  • A project that first materializes while the server is running is now self-describing immediately, instead of only after the next startup backfill (#643). Scope manifests (_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)
  • Generated routing instructions and all project-scoped managed Agent Skills now distinguish session-aware MCP clients from static clients. Static clients are told to pass exact 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).
  • Consolidation prompt assembly no longer re-renders the whole observation projection and re-scores every observation after each pruned one. With a few hundred long observations the quadratic loop cost ~14s per prompt; pruning now works from per-observation scores and block sizes computed once, with byte-identical output.

Security

  • A purged project or workspace can no longer be resurrected by a later 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.
  • Concurrent writes to the same page path are now serialized (#607, item 3). Per-page writes take the shared side of the wiki mutation lock, so two writes to one (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.
2.0.3追加1件変更2件修正9件

Changed

  • Release, Docker, and the 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.
  • The _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.

Added

  • 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.

Fixed

  • The pre-migration safety archive is now taken before the SQLite schema is migrated, so it is a genuine pre-2.0 recovery point a 1.x binary can reopen — restoring the documented "reversible upgrade" (#633). Previously the archive was written inside the wiki migration, which runs after 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.
  • Typed edges (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.
  • generated TypeScript integrations (--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.
  • The wiki watcher no longer logs a scope-resolution failure for every page in a project directory the store has no row for, on every pass, forever (#613). The OKF v0.2 migration seeded an 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).
  • The store error for an unknown project id now says "does not exist" instead of "does not belong to workspace X" (#612). A dangling id names no workspace mismatch to hunt for; the disambiguating lookup runs only on the failure path, so the common case still pays a single query.
2.0.2修正10件セキュリティ2件

Fixed

  • Web UI: OKF bundle-index links (and any relative in-wiki link) no longer 404 (#603). Relative markdown link targets are now rewritten to project-scoped URLs — the same convention [[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.
  • Pages with an empty frontmatter 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.
  • Local-embedding model download (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).
  • The OKF v0.2 migration no longer crash-loops on startup with a libgit2 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.)
  • HTTP headers that carry a credential in an opaque value are now redacted before reaching durable storage. Previously such a header matched no built-in pattern unless it used the 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.
  • OpenCode Go requests now send 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.
  • The OKF v0.2 pre-migration backup walk now skips the .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.
  • The Windows PowerShell wrapper (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.

Security

  • Handoff and workstream-event content is now size-bounded at the store boundary as defense in depth (data-layer audit follow-up, #607). The MCP/hook callers already scrub and cap this prose, but the store is the last gate before durable persistence, so it now bounds each handoff field (16 KiB, matching the observation body) and list length, and a workstream event's free-text content — a caller that ever forgot its own cap can no longer write unbounded content to the DB.
  • Untrusted 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.

GitHubで変更履歴の全文を見るすべてのリリース1.xからのアップグレード

今日からエージェントに記憶を。

MITライセンスの無料オープンソースです。アカウントもAPIキーも要りません。