@staix/agent-mem
v0.14.0
Published
Curated, shared memory for coding agents: append-only logs, a derived index, tiers and a typed graph.
Readme
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.14.0. 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 --applyReplace claude with codex, kimi, pi, qwen or jcode 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_IDReplace 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
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 only, 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. No Jcode ingest command or capture hook is installed: 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 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 and Qwen 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. 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 and Qwen 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.
For a Claude Code plugin installation instead of direct setup registration:
claude plugin marketplace add STAIxBWLB/agent-mem
claude plugin install agent-mem@agent-memThe 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).
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.briefBytes,excerptBytes,capturePathscan only lower the built-in ceilings (the 1200-byte SessionStart context, 2000-character capture excerpt, 32 captured paths).capture.capMbandcapture.retentionDaysare the lowest layer of the capture size and retention bounds: a shard's ownconfig.jsonand the home'scapture-config.jsonstill 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 and capturePaths). 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.
amem sync reads this machine's own long-term notes through the configured policy before it stages anything. One declined note refuses the whole sync and names its id (never its text); the log is append-only and sync never skips a row. It does not read other machines' segments, which were screened where they were written.
--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.
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> --jsonReview 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. Listing does not open the ordinary memory index, and acceptance defers unrelated held-note promotions.
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. Library APIs are available, but this phase adds no automatic candidate producer or telemetry consumer. 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. This package registers no capture observer or host candidate tools; capture exclusion and authenticated producers must be established by the host 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.
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/_amemEnable 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
compinitStart 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/amemEnable it by adding this line to ~/.bashrc:
source ~/.local/share/bash-completion/completions/amemSource 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.fishFish 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
amemand press Tab: top-level commands appear. - Type
amem goaland press Tab: goal subcommands appear. - Type
amem list --tierand press Tab:shortandlongappear. - Type
amem goal state <id>and press Tab: goal states appear. - Type
amem import claude-mem --dband press Tab in a directory containing a filename with spaces: the path remains one argument. - Type
amem completionand press Tab:zsh,bash, andfishappear.
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; eviction removes the oldest capture rows first in bounded batches. The 2026-10-17 owner decision keeps the P1 cap per shard; the machine-wide total is #15 T5. 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/ (an unreleased layout of this branch) is migrated per project on open and removed once empty.
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 are #15 T5 and not built.
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 askThe 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 previewarchive,unarchiveandforgetare append-only ops inlifecycle.jsonlof the memory home (local, never synced, likealiases.jsonl): nothing rewrites a line and nothing deletes a shard. The latest op wins andforgottenis 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;unarchiverestores an archived one. A forgotten project's bytes stay on disk:forgetis an append-only lifecycle record, not an erase. An alias shard of an archived or forgotten project counts as that state,alias addto 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 asproject_archivedorproject_forgotten).openShardStorerefuses such a project at the open, so a library caller such as agent-hub is gated too once it pins this release.gcopens only the capture store of each shard and calls its boundedevict()(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 saysremainingOverCapand 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.exportchanges 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 curatedindex.dbandsearch.db, SQLite sidecars andcatalog.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 inwithheld.search --all-projectsruns 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 ownmemorycommand) refuses the flag.capacityalso 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 andcapacitysays why (splitRefused). It proposes capture bounds only, and never evicts curated notes, goals or work lists.capacity apply --splitpreviews, then--confirmwrites exactly the previewed values to each shard'sconfig.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-sharegiven only to--confirmchanges 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 peak RSS (R2), the search path under 100 MB idle and 200 MB peak RSS (R3), and no amem process may remain after exit (R1). 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-memUse 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 context starts with an untrusted-data warning and per-section complete, partial or unavailable coverage, followed by JSON records. 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.
A valid pending body is eligible for retirement only after at least seven days and current evidence that its scope is unknown/mismatched or its original cwd is positively absent. A transiently removed or unmounted cwd younger than seven days keeps its data. A positively drainable foreign-project record is retained even when older than seven days; another project's admission does not make it undrainable. Retirement removes the body only after persisting a reason/count metric (captureDrops and captureDropReason, with cwd_removed, undrainable_expired or scope_unknown); failed counter persistence leaves the body pending. No retired body becomes a replay/context source. Malformed publications can be removed through bounded owned-file checks. Explicit current-policy rejection also retains only a numeric private receipt. This bounded loss policy prevents permanently undrainable records from disabling capture, while never rebinding them to a new project.
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 initial untrusted-data header inside additionalContext, not in new top-level fields. These are rolling-window counts, not all-time totals. The header stays first even when the brief is otherwise unavailable, and the combined context stays within its 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 and defers unsafe curated refresh with a fixed coverage reason. Native capture reads the newest 128 rows through an existing read-only time index and marks excluded older history partial, instead of rejecting a larger store. It never 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, reader-lease and capture 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, retirement or replay. 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 and 100 MB summed main/helper peak RSS, with a 140 ms handler budget. 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.
Doctor and the sync cadence
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 --jsonDiscovery 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 --deactivateMigrate 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 attr
