Skip to content
STAIxBWLBPublic

About

amem: screened, local memory for coding agents. Curated notes, a separate capture tier, ranked recall, an MCP server and opt-in git sync. Part of the ahub family.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

agent-mem

Curated memory with append-only JSONL logs, a derived SQLite index, short- and long-term tiers, a typed graph, goals with leases, and git sync between machines. Extracted from agent-hub, with compatible storage and entry IDs.

Version: 0.23.1. Requires Bun 1.4.2 or later; no runtime dependencies.

Quick start

Install the package, preview setup, then apply it for your client:

bun add -g @staix/agent-mem
amem --version
amem setup all
amem setup claude --apply

Replace claude with codex, kimi, pi, qwen, jcode, opencode or gemini for that client. The default command shows a read-only plan; --apply writes it without an interactive prompt. Complete the client's own hook-trust review when prompted.

Agents can submit pending memory candidates through MCP. Review each candidate with the CLI; acceptance and rejection recheck the configured policy and record the caller's actor and peer:

amem review list
amem review accept CANDIDATE_ID  # or: amem review reject CANDIDATE_ID

Replace CANDIDATE_ID with an id from the list. MCP remains proposal-only: it never accepts a candidate or writes a curated note.

Claude's registered hooks capture events during the session. The configured Codex/Kimi/Qwen SessionEnd hooks and Pi shutdown extension ingest transcript tails incrementally at session end. If those integrations are unavailable, run amem ingest codex (or kimi / pi / qwen) after each session, then repeat the review loop. The setup section describes the registration and native trust steps.

From a checkout, install development dependencies with bun install and run bun src/cli.ts --help.

Setup / plugin

For Orca-managed Claude and Codex homes, setup writes the personal source that Orca copies into new sessions. Claude hooks go to $HOME/.claude/settings.json and its MCP entry to $HOME/.claude.json, because Orca syncs the personal mcpServers over each profile's copy (#243); the profile copies stay untouched. Codex MCP and hooks go to $HOME/.codex/config.toml and hooks.json. Detection requires an exact managed-home shape under ORCA_USER_DATA_PATH (default ~/Library/Application Support/orca); a similar directory elsewhere is unchanged. Preview lists each redirect and its reason. Start a new Orca session after applying. Existing matching managed-copy receipts migrate on apply; undo restores the files and bytes recorded for that installation. Codex hooks still require the person's native /hooks review; setup never grants trust or writes trusted_hash.

amem setup agy registers MCP only in the current git repository's .agents/mcp_config.json, with an explicit absolute repository cwd and a private receipt per repository. setup all excludes agy; run it separately in each repository. Setup refuses tracked config files and JSONC, and warns when the file is not ignored. Restart Antigravity after applying, use one workspace per session, and ensure no global, plugin or running server is also named amem. No global configuration, ingest command or hook is installed. Undo removes .agents only when setup created it and it is still empty; pre-existing or nonempty directories remain. Older receipts still restore their configuration without directory cleanup. See the Antigravity contract and capture limitation.

amem setup jcode registers MCP and capture hooks (below), using Jcode v0.99.1's global JCODE_HOME/mcp.json (default ~/.jcode/mcp.json). An explicitly set JCODE_HOME must be absolute for amem setup: empty, relative and literal ~ values are refused. Unset uses the default, and absolute values retain their literal spelling without trimming. It owns only servers.amem, sets shared: false for a workspace-bound session, and preserves unrelated configuration bytes. Jcode has no native MCP add/remove command in this release, so setup edits the owned entry directly. Setup also adds amem hook jcode to the post_tool and turn_end lifecycle hooks in JCODE_HOME/config.toml, keeping an existing user command for those events (array form). Each captured row is scoped by that event's JCODE_HOOK_CWD; events with no cwd or outside a Git repository are skipped. On turn_end, 0.23.0 also captures bounded journal metadata as Title and Compact rows, independently of the ordinary assistant-text capture. It does not ingest persisted message bodies or snapshots. There is still no amem ingest jcode: saved messages lack per-message origin while the session cwd can change. Launch Jcode with JCODE_DISABLE_CLAUDE_MCP=1 and without a same-name project override, which could otherwise replace the private binding. Use a new workspace-bound session when changing projects. See the Jcode contract and capture limitation.

amem setup gemini registers MCP only, at user scope in Gemini CLI 0.61.0's ~/.gemini/settings.json (GEMINI_CLI_HOME, which must be absolute, moves it). It runs the native gemini mcp add --scope user (and remove for an earlier entry) in a private staging HOME, owns only mcpServers.amem and keeps every unrelated setting. Antigravity's ~/.gemini/config/mcp_config.json is a different client's file and is never read or written; amem setup agy stays per repository. A workspace .gemini/settings.json that defines mcpServers.amem overrides the user entry. Setup refuses settings that are not strict JSON (Gemini allows comments, but setup's exact undo needs strict JSON) and runtime paths containing $, which Gemini expands. No hook, ingest command or GEMINI.md edit: see the Gemini CLI contract for why transcripts are not ingested. setup all includes Gemini CLI when it is installed.

amem setup opencode registers MCP only in OpenCode 1.18.35's global opencode.json (default ~/.config/opencode/opencode.json; an absolute XDG_CONFIG_HOME moves it and is used literally, untrimmed, as OpenCode does; a relative value is refused). It owns only mcp.amem, a type: "local" server with no cwd, so OpenCode starts it in each session's project directory, and it preserves unrelated configuration bytes. OpenCode's mcp add is interactive and has no remove, so setup edits the owned entry directly. Setup refuses strict-JSON violations (JSONC), a sibling opencode.jsonc or config.json in the same directory (OpenCode merges them over this file) and OPENCODE_CONFIG, OPENCODE_CONFIG_DIR or OPENCODE_CONFIG_CONTENT. A project opencode.json that defines mcp.amem overrides the global entry. No plugin, hook or ingest command is installed: see the OpenCode contract for why its session database is not ingested. setup all includes OpenCode when its CLI is installed.

amem setup qwen previews a user-scope MCP registration and a bounded SessionEnd ingest hook in the same QWEN_HOME/settings.json (default ~/.qwen/settings.json). --apply and --undo follow the same confirmed, private-receipt contract as the other targets. Unrelated settings, servers and hooks are preserved. Qwen runs this hook on normal interactive exit; use amem ingest qwen when the session ends without that hook. Native-client operation is source-verified; the installed-package tests use stub registration CLIs.

Preview the exact configuration files, native registration commands and private undo receipts before changing anything:

amem setup all
amem setup claude --apply
amem setup codex --apply
amem setup kimi --apply
amem setup pi --apply
amem setup all --undo

--apply and --undo are explicit writes; the default remains a read-only plan. They do not require a TTY prompt, and an agent marker supplies identity rather than a tool refusal. Follow workspace authorization for changes to machine configuration. Native Claude, Codex, Pi, Qwen and Gemini MCP registration commands run in a private temporary HOME before the listed files are published; current Kimi Code uses its MCP JSON file. No AGENTS.md, CLAUDE.md or rule files are edited. Repeated apply is a no-op even after routine CLI metadata writes. Registrations use the absolute Bun path found on PATH, preserving its stable symlink rather than a versioned Cellar target; temporary package entries (such as bunx caches) are refused; reapply refreshes moved package/runtime locations automatically. Private receipts restore original bytes, permissions and absent-file state when files are untouched. When a CLI updates its own metadata, undo restores only the managed MCP entry and hook rows while preserving those updates; edits to those managed entries refuse rather than overwrite. Config files have an 8 MiB bound and generated TOML is validated before publication.

Claude receives its seven bounded hooks; Jcode receives post_tool and turn_end capture hooks. Codex, Kimi and Qwen receive SessionEnd incremental ingest; Pi receives a session_shutdown extension. Codex's native /hooks review remains a pending person step: setup never grants or bypasses hook trust. Native registration requires a CLI exposing mcp administration (Claude, Codex, Pi, Qwen and Gemini are checked through their help before any registration command); current Kimi Code uses its documented config files. setup all skips and reports missing CLIs; an individual missing target refuses. Setup does not download tools or start a resident worker.

To keep several releases side by side, install each under its own root (for example <install>/releases/<version>/) and register one with amem setup <target> --root <abs path> --apply. The root is resolved to its real path, so a current link registers the release it names, and replacing another install never changes what the registered hooks and MCP server load. amem manifest --json prints the version, the MCP tools (read-only ones as { name, readOnly: true }) and the commands of an install. See versions on one machine.

For a Claude Code plugin installation instead of direct setup registration:

claude plugin marketplace add STAIxBWLB/agent-mem
claude plugin install agent-mem@agent-mem

The plugin packages MCP plus the existing hooks/hooks.json once, using Claude's default hook discovery. It needs no --plugin-dir. Choose either plugin installation or direct Claude setup to avoid registering the same hooks twice. The direct setup dry run names an installed agent-mem plugin and warns about duplicate MCP and hook registrations.

CLI

The flat amem commands are the memory commands of the library (a host such as agent-hub exposes the same ones under its own name); goals use amem goal:

amem list --json
amem search "release decision" --limit 10
amem show <id> --json
amem graph <id-or-node> --hops 2 --json
amem suggestions
amem goal list --all --json
amem goal new "Ship a release" --milestone "Review changes"

amem goal link needs a host that has a task board; amem alone has none, so it refuses.

Memory commands: list, search, show, graph, suggestions, confirm, reject, promote, demote, forget, supersede, rebuild, dedupe, sync, machine, and import. Use --json where the command supports it. dedupe previews candidates; dedupe --apply appends reconciliation operations. amem alias add|list manages reviewed project key aliases; amem capacity reports and proposes capture bounds (see below).

amem import codex reads Codex's Markdown memory as review candidates. It supports both on-disk layouts: memories v1 under $CODEX_HOME/memories (else ~/.codex/memories) and memories v2, the default for fresh profiles since Codex 0.165.0, under the sibling memories_v2/ (memory_summary.md, rollout_summaries/, extensions/; v2 titles start with memories_v2/). The v2 SQLite state is never read. With --from only the given directory is read. When neither layout exists, the receipt names the locations it checked.

Agent markers are honest-session identity context, not authentication. Marked amem note add writes use the existing peer field for attribution; its memory reads use the screened policy and historical reads are refused. Unmarked reads retain their existing unscreened behavior. amem mcp remains scoped to its startup project and can write only candidate and work-list data; hooks and ingest append capture only. Curated candidate acceptance and other CLI writes use the current configured policy. The CLI no longer has a general agent-marker refusal or stdin-TTY gate; use each command's explicit preview and --confirm/--apply flow where provided. amem review walk stays interactive and requires terminal input and output; $EDITOR opens only when a terminal is available.

Configuration and privacy policy

The configured privacy policy screens capture, MCP reads, the SessionStart brief, candidate acceptance, promotion, import, sync, repo promotion and vault-candidate selection. Agent-marked CLI memory reads use the same screened boundary as MCP. Unmarked CLI reads (list, search, show) retain their existing unscreened behavior. The policy combines the built-in secret and personal-data screen with your configured rules.

The machine-local file amem.config.json in the memory home (see amem status for where that is) holds your own policy:

{
  "denyPaths": ["confidential-vault", "customers/"],
  "piiPatterns": ["codename-zebra", "acme corp"],
  "briefBytes": 900,
  "excerptBytes": 800,
  "capturePaths": 16,
  "capture": { "capMb": 256, "retentionDays": 30 }
}
  • denyPaths: substrings of a path that are never captured and that decline any note, candidate or MCP answer naming them. piiPatterns: case-insensitive literal substrings (never regular expressions) that decline a value and redact a capture excerpt. Each list holds up to 64 entries of up to 256 bytes.
  • records: optional boolean, enabled by default. false disables the static native SessionStart instruction and new Claude Record extraction; a repository may disable it but cannot re-enable a machine opt-out. See assistant records.
  • briefBytes, excerptBytes, capturePaths can only lower the built-in ceilings (the 1200-byte SessionStart context, 2000-character capture excerpt, 32 captured paths). capture.capMb and capture.retentionDays are the lowest layer of the capture size and retention bounds: a shard's own config.json and the home's capture-config.json still override them.
  • A file with an unknown key, a bad value, a link, or more than 64 KiB is an error that names the file. An unreadable policy is never treated as no policy.
  • <private>...</private> spans are dropped before anything else reads the text, at every write boundary (capture, MCP candidates and work lists, notes, goals, promotion and import), so they are never screened, counted or stored. Tags match like HTML (case-insensitive, with attributes or spaces, and <private/> opens a span); an unclosed span drops the rest of the text.

A repository may add .amem/config (same shape, only denyPaths, piiPatterns, briefBytes, excerptBytes, capturePaths and records). It can only narrow: its lists are added to yours and its budgets count only when lower. It cannot remove an entry, raise a budget, move the home or change the capture bounds, and any other key is an error (a malformed repository file stops amem in that repository until fixed; it never falls back to no policy). A stricter policy a host passes through the library always wins over both files.

On non-placement profiles, amem sync screens this machine's owned outbound segments before staging. One declined row refuses the whole sync and names its id (never its text); the log remains append-only. On active placement v3, permitted originals can publish while declined rows receive sticky holds, subject to the policy and dependency checks described under placement activation. Malformed or unavailable source metadata still fails closed. Other machines' segments are not the source of the non-placement outbound preflight.

--policy-free is still accepted by promote, import, sync and review accept so existing scripts keep working, and it means nothing more: the configured policy always applies. It does not bypass the secret screen or the sync remote's privacy checks. Forgetting a shared note appends an operation; its previous content remains in the remote's git history.

If the screen misses a secret or personal data, amem forget <id> hides the entry from every reader but does not remove its bytes. docs/purge.md is the procedure for rotating the secret, removing local copies, cleaning machine-local tiers and replacing the sync remote; amem forget lists nearby capture rows, candidates and work items by id, and amem doctor reports forget and prune counts as a reminder.

Writing notes and reviewing candidates

amem note add [--kind decision|finding|contract|fail] [--tier short|long] [--scope project|shared] [--title T] [text... | -] writes one curated note with CLI peer attribution (agent-mem#40 F6). The text is the arguments, - to read stdin through end of input, or nothing to open $VISUAL (an empty one is skipped) or $EDITOR on a private temporary file when a terminal is available (the command is run without a shell: an existing path with spaces works as is, otherwise quotes and backslashes split it, and Ctrl-C reaches the editor while it runs). A fail note takes its five fields instead of free text: --approach, --symptom, --cause, --evidence (repeatable) and --retry-when. All five are required, and the command names the ones that are missing; with none given it opens a labelled template in the editor. The configured policy screens the title and text before anything is written, and a refusal never echoes them.

The SessionStart brief carries one line when candidates wait, for example 2 candidates pending, oldest expires in 27 days (agent-mem#40 F7). It holds numbers only, is read-only (the hook never marks anything shown), comes out of the brief's own byte budget, and is left out when it does not fit. amem status --json adds a candidates object (bare amem status shows the same as a labelled row and next action) (or unavailable when candidates.db exists but cannot be read, never a false zero) and a warning when any is within 7 days of expiry. amem review walk needs a terminal on both stdin and stdout (it refuses when stdout is redirected to a file or a pipe, because nobody would see what it asks about). It prints each pending candidate, oldest first, and only then asks accept, edit, reject, skip or quit: accept goes through the normal policy, edit opens the text in your editor and writes it as a note under the current CLI actor (a candidate proposed as a fail note opens the labelled template), and what it prints has control characters removed. Nothing expires unseen: a candidate counts as shown only when review walk has printed it (the plain review list prints no content and runs in scripts, and the brief line cannot know you read it, so neither marks anything). A candidate is kept past its 30 days until it has been shown, and then for 7 more days. When 500 unseen candidates wait, a new one is refused with a named error (an MCP tool returns a fixed queue-full response) instead of dropping an unseen one.

Candidate review

amem review lists this project's pending machine-local candidates. They are proposals, not memory notes and not evidence. Use JSON to inspect the complete candidate payload before accepting or rejecting its full 64-character ID:

amem review --json
amem review list --kind correction --status all --limit 50 --json
amem review accept <full-candidate-id> --policy-free --json
amem review reject <full-candidate-id> --json

Review commands accept or reject candidates by their full id. CLI actions derive actor and peer from the session and screen both the candidate and proposed note with the configured policy (--policy-free is still accepted and ignored). amem mcp remains proposal-only. The CLI refuses secret-looking candidate input or proposed note content. Plain listing does not open the ordinary memory index; JSON listing reads an existing curated store for overlap hints without initializing an absent one. Acceptance defers unrelated held-note promotions.

Pending candidates include up to three overlapping current notes from the same project as a review hint. amem review list --json adds overlaps with each note's id, title and score; amem review walk prints one line per overlap before its prompt. amem_remember adds an overlaps array of note ids to its candidate receipt. Hints reuse consolidation's deterministic word-overlap measure, with identical text scoring 1, and never merge, reject or block a candidate. Superseded, expired, forgotten, other-project and policy-refused notes are excluded before selection, with no excluded count or id returned. Hints are computed on demand, are not stored with the candidate, and make no model call.

JSON listing screens and tokenizes each current project note once for the whole candidate batch, retaining only three matches per candidate. The score divides shared terms by the smaller term set: a one- or two-term candidate can therefore score 1 against a longer note containing those terms. A score is an overlap hint, not a verdict that the notes are equivalent.

Candidates live in candidates.db beside the memory logs. This sidecar is machine-local: it is never synced, imported, or read by recall, briefs, ask or graph evidence. Hosts must supply an explicit public privacy verdict before putting candidates or counters; private, PII and unknown verdicts are refused. The package supplies proposal-only MCP tools, on-demand consolidation and goal-promotion offers, and callback-confirmed MCP use counters. Candidate retention is 500 per project and 30 days; counters retain 10,000 per project and 90 days. Candidate payloads are capped at 16 KiB.

Acceptance creates ordinary v1 memory data. A correction retains its target's scope, project and storage reach, writes the corrected note and applies the existing supersede operation; its exact current target revision must match. A local target stays short-term; a synced target stays synced, including a demoted target whose effective tier is short. A fail becomes a long-term project note. The accepted text binds the candidate ID, target revision and target ID; a fail proposal without a target records none for both target fields. Both paths require the actual MemoryStore promotion screen to return promote at acceptance time; keep, skip or an absent screen refuses acceptance without falling back to short-term storage. Accepted v1 notes and operations follow the existing storage and sync rules; the candidate/counter sidecar remains local and outside evidence. No new v1 operation is introduced. Decay candidates are listing-only and cannot be accepted; a CLI caller may separately choose amem demote or amem forget. The sidecar receipt, note append and correction's supersede append are separate durable writes. An interrupted acceptance can leave a note without its supersede; it is held for explicit inspection and never replayed automatically.

The library entry exports CandidateStore({ home?, now? }), put(input, verdict), recordCounter(key, values, verdict), and accept(id, { project, actor, verdict, store, policy }). home retains the parent-home convention (<home>/memory); the default uses memoryHome(). The host must derive actor and peer from its session, bind the current project and privacy verdict, and supply a current acceptance policy plus the ordinary store's actual promotion screen. An actor string or caller-supplied public verdict alone is not authentication. Counter writes are idempotent snapshots for the same contextual key, not increments: observed 0 stays zero, while null means unknown (a later unknown value does not erase an observed value).

Open a review-only MemoryStore with deferPromotions: true to prevent unrelated automatic promotions during construction and reads. Explicit candidate acceptance still runs its promotion screen. The standalone MCP registers amem_remember and amem_correct as candidate tools. Additional host tools or capture observers must establish capture exclusion and authenticated producers before those integrations are enabled.

Acceptance normalizes its note once, including timestamp, content ID and short-term expiry, then screens and appends that exact body. The final target-revision check and correction appends hold the same machine-local mutation-lock.db writer transaction used by this version's ordinary memory, import and sync writers. This lock is independent of the derived index, so an ordinary write still persists when SQLite is busy. External or older writers that edit segments without this lock do not participate in serialization. The logs remain authoritative after an interrupted acceptance; the next read refreshes the derived index.

Console (acon)

@staix/agent-mem/ui-plugin is a plugin for the local agent console acon (@staix/agent-console 0.1.3, contract api 1). It adds a Memory area with the panels Review, Recap, Search, Timeline, Work lists, Goals and Capacity, puts pending candidates (group candidate) and a capture capacity proposal (group alert) in the console inbox, and answers the console palette with memory search hits.

amem setup console           # preview
amem setup console --apply   # write $AGENTHUB_HOME/console/plugins/amem.json
amem setup console --undo    # remove it (only when it points at this install)

The manifest holds the absolute path and sha256 of src/ui/plugin.ts (of the --root install root when given), in a 0700 directory and a 0600 file, as acon requires. acon refuses a module whose hash changed, so run amem setup console --apply again after upgrading amem. setup all does not include it.

  • Reads are screened. Search, Timeline and Work lists call the in-process amem_search, amem_timeline and amem_work_state_read handlers for the chosen project and forward every option the tool declares, so console search returns exactly the amem_search set for the same query. Goals and the review queue pass the configured policy; a candidate that fails it, or whose target fails it, is not shown (the panel counts it and points to amem review walk). The console never shows unscreened content, also not to the person. It runs in no repository, so a repository's .amem/config narrowing does not apply; the machine's amem.config.json does.
  • Recap computes the current SessionStart brief only on its own panel snapshots, using machine policy. Curated and work inputs use bounded private scratch copies; capture is read-only, and no live reader lease is taken. It shows the full context, coverage, partial status, token upper bound and generation time; it is not a stored copy of a prior session's injected brief. The console has no repository binding, so the repository section is absent. No recap or new capture field is stored in the memory home.
  • Review shows the candidate kind, proposal, correction diff, stored evidence references, target-note quotes with dates and similar memories. Source sessions, proposing agents and times seen are not recorded by the candidate schema; the panel labels them unavailable rather than inferring them from capture data. An explicitly unavailable project is refused instead of falling back to another project.
  • Review actions are the CLI's. Accept runs the same acceptance as amem review accept and writes the same note, edit-then-accept is review walk's edit (the person's own note, then the candidate closed), reject is review reject. The console stamps the person as the actor (peer person). A privacy-screen hit refuses with a named reason and writes nothing. agent-mem keeps no reject reason, so a reason typed in the console is not recorded, and the reply says so.
  • Acon displays the Undo offer for 10 seconds; the plugin accepts a matching undo record for 60 seconds within its current provider instance. Undo stays bound to the candidate action: undoing an accept forgets the accepted note (an append-only forget op; a note a correction replaced shows again), undoing a reject makes the candidate pending again. A promotion has no undo. merge_into is not offered: the library has no merge operation.
  • Destructive forms (gc, capacity_apply, project_forget, decay_apply, prune) answer needsConfirm with the preview and changes[] and never write on the first call. The console's single-use confirm token carries the fingerprint back; the plugin recomputes the preview and writes only when it still matches. prune needs a generation home, as amem prune does.

Shell completion

Generate completion for zsh, bash, or fish with amem completion <shell>. Generation prints a script to stdout; it does not install files or edit shell configuration. The following commands configure shell completion.

zsh

mkdir -p ~/.zfunc
amem completion zsh > ~/.zfunc/_amem

Enable it by adding these lines to ~/.zshrc, placing fpath before your existing compinit call (use only one compinit call):

fpath=(~/.zfunc $fpath)
autoload -Uz compinit
compinit

Start a new shell. After upgrading amem, regenerate ~/.zfunc/_amem with the same command and start a new shell. To remove it, delete ~/.zfunc/_amem and start a new shell; remove the added fpath entry only if no other completions use that directory.

bash

mkdir -p ~/.local/share/bash-completion/completions
amem completion bash > ~/.local/share/bash-completion/completions/amem

Enable it by adding this line to ~/.bashrc:

source ~/.local/share/bash-completion/completions/amem

Source that file or start a new interactive Bash. After upgrading amem, regenerate it and source it again. To remove it, delete that file, remove its source line from ~/.bashrc, and run complete -r amem in the current shell (or start a new shell).

fish

mkdir -p ~/.config/fish/completions
amem completion fish > ~/.config/fish/completions/amem.fish

Fish loads this directory automatically. Start a new fish shell. After upgrading amem, regenerate the file and start a new shell. To remove it, delete ~/.config/fish/completions/amem.fish and start a new shell, or run complete -c amem -e to clear the current shell's definitions.

Manual Tab checks

These interactive checks remain unrun until a person checks them in each installed shell; automated syntax/candidate tests do not establish terminal behavior.

  • Type amem and press Tab: top-level commands appear.
  • Type amem goal and press Tab: goal subcommands appear.
  • Type amem list --tier and press Tab: short and long appear.
  • Type amem goal state <id> and press Tab: goal states appear.
  • Type amem import claude-mem --db and press Tab in a directory containing a filename with spaces: the path remains one argument.
  • Type amem completion and press Tab: zsh, bash, and fish appear.

Data directory

memoryHome() selects AMEM_HOME first; then the home of a pointer file amem.config.json in the default home (a file with home carries nothing else, and a redirected home's own file cannot redirect again); then $AGENTHUB_HOME/memory, otherwise ~/.agenthub/memory. amem status prints the home in use, which of these chose it, and the configuration files in effect. AMEM_HOME names the memory directory itself; AGENTHUB_HOME names its parent. Existing agent-hub memory directories need no migration. An explicit StoreOptions.home retains the parent-directory convention and appends /memory.

The JSONL segments are authoritative. index.db is derived and can be rebuilt. Entries are immutable; changes append operations. The compatibility schema remains agent-hub.memory/v1 because it participates in entry IDs.

Capture tier and full-text search

From issue #13 (phase P1), the package carries a second, machine-local tier beside the curated store, born sharded (#15 T1): one capture shard per project key at projects/<key-hash>/machine/<machine-id>/ under the memory home, with its own locks so two projects never contend. The capture tier records deterministic tool-event rows (project, agent, event, tool, paths, exit status and a bounded, secret-screened excerpt) in the shard's capture.db. It is never synced, never read as curated evidence, and never writes a curated log, so its maintenance can never evict notes, goals or work lists. Capture is bounded by a soft size cap and a retention window (defaults 512 MB per shard, 90 days), resolved at every invocation from the library options, then the shard's config.json, then the home default capture-config.json, then the capture section of amem.config.json; eviction removes the oldest capture rows first in bounded batches. The 2026-10-17 owner decision keeps the P1 cap per shard; amem capacity apply --split divides the machine-wide budget among the shards (#15 T5, see Managing the project shards). On lock contention a writer publishes a complete immutable per-writer spool segment and exits fast; the next uncontended writer replays the spool idempotently. The capture path makes no network or model call. The pre-T1 machine-wide capture/ (a historical unpublished layout) is migrated per project on open and removed once empty.

Claude's PreToolUse, PostToolUse and PostToolUseFailure excerpts start with a deterministic tool-input line before the existing content. String values use this key order: command, pattern, query, url, description, prompt, skill, subject, then other string fields in sorted key order. Bulk content, old_string, new_string and edits are omitted. Private spans are removed before clipping; the line is at most 512 UTF-8 bytes and the combined tool excerpt is at most 2,000 bytes or the lower configured excerptBytes limit. Existing envelope, secret/PII and path screens remain in force; a refused tool excerpt drops the row. Legacy rows and non-tool/other-client shaping are unchanged.

Selected-project store.search and amem_search first reserve the existing unweighted result's non-capture cohort and capture slots. Only the capture slots are refilled: Stop, UserPromptSubmit and the transcript summary events (Title, Recap, Compact) receive a fixed 1.25x ordering weight, and only the best matching row per existing valid session hash is kept. A row without a hash is its own group. If distinct sessions run out, unused capture slots are omitted, yielding a shorter dense result while retaining the curated cohort and its relative order. Returned scores and explanations retain their unweighted priority values; the capture-slot ordering is separate. Collapsed rows remain accessible through timeline/show. The standalone captureSearch uses weighted session representatives too; --all-projects retains its existing separate merge. The SessionStart brief continues to use chronological capture pages, not this ranking or session collapse.

Project identity (#15 D2) keeps memoryProjectKey: a worktree, a submodule and the main checkout of one repository share its key; two unrelated same-named repositories without a remote get distinct path-hash keys, flagged local in the shard's project.json and the catalog. A no-remote key hashes the main working root (using the actual top-level for a separate git dir), so keys written from the repository root keep their pre-#15 value. Known limit: a linked worktree of a no-remote separate-git-dir repository gets its own key, because git does not record that repository's main working root (probe: alpha and alpha/sub share alpha@<hash>, alpha-wt gets alpha-wt@<hash>); the key stays as is for root-key compatibility, and a reviewed alias reconciles its capture. Keys written by older builds from a subdirectory or worktree still used that invocation's cwd hash; an explicitly written alias maps such a key to its canonical one for capture, work lists and the CLI and MCP project binding. Curated rows written under the old key keep their key in the log (nothing is rewritten), and the CLI and MCP read them under the canonical project: the project filter of notes, goals, candidates, vault candidates and work lists covers the canonical key and every key aliased to it, and presents those rows under the canonical key (#27). The library MemoryStore still reads by the raw key unless a host passes reconcile, and shared rows stay visible as before. Writes still go to the key's own log and shard, and a settle or forget of an aliased row lands in the shard that holds it. amem alias add <from-key> <to-key> appends an op to aliases.jsonl (latest wins, never chained, never automatic), amem alias remove <from-key> appends a removal (the old key reads under itself again, the history stays), amem alias list shows the ops including removals. Both append an alias operation without a general TTY gate. An amem or hub older than this change ignores a removal and keeps the old binding until it is upgraded, so upgrade every host that shares the home first. A correction accepted on an aliased row lives under the canonical key, so after a removal the old key shows the superseded note again (see docs/contracts.md). catalog.db in the memory home (#15 D3's storage part) is derived only from the shards and the alias log and rebuilds to the same rows; the D3 management commands (amem projects, usage, gc and project) are #15 T5, described in Managing the project shards.

store.search(query, filter) searches the curated tier and the current project's capture shard with SQLite FTS5 and BM25 plus tier/recency weighting: natural multi-word queries match partial term sets (the substring filter required every word), and a curated row outranks the same text in the capture tier. Prefix terms match inflections and Korean particles in either direction by retaining the original query term and adding a common-particle-stripped alternative. The FTS copy of the curated rows lives in its own derived search.db (index.db's table set is a frozen contract and gains nothing); it is written from the same apply path as the index, and a prune, crash or rebuild is detected and resynced, so the incremental FTS copy equals a full resync. Where FTS5 is unavailable the engine falls back to the substring filter and reports engine: "substring"; capture recall needs FTS5 and is skipped in the fallback. The library entry exports CaptureStore, captureSearch, readCaptureConfig, writeCaptureConfig, SearchIndex, fts5Available, the shard helpers (resolveAlias, appendAlias, listAliases, shardKeyHash), the catalog (rebuildCatalog, readCatalog, scanCatalog) and the capacity advisor (capacityReport).

amem search <words...> ranks the current project's current curated notes with store.search (FTS5/BM25 plus tier and recency weights). Natural multi-word queries can match partial term sets: amem search lighthouse beacon can return a note containing only one of those terms. Add --filter to retain the previous behavior for current-project curated notes: every query word must appear as a substring, and results are newest first. --history, --as-of and an empty query retain the previous filter/newest-first path. Repository-scope notes remain a separately screened append to these results. --all-projects keeps its existing ranked path and cannot be combined with --filter. JSON output retains the existing note fields.

amem eval [--set ko|<file>] [--k 1,5,10] [--json] measures recall with no model and no network (agent-mem#40 F13): it replays a question set against ranked store.search and reports recall@k, hit@k and MRR. The ordinary curated project amem search path now uses that ranked search; --filter selects the legacy substring behavior. Without --set it scores the built-in synthetic set (26 invented harbour entries, long and short tier, written a day apart, and 32 questions, shipped in src/memory/eval-set.json), which is seeded into a throwaway store, so it never reads or writes your memory home and gives the same numbers on every machine. A set of your own either carries its own entries (self-contained, named by key) or, without entries, names real entry ids (or a prefix of 8 or more hex characters) and is scored against this project's store; each name is resolved like amem show does (a promoted short-term id finds its long-term copy, and a superseded id stands for the entry that replaced it, since search returns only live entries), and a prefix that matches more than one live entry is refused. mem:<id> as a brief prints it is accepted. Questions whose expected entry is not in the store score 0 in all and are left out of answerable, and answerable is scored over the entries the store holds, so a ranking problem and a coverage problem stay apart (n/a when no question is answerable). --set ko scores the second built-in set instead: 37 invented Korean entries and 38 questions (particle variants, spaced and unspaced compounds, mixed Korean-English terms, Latin abbreviations, short and long tier), with its baseline recorded in docs/contracts.md (agent-mem#62). Set format: docs/contracts.md#recall-evaluation-f13.

External retrieval measurements for MCP, library search and a plain BM25 baseline are recorded in LongMemEval S/M. The full datasets stay outside the repository; CI runs only synthetic harness fixtures.

The R7 capacity advisor reports this machine's load and proposes capture bounds: amem capacity [--json] shows capture size and growth, disk, memory, the rolling metrics.jsonl readings and the proposal with its binding constraint; amem capacity apply [--cap-mb N] [--retention-days N] saves a preview, and only a matching later --confirm writes the home default config. The CLI previews capacity changes and writes only when the matching command is repeated with --confirm; an agent marker does not refuse the command. The read-only proposal is also available through amem_capacity. Under the 2026-10-17 owner decision (item 2) the proposal is the largest cap and retention that fit, and it may be above the current values: the cap is the disk-share bound itself (default 2% of free space, --disk-share), and retention is the share divided by the measured daily growth, up to a 365-day ceiling (a ponytail: constant, a person setting is the upgrade path). A hook p95 over 150 ms or hook RSS over 100 MB halves the proposed cap, and search RSS over 200 MB halves the retention. The reason names every binding constraint: disk-share retention or retention ceiling, disk-share cap, R3 search RSS, R2 hook cost. A proposal is never written by the advisor, and apply still takes --cap-mb and --retention-days to override it. Benchmark samples stay in the measuring script's synthetic home; only real hook and search invocations feed the advisor's rolling metrics.

Repo scope (#15 T4)

Team-shareable decisions, contracts and fail lessons can live in the repository itself, in .amem/log.jsonl, and travel with git. Nothing enters it by itself.

amem promote <id> --to repo --policy-free            # preview: the exact line that would be committed, and the repository's visibility
amem promote <id> --to repo --policy-free --confirm  # append it (private repository)
amem promote <id> --to repo --policy-free --confirm --confirm-public <id prefix>   # public or unverified repository: per entry
amem repo status                                     # the file, its entries, what the screen withheld
amem repo attributes [--confirm]                     # add `.amem/log.jsonl merge=union` to .gitattributes, only when you ask

The CLI previews the entry; --confirm appends it. The entry is screened (secrets, personal data, private paths, the repository's own local.deny and pii.paths with agent-hub's glob rules), and amem never commits or pushes: you review and commit the file. A repository counts as private only when every remote's effective URL (after insteadOf) is a github.com repository that gh reports private; any other host is unknown and asks for the per-entry confirmation. The file is read as untrusted input: the newest 1 MiB, no control characters, a future date shown as now. Recall, brief and MCP search include the current repository's .amem/ when that repository is the project, never another project's.

Managing the project shards (#15 T5)

One catalog view across every project shard of the memory home. Commands retain their own preview, confirmation and reader-admission fences; agent markers do not trigger a general refusal and no general TTY gate remains:

amem projects [--all] [--json]            # key, state, aliases, capture and curated sizes, disk use; forgotten ones only with --all
amem usage [--limit N] [--all] [--json]   # totals and the largest shards
amem gc [--confirm] [--batches N]         # each shard's own retention and cap, in bounded batches; a preview without --confirm
amem project export <key> --to <file>     # one project's capture, notes, goals and work lists as one new 0600 bundle (never overwrites)
amem project archive|unarchive <key>      # take a cold project out of recall, or bring it back (append-only ops)
amem project forget <key> [--confirm]     # append-only record that takes one project out of recall and every listing for good; preview first
amem search --all-projects <words>        # the reply begins "crossed projects" and every hit names its project; --include-archived reads cold ones
amem vault-candidates [--all-projects | --project <key>]
amem capacity apply --split [--confirm]   # each shard's share of the global capture budget, written to its own config after a preview
  • archive, unarchive and forget are append-only ops in lifecycle.jsonl of the memory home (local, never synced, like aliases.jsonl): nothing rewrites a line and nothing deletes a shard. The latest op wins and forgotten is terminal. An alias resolves to its one canonical project, and the reply names it. An archived or forgotten project has no writable capture store, no SessionStart brief and no MCP server; unarchive restores an archived one. A forgotten project's bytes stay on disk: forget is an append-only lifecycle record, not an erase. An alias shard of an archived or forgotten project counts as that state, alias add to or from a forgotten key is refused, and hooks that staged capture for a project before it was archived or forgotten are retired at the next drain (counted as project_archived or project_forgotten). openShardStore refuses such a project at the open, so a library caller such as agent-hub is gated too once it pins this release.
  • gc opens only the capture store of each shard and calls its bounded evict() (at most four batches of 256 rows and 25 ms per call), so a cap shrink never takes a shard in one pass: the report says remainingOverCap and the next run continues. It never opens a curated store, a goal or a work list. An archived shard is still bounded; a forgotten one is skipped and listed as skipped.
  • export changes no content under the memory home: it opens the capture databases read-only and reads the curated store, and the only files that may appear or refresh are derived (the curated index.db and search.db, SQLite sidecars and catalog.db), which every reader rebuilds from the logs. A destination inside the home is refused. The bundle is screened like sync and promotion are, so a secret-looking value, a PII pattern or a denied path is withheld and counted in withheld.
  • search --all-projects runs one ordinary project-scoped search per active project and merges by score. It is a person command: the MCP server stays bound to one project and has no all-projects argument, and the raw memory command (what a host exposes as its own memory command) refuses the flag.
  • capacity also prints a per-shard split of the global disk-share budget. Each shard's weight halves every 14 days since its last capture, so recently used projects keep more capture and cold ones shrink first; the split never exceeds the budget, gives every shard at least 1 MB, keeps every shard at what it holds whenever the total fits, never proposes a retention shorter than a shard's idle time plus 30 days, and says when the budget forces a shard below what it holds. When the budget holds fewer whole MB than there are shards, no split is proposed and capacity says why (splitRefused). It proposes capture bounds only, and never evicts curated notes, goals or work lists. capacity apply --split previews, then --confirm writes exactly the previewed values to each shard's config.json, and only if the preview is under an hour old and the set of shards and every shard's config are unchanged since then (a --disk-share given only to --confirm changes nothing that is written).
  • amem lists vault candidates and never writes the vault.

Resource budgets are acceptance criteria, measured by bun scripts/measure-budget.ts in CI on Ubuntu and macOS: a hook-like append (including the spool-and-exit path under contention) must stay under 150 ms p95 wall time and 100 MB RSS (R2), the search path under 100 MB idle and 200 MB peak RSS (R3), and no amem process may remain after exit (R1). For the actual-hook scripts (measure-hooks-budget.ts, measure-transcript-budget.ts, measure-full-spool-budget.ts) R2 is three gates: a 150 MB ceiling on every invocation, a regression gate of +10% over a committed per-runner p95 baseline (docs/bench/r2-baseline.json), and a 300 ms per-invocation maximum on the handler's own wall, with the outer CLI wall reported only (the transcript harness enforces it too since #226); see docs/contracts.md (#228). The script prints a machine-readable verdict per budget and its CI log is also the FTS5 availability evidence for Linux (R5). The AC4 retrieval re-check of the frozen P0 query set is in docs/p1-ac4-recheck.md.

Claude Code hooks and SessionStart context

The package includes a local Claude Code plugin: .claude-plugin/plugin.json and hooks/hooks.json. Install the package as above, then explicitly load its package root in a person's Claude Code session. A checkout can be loaded directly:

claude --plugin-dir /absolute/path/to/agent-mem

Use the directory containing .claude-plugin/, hooks/ and bin/, not the global executable directory. These are setup instructions, not an installation performed by this repository. Package installation alone does not edit Claude settings, register the plugin or disable any existing memory integration. Check for duplicate capture hooks before opting in; the separate P5 cutover owns changes to existing integrations.

The packaged executable uses #!/usr/bin/env -S bun --smol, and plugin commands explicitly pass bun --smol. An explicit bun bin/amem.js hook ... invocation without that flag replaces its bootstrap with execve before loading hook stores/policy. That fallback keeps the same PID, stdin, preload arguments and original wall deadline; it does not add a third Bun process. Process helpers load on demand, and machine identity lives in a small shared leaf, avoiding eager curated-store loading in the hook parent/work-list path.

The plugin observes seven events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop and SessionEnd. They are deterministic capture/context hooks, never permission hooks: they cannot deny a tool call, replace its arguments or turn a capture failure into a native failure. Input is capped at 64 KiB. Malformed, private, secret-looking, denied-path or unavailable input is skipped, and hook errors exit successfully without a native decision. All seven events may append screened rows to the current project's machine-local capture shard; only SessionStart adds context.

The standalone brief prioritizes open work lists, recent curated notes and then recent capture; native SessionStart uses the narrower deadline-safe sources described below. The memory context starts with an untrusted-data warning and per-section complete, partial or unavailable coverage, followed by JSON records. Native SessionStart may prepend the package-owned Record instruction before that warning. Capture records carry evidence: false. The default context bound is 1,200 UTF-8 bytes, counted conservatively as a token upper bound, including the header. Unicode-safe display limits flatten control/line separators; memory text cannot introduce a new header or instruction frame. Full rows and composed work views pass standalone and any stricter host policy before selection; displayed projections are screened again. Canonical project scope includes reviewed aliases for curated notes and work lists, excludes foreign shared-project rows, and is rechecked with profile/selector bindings after asynchronous policy calls.

Before optional work starts, the parent screens the complete hook input and bounded normalized capture fields, then closes and atomically publishes a private immutable record in <memory-home>/hook-spool/. It retains normalized capture data and a physical-cwd/scope stamp, never the raw native envelope or native argument vector. Non-SessionStart events use the fixed Bun --smol worker in a short-lived owned process group. The worker resolves the current project, rechecks scope, drains by a stable receipt, and acknowledges only after append or capture-spool publication. The parent reaps it, and it has its own deadline. SessionStart computes context in the already-running CLI, avoiding a second Bun boot. Its native metadata commands have owned shell supervisors with the same remaining deadline, an armed self-watchdog and group cleanup even if the CLI dies. A timeout leaves accepted capture pending for later replay; unresolved pending records never enter context.

Pending capture has an ordinary soft tier of 256 files/2 MiB and a separate private reserve of 32 files/256 KiB, admitted before optional recovery. Replay is globally oldest-first across both tiers, at most 32 files/256 KiB per drain. A record replays only when its original non-null scope positively matches. Null scope is never adopted later, even with a physical cwd receipt: protecting project privacy takes priority over recovering an unattested event. Alias/profile/shard changes invalidate the derived identity cache and reader binding without changing pending scope. Positive fresh native project identity may still supply known-work context when pending scope is unknown; it does not authorize replay of those records.

Known-format pending records whose cwd is positively absent can retire after one hour (capture and file modification time must both exceed the grace). Other undrainable records keep the seven-day rule; a changed device/root/scope alone never proves cwd removal. Fair-share pressure can retire known-format records with a counted scope_capacity loss, while unknown/newer schemas retain seven-day protection. Each retirement persists numeric reason/count metrics before removing the body; a failed receipt leaves it pending. Retired bodies never become replay or context sources, and records are never rebound to the admitting project. Malformed publications still use bounded owned-file cleanup. See the spool admission contract for identity, N-1 and retention details.

If both replayable tiers fill, or SessionStart's reserve alone fills, the current event can be dropped. The library receipt reports dropped: true, dropReason: "spool_full", dropCount: 1, and the metric reports captureDrops: 1 with captureDropReason: "spool_full". Non-SessionStart native stdout remains empty. Native SessionStart emits only hookSpecificOutput, placing a bounded notice with reason counts from the last 200 metric samples after its untrusted-data header inside additionalContext, not in new top-level fields. These are rolling-window counts, not all-time totals. The header stays before all memory data even when the brief is otherwise unavailable; the optional trusted Record instruction precedes it. The combined instruction and context stay within the same byte bound. Finite-capacity and retirement losses are observable rather than reported as captured. Exact-once replay applies to positively bound retained records that can progress; unlimited lossless intake is not promised. Soft-cap overshoot, unpublished .writing/ leftovers and power-loss durability retain the contracts ceiling.

SessionStart prioritizes known open work. Native curated notes use a private prebuilt section prepared after curated writes and sync, with current policy screening before work and whole-line output after work. Admission requires all writers to be upgraded and a person-raised home or curated floor of 0.24.0 or later; code never changes that floor. Below it, native curated stays deferred. Migration and profile/selector changes invalidate caches without rebuilding; a later curated write or explicit trigger prepares them. Only an unmarked person amem brief or an explicit off-hook host call to the root export prepareCuratedBriefCache(base, project) prepares a quiet project cache. The export is a write: it opens one selected store, reads at most four 32-row pages and writes one file up to 32 KiB. MCP, agent-marked CLI reads and library sessionStartBrief() never prepare it. A project with no curated writes and neither explicit trigger keeps native curated unavailable; mutations rebuild only their touched projects after invalidating old derived files. Missing and stale files report curated cache missing or curated cache stale without rebuilding. Profile/generation binding, selected-note expiry and a 24-hour age cap bound freshness; see the curated cache contract. Native capture uses the prebuilt Record/Recap/Compact section; raw chronological history remains a library source. Neither native cache path creates schema/FTS or repairs indexes. Missing/locked/incompatible sources are explicitly unavailable. The standalone sessionStartBrief() retains all its original sources. Recovery failure cannot discard a computed brief. Work and reader-lease host ports use zero busy-wait and cooperative deadline checks; ordinary library callers retain their original defaults, including 3,000 ms waits. No watcher, resident worker, model/network call, embedding job or vault access is added. Host policy callbacks remain unsupported by runHook, while the standalone brief composes them normally.

SessionStart admits directly to the reserve with at most 32 metadata entries and performs no active-tier scan or replay. Both tiers use per-cwd fair shares (64 active records, 8 reserve records), grouping together all HOME/scope variants of a cwd. Admission can retire native records from an overrepresented cwd, counting each loss as scope_capacity before removal. Accepted summaries survive same-cwd pressure; new summaries backpressure without advancing their transcript cursor. A fully verified foreign all-summary inventory can use counted summary_capacity eviction; otherwise it stays pending for replay. All admission maintenance shares a 32-file/256 KiB body-read bound; unknown/newer schemas retain their seven-day protection. Non-SessionStart hooks maintain and drain both tiers. This is bounded reserve admission, not O(1) accounting; an atomic slot-reservation protocol is the upgrade. Parent brief storage still contains synchronous filesystem/SQLite work between cooperative deadline checks; asynchronous/deadline-aware storage remains the upgrade, not a larger handler limit. The R2 limits remain 150 ms p95, with a 140 ms handler budget; the absolute 100 MB peak RSS was retired by #228 for a 150 MB per-invocation ceiling plus a per-runner baseline regression gate. Functional tests construct a programmatic host-only deadline; the actual executable exposes no environment or CLI deadline extension. Its exec-only boot-clock handoff can only shorten the deadline and is cleared before hook import. Dedicated budget scripts retain the actual CLI, original sample counts, cold attempt 0 and timeout/reaper assertions. The P3 receipt identifies prior-head evidence separately from current acceptance; the current rework has no newly claimed both-OS green CI result.

A private admission cursor rotates bounded inspection across pending records (at most 32 files/256 KiB per pass), so a healthy first slice cannot indefinitely hide later removed/unknown entries from retirement checks. Git include/conditional identity dependencies are traversed within depth four and 16 files. Pending scope hashes relevant semantic project-identity values rather than unrelated full-config or HEAD churn; derived cache validity may additionally hash raw configuration/HEAD bytes. Unconditional and gitdir includes bind semantic identity values. Dynamic onbranch/hasconfig or unknown conditional trees without identity keys bind nothing; if they contain origin/rewrite/worktree identity keys, scope stays unknown. Native read-only identity may still supply known work, but it never adopts that pending capture; counted scope_unknown retirement needs seven days. This conservative ceiling is upgraded by evaluating onbranch against HEAD at drain time, not by adding a condition interpreter in this PR.

Successful native project identities have a private, bounded derived cache (128 cwd entries, 4 KiB each). A hit must match the current project scope, stricter Git-validity metadata and executable revision; a miss still uses the shared hardened Git resolver. It stores no capture or context, and does not guess an identity when Git fails. The hook uses the selected developer Git directly only when PATH selects Apple's root-owned shim; custom PATH binaries remain selected. Cache ceilings and invalidation are in the P3 contracts.

SessionStart honors Claude Code's source field. On resume the earlier brief is already in the restored transcript, so the hook emits only the untrusted-data header line, plus the capture-drop notice when there is one, and no brief body. startup, clear and compact (compaction removed the earlier brief), as well as a missing or unknown source, keep the full brief. source is read, never stored. Only the native SessionStart hook follows this rule; sessionStartBrief(), amem brief, MCP tools and other clients are unchanged.

Hook capture rows carry two optional fields: session, a 16-hex hash of Claude Code's session_id (never the raw id or transcript path), and subagent: true when the event fired inside a subagent (its payload carries agent_id). The SessionStart brief and sessionStartBrief() leave subagent rows out; search, timeline and show keep them and show the subagent field. Older rows read unchanged. Field sources and the mixed-version rule are in the contracts.

Doctor and the sync cadence

Doctor checks applied setup receipts against the resolved configuration files. Its additive setup section names a target, file and entry when MCP or capture wiring is missing or changed, or when the receipt points at a different setup path. It reports missing Codex hook trust records as review-needed; a present record is not proof that its digest is valid. Configuration contents and stored trust hashes are never included in the report, and these checks write nothing into the memory home.

amem doctor, amem status and amem setup print a readable report: an overall state, one labelled row per check (OK, ATTENTION, SKIPPED, UNAVAILABLE) and the next actions. Color appears only on an interactive terminal and is turned off by NO_COLOR or TERM=dumb. Add --json for the machine-readable object (for status and setup this was the default before #125). Setup also shows each available native client's version against its recorded layout verification version; the advisory probe uses a private staging home, reports unknown or unrecognised versions without blocking apply, and is skipped for Antigravity and undo.

Changed default (#134): amem ingest, amem migrate, amem prune, amem capacity apply (including --split) and amem vault-candidates now print readable states, labelled facts and next actions. Ingest uses that format only when stdout is a terminal; ingest stays JSON when stdout is not a terminal, because native hooks read it. New Codex, Kimi and Qwen hook registrations explicitly request --json. Pass --json for automation and saved artifacts; it retains the previous objects and serialization without ANSI, including compact JSON for ingest. Capacity preview and confirmation receipts use the selected format. Exit codes, capture-only ingest and policy screening are unchanged; migrate and prune retain reader-admission and split-marker fences.

Prune apply still requires a sealed JSON plan. Save amem prune --dry-run --cutoff <UTC> --json > plan.json, then use amem prune --apply --plan plan.json in the same project (keep --user on both commands if selecting the user shard). The readable preview is not a plan file. Capacity confirmation keeps the same arguments and adds --confirm.

amem doctor prints one read-only report of this project's memory: duplicate and stale notes, pending candidates, capture freshness and hook registration for each CLI, Qwen included (a CLI with no hook is manual; a registered Codex hook that never captured points to the /hooks trust review), recent capture drops by reason (counts only, from the bounded metrics tail), the SessionStart brief's token cost, whether the index equals a rebuild of the logs, reader leases and the policy in effect. amem doctor --json is a stable machine-readable form (amem.doctor/v1). It writes nothing into the memory home (every check runs on a temporary copy of the logs) and creates nothing in a home you never used.

Intake runs at the end of each Codex, Kimi, Pi and Qwen session once amem setup <cli> --apply has registered it. Sync is off unless you ask for it: amem registers no sync hook and runs nothing between sessions. To sync on a schedule you have authorized, call amem sync --if-due from the chosen shell or scheduler; it has no TTY gate. This release registers no sync schedule or hook, and the configured privacy and remote-visibility checks still apply. It syncs at most hourly, backs off 15 minutes doubling to a day after failures, stops after 30 seconds, and does nothing without a remote set by amem sync --remote <private git url>. --force skips the schedule and the backoff.

V1 split-home sync durably holds verified foreign-generation prunes with a "held, not applied" warning. The opt-in v2 path below applies eligible complete same-realm public prunes across generations after durable proof and root-map commitment. Unsupported historical lineage and multi-realm translations remain held; targets can remain visible and agent-mem#15 AC4 stays open (see the sync contract).

Native transcript intake

amem ingest reads existing Codex, Kimi, Pi or Qwen transcripts and writes screened deterministic capture rows, never curated notes. Run it from the project to bind intake to that working directory; the CLI exposes no project/home override or policy bypass:

amem ingest codex --since 2026-10-01T00:00:00Z --json
amem ingest kimi --json
amem ingest pi --since 2026-10-01T00:00:00Z --json
amem ingest qwen --json

Discovery uses CODEX_HOME or ~/.codex (sessions/ and archived_sessions/), KIMI_CODE_HOME or ~/.kimi-code (sessions/**/wire.jsonl), and PI_CODING_AGENT_DIR or ~/.pi/agent (sessions/). It does not follow directory symlinks. A native record must carry a recognized schema/time and an observed absolute working-directory binding that resolves to this canonical project, including reviewed aliases. Unknown working directories, unsupported Kimi wire versions, missing timestamps and unavailable source roots are reported rather than guessed as an empty successful history. Foreign/private/secret rows are not imported. Full native envelopes and extracted capture inputs both pass current policy.

Qwen Code uses QWEN_RUNTIME_DIR, then QWEN_HOME, then ~/.qwen, with bounded discovery of projects/*/chats/*.jsonl. Each row proves its own project through cwd; a sanitized directory name is not an identity. The reader supports the verified 0.26.1 native envelope, an internal format rather than a public schema, and excludes thought/media content. For advanced.runtimeOutputDir, export the matching QWEN_RUNTIME_DIR; amem does not reproduce merged Qwen settings or read .env. See the Qwen capture contract for source pins, cursor upgrade behavior and limitations.

A receipt with reason: "unsupported_version" and a version (for example 0.27.0 for Qwen or 1.6 for Kimi) means the client wrote a native format version this amem does not read (newer or older than the supported ones): nothing from those records is captured. Qwen records are read again once an amem update supports that version, because the Qwen cursor is keyed to its decoder contract. schema_or_time still means a malformed or untimed record (#136).

Per-source cursor databases and a fair rotation cursor live in the capture shard's private ingest/ directory. Filenames and source identities are hashes; cursor contents retain offsets, anchor hashes, scope/protocol/cutoff and ownership metadata, not raw transcript paths or text. Capture receipts make replay idempotent across a crash between append and cursor acknowledgement. Repeating the command resumes its bounded tour instead of repeatedly favoring the first files. Broadening --since rechecks earlier history while existing capture receipts prevent duplicates. The JSON receipt reports files, read, imported, skipped, spooled, remaining, errors and an optional bounded reason; unavailable coverage is not silently represented by zero imported rows.

The default invocation reads at most 4 MiB and attempts 32 sources; trusted library options can raise those limits to 16 MiB and 128 sources. It examines at most 4,096 records, bounds a record at 256 KiB, and caps discovery at 4,096 directory entries/depth 8. Re-run a receipt with remaining: true after inspecting its reason; ingest creates no polling timer or watcher. Oversized records, including an unbroken record longer than the 4 MiB default window, are counted once and discarded through their next newline using a durable byte offset/skip flag. Later valid records remain reachable without retaining oversized content; an unfinished oversized tail stays pending until its newline arrives. The cursor assumes append-only native files and verifies file identity plus the last 1,024 acknowledged bytes. Historical rewrites outside that anchor are a declared ceiling, not full-file verification. These readers do not establish account-backed native interoperability, P5 cutover or P6 live-week acceptance.

Library

import { MemoryStore, memoryProjectKey } from "@staix/agent-mem";

const store = new MemoryStore();
try {
  const notes = store.list({ project: memoryProjectKey(process.cwd()) });
  console.log(notes);
} finally {
  store.close();
}

The root entry needs Bun (it loads bun:sqlite). Node hosts that need only the deny list, hardened git invocation, realPath or process identity import @staix/agent-mem/platform, which uses node: built-ins only.

The entry point exports the store, types and identity helpers, sync and claude-mem import, project/home helpers, raw memory/goal command handlers, and platform helpers. Hosts inject their own privacy policy through StoreOptions.screen; CLI wrappers can supply screened reads and import policy through the raw command options.

Contracts

Shared segments retain the agent-hub.memory/v1 profile. OP_CONTRACTS and V1_OPS keep the shared-v1 operation contract; both shared append paths refuse unknown or truth-changing operations. The separate GENERATION_OP_CONTRACTS enables prune and local exact-entry vault_link after an explicit generation selection. correct, lift, consolidate and group remain blocked. Reader announcements cannot authorize a new operation or relax generation isolation.

The shared-v1 contract suites run the published 0.1.1 reader and compare all six v1 derived tables after replay and rebuild; generation fixtures cover their expanded derived state separately. Privacy and evidence matrices test the core's actual callbacks and sidecar boundaries; model-facing reads, PII turns, staleness checks and capture remain the host's responsibility. Direct person reads remain unscreened. Expiry after a wall-clock rollback retains the existing C05 ceiling: a rebuild can restore a previously expired row until its deadline passes again. A generation can persist exact-entry prune; shared-v1 and unpruned entries retain their existing expiry limitations.

See the contract and coverage tables for the fixtures, host-owned rows and declared ceilings. These are automated synthetic checks, not proof of the separate multi-machine or managed-agent live legs.

Generation migration and exact-entry prune

Available from 0.3.0. Do not migrate a memory home that agent-hub also uses until agent-hub pins a generation-capable agent-mem and binds the selected profile (rollout order in #9); until then a running hub keeps writing shared v1 and the person's notes would split.

Shared v1 remains the default until the memory home is explicitly migrated. A generation uses M/generations/G/ and a separate Git repository/ref (refs/heads/amem/gen/G); legacy directories and main are unchanged. The one-way readonly bridge consumes complete validated legacy prefixes without using the old index or writing legacy notes. Old programs continue using their legacy view, and generation-created notes never flow back to it.

Complete malformed legacy lines (invalid JSON, unknown operations or mismatched content IDs) are skipped and counted, matching shared-v1 handling. Migration dry-run/apply and the person's human CLI status report the skipped count without printing the bodies; JSON list output stays an array of notes. Prefix rewrites, truncation and non-fast-forward legacy history still refuse access. A generation never promotes a bridged note or applies its own settlement to one; only generation-born notes receive generation settlements and promotions.

Generation bridge reads use plain-file snapshots captured during explicit legacy sync and never invoke Git. Unchanged segments use metadata checkpoints without reading or hashing the data, including after reopen. A seeded live handle hashes only the appended suffix, while comparing existing prefix bytes on changed input; the first changed read by a cold handle and an explicit rebuild verify the full prefix. These are read-path bounds, not a latency or speedup promise.

The following interfaces use an already initialized synthetic memory home. They do not authorize real-data migration. Bare amem migrate and amem migrate --shards preview; add --apply to write. --dry-run remains a preview alias, while --resume and --deactivate stay explicit operations. Use the same current project directory throughout planning and apply:

AMEM_HOME=/path/to/synthetic-memory amem migrate --dry-run
AMEM_HOME=/path/to/synthetic-memory amem migrate --apply
# If interrupted, resume the recorded generation rather than creating another:
AMEM_HOME=/path/to/synthetic-memory amem migrate --resume
umask 077
AMEM_HOME=/path/to/synthetic-memory amem prune --cutoff 2026-10-01T00:00:00.000Z --dry-run --json > prune-plan.json
AMEM_HOME=/path/to/synthetic-memory amem prune --apply --plan prune-plan.json
# Explicitly select shared v1 again while preserving both histories:
AMEM_HOME=/path/to/synthetic-memory amem migrate --deactivate

Migrate and prune retain reader-admission and split-marker fences; agent markers and missing stdin TTY no longer cause a general refusal. --generation <32-hex-G> selects an explicitly named generation during migration/join; it is never discovered and activated from a remote automatically. Invalid active metadata blocks access rather than silently opening shared v1. Existing handles remain bound to the profile they opened; a selector change does not restart or rebind a running hub.

Explicit --deactivate is the recovery path even when the selected generation is corrupt or missing. It reads the selector under the home-wide profile lock, selects shared v1 and retains all generation and legacy data. Normal reads still refuse corrupt active state. Shared-v1 sync keeps its pre-generation Git invocation and caller environment; generation-only sync adds repository-environment isolation and a private child-process umask.

A prune plan is a local JSON receipt containing exact target/basis hashes, selector/project/policy revisions, fixed row bytes, local counts and a seal. Apply commits that same receipt after revalidation; it is not an authorization token. Maximums are 256 KiB per plan, 16 rows, 256 targets, and 32 targets/16 KiB per row. Larger automatic selections report hasMore; another plan handles remaining targets. Keep redirected plans private and out of source control, model context and sync.

Only explicit serialized expiry is eligible: a note's expires, or an exact demote/settle row's by. Implicit backstop or instance-TTL deadlines cannot prove prune eligibility. Exact pruned IDs stay unavailable after clock rollback, including get/alias/graph-start paths. A different later promotion or replacement ID remains an independent note; this is not text/family removal, log erasure or storage reclamation. Unpruned expiry still follows the clock.

Standalone CLI prune is local-only, even over a synced target. No --policy-free escape enables public prune. A later policy-aware host integration must authenticate intent and attest the exact public payload and current policy revision; only verified public target/basis provenance may sync. Local masks, plans, journals, caches and sidecars never sync. Generation sync refuses public or unknown remote visibility. An interrupted apply resumes the same durable row IDs and exact partial tail; it never rewrites complete lines.

Candidate/counter sidecars are not migrated. amem review list --kind memory lists the new machine-local memory candidates under a selected generation; the default legacy review list retains its generation refusal. General memory candidates can be accepted with review accept --kind memory into the current project store after current standalone screening; older correction/fail candidates still require profile-aware host integration for generation acceptance.

Real migration remains blocked until the separately reviewed generation-capable amem release and exact-pinned agent-hub integration are installed and their running stores prove the same home/G/selector revision. Synthetic cross-writes and exact prune readbacks precede separately confirmed real migration; known local handles must be quiesced/closed and reopened through the supported session-preserving lifecycle. Release, deployment, real migration, host evidence/capture receipts and real multi-machine sync are not established by these source changes. Deactivation omits generation-only notes/prunes from the v1 view and is not a lossless rollback.

Curated shard migration (#15 T2)

Bare amem migrate --shards is a preview; --apply writes the migration. amem migrate --shards --apply moves the curated single home into projects/<key-hash>/ and user/ through a resumable generation journal. The original single-home log bytes are never rewritten. A populated selected generation is supported and keeps its generation ID. Each shard keeps private legacy inputs in read-only legacy/, public-main inputs in read-only legacy-main/, and generation-born rows in its own log/ and local/. It has its own index.db, search.db and mutation lock; original IDs, hashes, placement and own/local/public provenance are preserved. The source generation bytes stay unchanged. Immutable prune proofs are validated before effects; a multi-shard prune is translated into one deterministic row per shard, with a receipt naming the original row. Existing capture and work/ directories are not migration inputs and are left untouched. catalog.db remains derived and rebuildable, now including curated entry counts and last use. Split curated stores sync the opened canonical project and user overlay to separate private-remote refs (amem/p/<key-hash> and amem/u) with amem sync --policy-free; unopened projects and machine data are not fetched. Original IDs and attribution survive independent generations. See docs/contracts.md, "Per-shard private sync", for prune transport and application boundaries.

Before splitting a live home, every installed host must open it through openShardStore({ project }) or the explicit user shard. Plain new MemoryStore() refuses any split marker, including an interrupted split, so it cannot silently keep writing the old layout. No host capability registry currently exists; migration preflight output states that limitation. Do not release T2 for agent-hub use until its memory factory is integrated with openShardStore and that host version is installed. Existing unannounced older builds remain the documented unchanged-source ceiling.

Use the same synthetic-home and separate real-data authorization boundary as generation migration:

AMEM_HOME=/path/to/synthetic-memory amem migrate --shards --dry-run
AMEM_HOME=/path/to/synthetic-memory amem migrate --shards --apply
AMEM_HOME=/path/to/synthetic-memory amem status
# Later old-layout appends are reported by status and carried in idempotently:
AMEM_HOME=/path/to/synthetic-memory amem migrate --resume

Every ordinary build that reads the single-home layout holds a cooperating lease in readers/<pid>-<start-signature>.json, renewed within 30 seconds and removed on close. Apply and resume refuse a live or unverifiable lease and remove only leases whose pid is confirmed gone. Removed lease filenames and times are recorded in the migration journal and status; registration also refuses while the profile lock is held. Published builds without leases cannot be detected: no process scan proves their absence, and their safety rests only on unchanged legacy bytes. Old-format writers may still append after the split; amem status reports those rows and resume carries them into the proper shards. A resume with pending work marks the split incomplete before copying; reads and writes through new or existing shard handles refuse until complete activation. amem status then reports the captured phase and pending rows, and migrate --resume finishes it idempotently.

Library callers open an already sealed shard directly with openShardStore({ home, project }) or openShardStore({ home, user: true }); these opens do not scan the legacy home or walk the catalog. A project handle reads its own shard plus the user overlay, merging lifecycle metadata before applying visibility; it never reads another project's private text. Shared notes and goals written through a project handle go to user/, including their lifecycle operations. Project entries accept only their own project key, while the user shard accepts shared entries. store.dir names the selected shard; store.profile.home remains the base memory directory. Library callers with a direct base path can use memoryDir instead of the historical parent home option. Two project shards have independent writer locks, and each incremental index equals its rebuild. bun scripts/measure-shard-ac8.ts builds 20 projects and 200,000 entries, poisons the other 19 indexes, and measures open plus ten searches on one shard against the R3 idle/peak RSS limits.

Dry run reports v1, generation-owned/local, prune and translated-prune counts, plus refused relations. A private-to-private replacement across projects refuses before effects. A shared entry superseded by a private entry also refuses: the dry run reports shared superseded by private: N and entry IDs only. Un-supersede it, or promote the replacement to shared, then rerun. ponytail: supporting that direction requires a body-free lifecycle witness; revisit when a read-only dry run on a copy of the person's home reports a nonzero count, never by probing or migrating the live home here. Private-to-shared replacements remain supported. See the T2 contract.

After a split, ordinary generation amem migrate modes (including --deactivate) refuse without changing the selector. Use amem migrate --shards --resume to finish the split. General memory candidates can be accepted with review accept --kind memory into the selected project shard after standalone screening; correction/fail candidates still refuse pending shard-aware host integration. Default prune selects the current project's shard; use amem prune --user --cutoff <UTC> --dry-run and amem prune --user --apply --plan <file> for shared/user entries. The current project, reader-admission and split-marker checks remain in force.

A native shard crash tail is left to its original writer's recovery. Migration checks planned copy tails and translation-receipt tails before changing activation; matching copies finish append-only and mismatches give an inspection hint. Dangling lifecycle/suggestion metadata is carried as ids-only user data and counted, so a target arriving later retains its intent. Cross-private replacements have their own ids-only count and the un-supersede/promote-to-shared remedy. During interruption the catalog keeps capture information available while curatedEntries is null and curatedAvailable is false; its cached capture counts may still lag.

Session MCP and work lists (#13 P2)

Run amem mcp as a stdio child of the agent CLI. It binds the working directory, canonical project key, exact memory home and selected profile once, accepts no project/home overrides in tool arguments, and closes all stores on stdin EOF. It has no listener, daemon, watcher or model call. MCP remains the scoped agent tool surface and its write tools remain candidate/work-list only. The CLI also accepts marked sessions, uses session identity for attribution, screens agent memory reads, and keeps command-specific confirmation and interactive-review fences.

MCP search, timeline and show remain available above 5,000 rows. Reads page exact-project candidates and screen them before result selection. Search ranks the newest 5,000 permitted matching candidates per tier; since narrows that window. Timeline can anchor an older row directly. Capture MCP indexing is lazy on queried reads, never hook construction or append. Index contention uses a call-pinned substring fallback; later searches retry. See MCP candidate reads for the bounded-window and fallback contract.

The MCP server exposes these tools. The nine-tool contract frozen for dotfiles#255 and maru#458 is the subset without amem_evidence, amem_brief and amem_correct.

12 tools, generated from MCP_TOOLS in src/mcp.ts:

Tool Read-only Purpose
amem_search yes Search screened memory in this project.
amem_evidence yes Return a bounded bundle of cited, live curated entries for a question.
amem_show yes Show a screened memory entry in this project by its exact id or unique prefix.
amem_timeline yes Read screened memory near an entry or time in this project.
amem_brief yes Read an intent/path-scoped brief of live public memory: fails first, then decisions/contracts and one-hop neighbours.
amem_graph yes Read the screened curated-memory neighbourhood.
amem_work_state_read yes Read project work lists; closed lists/items can be included explicitly.
amem_work_state_write no Append scalar project work-list fields; null clears, task selects an item, list status done closes.
amem_remember no Propose a memory candidate (text, optional kind/title), or kind=fail with approach, symptom, cause, evidenceRefs and retryWhen.
amem_correct no Propose a correction to an exact curated entry id visible in this project.
amem_vault_candidates yes List screened durable curated notes for review before moving them to a vault.
amem_capacity yes Read this machine's bounded capture capacity proposal; applying it requires the matching CLI preview and --confirm.

Search/show/timeline label capture as recall with evidence: false; capture never enters the curated graph. Policy screens complete rows before selection, limits or graph construction. The standalone secret screen, private flags, PII patterns and deny paths compose with a host's stricter policy through createMemoryMcp({cwd, base, policy}). Screening errors fail closed. base is the exact memory root; historical library home options still name its parent. Scope and profile changes refuse further access instead of silently reopening another store. Graph reads retain the 5,000-row curated ceiling; work-list reads stream the complete ordered history without an operation-count ceiling, applying policy before latest-value reduction. The working state scales with the current result, not historical payloads. The graph ceiling is an operational bound, not an exposed count of unscreened data. Graphs cover curated-note relationships, not goal/suggestion tables.

WorkListStore writes projects/<key-hash>/work/ with its own append log, derived index and lock. Scalar fields reduce by latest value, null clears, list status done closes, and item status is todo|doing|done|dropped. Work lists are outside generation selection and capture eviction. amem import claude-mem additionally imports work_state_entries only through persisted D2 aliases, preserving source DB bytes and stable replay receipts; JSON keeps observation fields and adds work counts/cursor. --capture-days N also imports observations and session summaries from the last N days into machine-local capture under the same aliases, with read/added/spooled/skipped counts, stable replay receipts and the configured capture cap and retention. Widening N after an earlier import requires --restart to revisit rows below the saved cursor. Chroma vectors are never read.

amem vault-candidates --json and the MCP read tool select durable long-term notes under current policy, excluding task/review/PR/CI progress and already-linked entries. The deterministic status filter covers English status phrases and can miss paraphrased or other-language status text; candidates require explicit CLI review under current policy. No vault is opened, indexed or written. Library recordVaultLink records an exact-entry hash and relative Markdown path in a generation-only local append op; it survives index rebuild and shard migration, stays machine-local, and does not change shared-v1 schemas or sync semantics. There is no MCP link writer.

Before recording vault links, every host sharing the memory home must exact-pin a P2 vault-link-capable version (0.4.0 or later). Recheck that capability when changing a host pin; an ordinary successful list is not rollout readiness, because an unsupported reader can refuse the generation on refresh or rebuild. See the vault-link compatibility contract.

General memory candidates are separate from curated evidence. amem review accept <id> --kind memory --policy-free is available through the CLI with actor and peer derived from the session; it rechecks the current standalone privacy policy before appending the accepted note. Machine capacity reports numeric aggregate use across shards, as R7 requires; it does not return foreign project names or text.

bun scripts/measure-mcp-budget.ts exercises an actual stdio session with 5,001 curated notes, 5,001 capture rows and 10,001 work-list operations reduced into 100 items. It checks the R3 idle RSS limit of 100 MB, peak RSS limit of 200 MB across searches, latest/anchored timelines and work-list reads, and child reaping after EOF. CI runs the harness on macOS and Linux. All harness data is synthetic; live P6 measurements remain separate.

Development

bun install --frozen-lockfile
bun x tsc --noEmit
bun scripts/check-boundary.ts
bun scripts/readme-facts.ts     # README blocks between <!-- facts:<name> --> markers; --write regenerates them
bun scripts/check-docs.ts     # review stamps in docs/verified.json; stale documents are warnings
bun test
npm pack --dry-run

CI runs the typecheck, boundary check, README facts and documentation stamp checks and tests on Ubuntu and macOS with Bun 1.4.2. Tests use synthetic fixtures and isolated homes, not personal memory stores.

Publishing

0.1.0 was published once by hand, because npm trusted publishing requires an existing package; the trusted publisher for repository STAIxBWLB/agent-mem and workflow release.yml was configured after it. Every later release goes through the tag workflow.

After setup, a v* tag first proves that the identical whole Git tree already has a successful same-repository check run with both OS gates; it does not rerun the test shards. Matching active CI is awaited within 25 minutes. The release then runs its two-OS version, main-ancestry, CHANGELOG, manifest, typecheck, boundary, README-facts, documentation-freshness and pack gates. The tag must equal v plus package.json's version. Only after both gates pass does the release job install npm 11.20.0 (trusted publishing needs 11.5.1 or later) and run npm publish --access public, authenticated through GitHub OIDC with id-token: write; npm adds provenance itself. The workflow uses no NPM_TOKEN. Release checkouts pin the proven commit, and the tag object is rechecked before publication and Release edits. Manual release dispatch is proof-only and cannot publish; the release reuse contract states the last-read ceiling. The gates also refuse a stale documentation stamp (bun scripts/check-docs.ts --require-fresh). After the GitHub Release, a separate job sends a repository_dispatch to STAIxBWLB/agent-hub-site so its portal re-syncs; it is skipped without the SITE_DISPATCH_TOKEN secret and its failure does not fail the release.

License: MIT.

Intent recall and history

Use amem brief --for "repair the writer" --paths src/writer.ts for a byte-capped, fail-first brief. Repeat --paths or --symbols for explicit scope; --bytes 2048 sets the complete brief-body cap. Agents use the project-bound amem_brief MCP tool. Memory is untrusted data, not instructions.

Current recall omits superseded and expired entries. amem show <id> --history shows retained versions, amem graph <id> --history follows retained history, and amem search writer --as-of 2026-10-20 --history reads retained state at UTC midnight. Current forget/prune removals still hide the text at every past date. Promotion event time is not serialized, so affected history reports promotion time unknown: tier before promotion cannot be recovered. (stale?) is only a bounded path-metadata hint. See docs/contracts.md for retention and history limits.

The library exports buildIntentBrief, planGuardNodes, matchingPlanFails and nullable usage callbacks. Emitted output may record rendered counts; delivered and cited counts need explicit host observations. Missing evidence stays unknown. Agent-hub adoption is a separate follow-up after a published library release.

Consolidation and evidence

Run amem consolidate [--project <current-key>] (another project is refused) to queue deterministic proposals for amem review. It compares a bounded project-local window using indexed text overlap, shared paths and current supersession-chain metadata. It does not edit curated notes. The CLI reviews each merge, replacement or recurring-failure summary under the current caller identity, and acceptance rechecks every source revision. A recurring-failure summary preserves its source failures and decisions. Large source quotations are counted as too large instead of silently shortened into a proposed note.

The CLI runs the deterministic pass only. A library host may explicitly enable an injected model callback for the person; no provider is built in. It has batch, byte, deadline and persistent daily limits. Model answers remain marked machine-local candidates that cannot be accepted; amem note add is the separate path for an independently authored note. A non-answer is never saved.

Agents use amem_evidence with a question to get cited, live curated entries, not an answer. The complete JSON bundle is byte-capped (maxBytes, default 4096, range 256..8192); excerpted entries retain an exact mem: reference for fetching the full note. Capture, repository-only rows, pending candidates and model answers are excluded. amem makes no model call for this tool. Rendering a citation in the bundle records only rendered after successful output, never delivered or cited.

Public prune view and v2 sync (opt-in)

On an active curated split, the person can preview and enable the forward-only v2 reader floor. This keeps the receiving generation and native logs/seals, builds a separate index-v2.db, and preserves every native/local mask. Once activated, explicit sync uses the v2 transport and can install complete generation-neutral public-prune proof; historical hold receipts alone never authorize a mask.

AMEM_HOME=/path/to/synthetic-memory amem migrate --public-prune-view --dry-run
AMEM_HOME=/path/to/synthetic-memory amem migrate --public-prune-view --apply
# Finish an interrupted activation, using its exact recorded forward state.
AMEM_HOME=/path/to/synthetic-memory amem migrate --public-prune-view --resume

Dry-run writes nothing and reports mask building as deferred. Close cooperating reader handles for initial v1-to-v2 activation or its resume, and install compatible hosts first: older readers reject the new markers and transport tip. Initial activation preserves all native/local masks and currently performs ten old/new comparison rebuilds per realm across five verification passes, plus snapshot/index work. Deactivation is refused because v1 would omit foreign masks. Unannounced historical readers can still inspect unchanged old bytes; this is logical masking, not erasure.

The first explicit v2 amem sync primes a private create-once publisher boundary for each selected shard/current machine. Only complete own prune rows appended after that boundary can receive new post-seal lineage; all earlier rows and unsupported historical lineage stay ineligible. Sync never infers a native-generation claim or rewrites native seals/rows. Both machines need compatible v2 readers and explicit sync. Stable exact project/user refs make a one-way v1-to-v2 history transition; returning a ref to v1 is refused.

Sync validates all selected history and complete raw Git proof, and checks each realm's aggregate input/archive capacity before atomic publication. It then fsyncs immutable inputs and commits the root map before ordinary/native imports, preventing an unmasked first import. Already-v2 map advances preserve generation/format and monotone ancestry, use a forward-only intent and rebind existing readers without requiring them to close. A reader waits at most 3000 ms for a v2 writer; an unresolved intent, incompatible marker pair or missing committed proof reports unavailable. The same --resume command completes an interrupted advance forward. Read and capture never activate, prime a boundary or repair a pending operation.

SyncResult.pruneApplication reports transport durability, complete-proof count, neutral projection activity, native application, held count and phase separately. Transport durability alone does not establish completed local application. Historical multi-realm translations and older unsupported lineage retain "held, not applied" warnings, so agent-mem#15 AC4 remains open. The full contract states the owned-source integrity and resource ceilings; this source behavior does not establish release or live-host verification.

Claude transcript summaries

At Stop and SessionEnd, amem also reads up to 256 KiB of new bytes from Claude Code's transcript_path. It captures native session titles (Title), away recaps (Recap, without the config suffix), and compaction summaries (Compact). The existing policy, secret, PII, deny-path and private-span screens and excerpt caps apply. Lines without a native summary marker or amem: are skipped before JSON parsing. No network, model call or resident process is added.

Private cursors under hook-transcripts/ store only a session hash, file identity, byte offset, timestamp carry, untimed-content hashes with fallback times, and the last title hash. Repeated titles are skipped; deterministic spool receipts prevent duplicate replay after rotation/truncation. New summary events use hook-v3- spool names (format 3), which 0.18.0 and earlier hooks do not match. Byte, time or spool limits leave remaining lines for a later Stop/SessionEnd invocation. A single line larger than the 256 KiB ceiling is skipped and counted as transcript_line_bound rather than buffered. An unreadable or busy transcript leaves ordinary hook capture available.

The follow-up bound is a benchmark parameter, not a production retry loop. Backlogs advance on later Stop/SessionEnd calls of the same session. Anything unread after its last hook remains uncaptured, so a cold install on a long session keeps only what its remaining hooks can scan; committed rows are not lost.

Search gives these events the existing 1.25 conversation weight and session grouping. After eligible Records, the brief selects up to two newest screened main-session Recap/Compact rows, then fills from the existing chronological rows under the same record, byte and token caps. With no Records or summaries the output is unchanged. See verification evidence.

Assistant records (#313)

Native SessionStart (startup and compact) preloads and screens the capture cache before work can spend the deadline, reserving only the newest permitted main-session Recap/Compact line. Work consumes the remaining pool. At the existing capture output position, up to five whole canonical Records use the then-remaining bytes; the reserved newest summary follows Records, and a second summary may use only bytes left after Records. The library brief follows the same reservation and output order. A line that cannot fit whole after the header and Record instruction is skipped and marks capture partial; budget allocation never truncates a display line or Record. The library and CLI still query capture directly and retain chronological raw-row fill after the preferred rows.

Stop/SessionEnd Record/summary commits and hook-spool replay prepare brief-capture.json (0600) in the project's machine directory. It preserves up to five canonical Records before up to two screened main-session summaries, including the current session, with their display values and the committed capture row watermark. Ordinary raw recent captures are excluded. Record commits refresh the cache too. The native read is one byte-capped file read, re-screens full source values under current policy, and never opens a capture database or rebuilds the cache. Missing or unreadable caches report capture: capture cache missing; total context bytes and native deadlines are unchanged. A busy snapshot can leave the cache missing until a later Record or summary commit. Refs #337.

Since 0.23.0, native Claude SessionStart can prepend the package-owned RECORD_INSTRUCTION before the untrusted memory header, including on resume. Its three lines occupy 331 UTF-8 bytes, or 332 with the separator, reserved from the existing 1,200-byte context budget. The metric reports the actual emitted recordInstructionBytes and recordInstructionEstimatedTokens (83 when emitted, an approximation). The full block is omitted when it cannot fit; it never turns a failed or timed-out hook into valid context. Set records: false in the home's amem.config.json or the repository's .amem/config to disable the instruction and new extraction; false wins across both files.

For the #313 AC6 comparison week (#325), the person can create a private record-instruction.json in the memory home containing {"instruction":"alternate"} (a, b and alternate are the only values). This is separate from amem.config.json, whose records key remains boolean-only. The file must be regular, single-link, owned by the current uid and not group- or other-writable (mode & 022 == 0): normal 0644 is accepted; 0664 and 0666 are rejected. Core never writes the selection file. Missing means A; an invalid, unreadable, group/other-writable, oversized, foreign-owned or linked file also means A, with numeric recordInstructionInvalid: 1 in the hook metric, without refusing the hook. Existing 0.23.x readers ignore this file and retain A. B uses purpose framing with a basis: hint and ? for unverified text. Both reserve 332 bytes before the untrusted header; A emits 332 bytes including the separator, B emits 331 (about 83 estimated tokens each). Alternate uses even/odd parity of the last hex digit of the existing 16-character capture session hash for A/B, so resume keeps the same variant without exposing a raw session id. A missing or invalid session id falls back to A rather than guessing parity; metrics record that resolved A choice. records: false in either configuration file still wins. RECORD_INSTRUCTION remains the exact A text; RECORD_INSTRUCTION_B, RECORD_INSTRUCTIONS, readRecordInstruction, captureSessionKey, recordInstructionVariant and their mode/variant types let hosts reuse the package-owned text and selection rule. basis: and ? remain ordinary Record text, with no grammar or capture-field change. The person compares the variants during the live week before deciding the default. Record variant measurements records the public fixture protocol and its evidence status.

The live-week counters use a separate record-variants.jsonl beside the existing resource metrics. Each row contains at, the hook event, resolved variant (a or b) and allowlisted numeric Record fields; no Record text is written. Observations use single short O_APPEND writes and 0600 creation, retaining eight days within a 4 MiB hard cap. Capture rows, spool events/status values and the general resource metrics retention are unchanged. One session is one SessionStart with source startup or resume, including an omitted instruction; recordInstructionEmitted and recordEnabled distinguish exposure and opt-out. One turn is one Stop invocation. Stop/SessionEnd report eligible parsed Record counts, the number containing basis: and the number starting with ?; these are parsing observations, not durable-publication counts. SessionEnd contributes Records but no Stop-turn denominator. An unavailable scan leaves counts unknown, and the report never converts missing counters into zero. SessionStart reports actual emitted brief bytes including the instruction. Capacity trims leave 1 MiB of headroom (4 MiB trigger, 3 MiB retained). Incomplete or unknown transcript scans are excluded from exact Stop-turn observations; the full-window exact share stays null until coverage is complete.

Run bun scripts/report-record-variants.ts --input /path/to/memory/record-variants.jsonl read-only to report sessions, Records/session, the share of Stop turns with a Record, basis and uncertainty shares, and mean brief bytes with explicit denominators. Missing Stop scan counters produce an incomplete/null exact turn share and a separate observed share. The bounded first-line probe triggers an age rewrite only past nine days, keeping eight days (daily hysteresis). Ordinary appends never read the tail or rewrite it. When the 4 MiB cap forces older observations out, numeric capTrimmed markers make window.capForcedTrim true and coverage incomplete; do not treat a capped window as a complete AC6 week. The general metrics file retains its existing 64 KiB trigger and newest-200 discipline.

At Stop/SessionEnd, complete native assistant text can yield Record capture events through the existing transcript scan. parseRecordLines, RECORD_INSTRUCTION and recordsEnabled(base, cwd?) are public root exports; hosts check recordsEnabled before emitting the instruction or capturing parsed Records. The canonical grammar is maintained in src/hook-record.ts. The parser checks format and explicit text exclusions; hosts still apply their capture privacy policy before storage. It accepts the five kinds decision/fix/fail/contract/finding, repository-relative anchors and at most 300 text characters, excludes fenced/quoted examples, and requires the fix/fail relation. Missing native row time or stable message.id refuses extraction. At most two syntactically valid record lines are considered per stable message per scan call, with no persisted count. Whole records are declined on a denied anchor, policy refusal, truncation or redaction; no partial record is saved.

Records stay machine-local capture, not curated evidence or review candidates. They reuse Title/Recap/Compact staging, deterministic receipts and replay, with canonical line content and the native row timestamp; no Record-only receipt or capture-row schema is added. Source-row privacy flags are screened before capture, and replay refuses to clip or remove anchors from a pending Record when caps change. A slow replay opportunity cannot strand the second Record in the same transcript row. A crash after staging but before cursor save retains the existing duplicate window. A message-ID/ordinal receipt is a future upgrade if the AC6 duplicate experiment requires it. Record-to-candidate conversion and other ingest/Jcode paths remain outside this change; AC6 and real-home rollout are person-run checks.

The brief puts up to five recent, screened main-session Records bound to this project before summaries and raw capture lines, using the same remaining byte budget. Repository-relative anchors must match the stored paths; - is valid only for project-bound decision/contract Records. Subagent or malformed Records stay out. Large legacy stores read only the newest 128 rows until an off-hook search prepares the separate Record partial index; hooks do not build it above 128 rows. Search weights Record at 1.5 versus 1.25 for conversational capture without changing curated slots or session grouping. MCP preserves the canonical line and paths, and the existing all-project CLI capture display keeps the complete Record line and anchors.

Before relying on Record capture, upgrade every amem sharing the home or have the person append the existing home reader floor at 0.23.0. An old 0.22.1 hook drain silently discards a Record format-3 spool record without a counted loss, and its config parser refuses records as an unknown key, even when false. Set records: false only after every participating install is upgraded. Core never writes formats.json; a floor prevents old participation rather than teaching old readers the event.

Initial sync placement activation

Available from 0.22.0. 0.21.0 contains the placement protocol but not this command.

A person can activate per-row outbound placement on one whole memory home. It is forward-only, and never runs migrations, changes privacy policy or raises a reader floor automatically. Real-home activation still needs the person's separate authorization.

  1. Preview and complete the existing shard and public-prune migrations with amem migrate --shards --dry-run and amem migrate --public-prune-view --dry-run (then their separately reviewed --apply steps).
  2. On a 0.22 installation, have the person append the existing home floor at 0.22.0 in formats.json, preserving prior records, and close cooperating memory reader handles. This is recommended at activation and required before the first writer renewal, so old 0.21 readers refuse before opening a renewable home.
  3. Declare every local amem installation that writes this home, including an agent-hub library installation even if setup has no receipt for it. The CLI and current setup-receipt roots are mandatory members.
amem migrate --sync-placement --dry-run --writer-root '<cli-install-root>' --writer-root '<host-amem-install-root>' --json
amem migrate --sync-placement --apply --writer-root '<cli-install-root>' --writer-root '<host-amem-install-root>' --json
# Only for an interrupted initial activation, using its retained frozen inputs:
amem migrate --sync-placement --resume --json
# If inputs changed and BOTH marker files still have their original v2 bytes:
amem migrate --sync-placement --abandon --json

Dry-run shows root versions/pins and safe policy counts, never row text or patterns, and writes nothing. Initial writer errors name the root category and cause, with upgrade/setup instructions. If a pre-marker interruption retained an intent and its inputs changed, person --abandon removes only its owned initial metadata so a fresh preview/apply can follow. It cannot remove a v3 marker, active fence or journal history. After apply, choose the private remote on the first v3 sync (amem sync --remote <private-git-url>); ordinary and cadence sync then read the immutable root locator and measure the installs afresh. Allowed originals can publish while declined originals remain in the local append-only history, with counts in the sync receipt. No writer roots need to be repeated on sync.

On an active home, the same dry-run/apply commands explicitly renew the writer inventory on its existing epoch (#282). Declare the complete new 0.22.0-or-later roots after updating host pins and managed setup receipts. Ordinary and cadence sync then locate the new inventory and measure it afresh; original activation data, holds and prior receipts are unchanged. Pending publication or source-recovery work must first be finished with the inventoried install, for example after dot ai rollback amem or restoring the host's previous pin.

An interrupted renewal resumes only its exact intent. Before commit append starts, person --abandon can discard its unpublished metadata; after the first commit-log byte, only forward resume is supported. Unpublished intent staging requires explicit --abandon, then a fresh dry-run; it is never cleaned automatically. Compatible ordinary reads remain available while only renewal metadata is pending. After a crash, a retained profile.lock still blocks new reader admission until person --resume or, before commit append, --abandon. Never edit activation files, downgrade markers or start another epoch to discard a hold. See the renewal contract. Real-home renewal and release remain separately authorized person operations.

About

amem: screened, local memory for coding agents. Curated notes, a separate capture tier, ranked recall, an MCP server and opt-in git sync. Part of the ahub family.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages