@staix/agent-mem 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +70 -44
- package/package.json +5 -2
- package/src/cli-agent-reads.ts +105 -0
- package/src/cli.ts +219 -56
- package/src/commands-table.ts +10 -9
- package/src/index.ts +1 -0
- package/src/ingest.ts +21 -4
- package/src/mcp.ts +8 -8
- package/src/memory/candidates.ts +15 -13
- package/src/memory/capacity-split.ts +4 -1
- package/src/memory/capacity.ts +48 -4
- package/src/memory/catalog.ts +4 -4
- package/src/memory/cli-attribution.ts +23 -0
- package/src/memory/commands.ts +63 -11
- package/src/memory/consolidate.ts +3 -2
- package/src/memory/decay-review.ts +4 -4
- package/src/memory/doctor.ts +36 -7
- package/src/memory/generation-index.ts +56 -11
- package/src/memory/generation.ts +73 -7
- package/src/memory/metrics.ts +20 -8
- package/src/memory/migrate.ts +31 -0
- package/src/memory/mutation.ts +24 -4
- package/src/memory/note-add.ts +4 -4
- package/src/memory/prune-publisher-lineage.ts +139 -0
- package/src/memory/prune.ts +28 -1
- package/src/memory/public-prune-activation.ts +260 -0
- package/src/memory/public-prune-advance.ts +149 -0
- package/src/memory/public-prune-input-chain.ts +73 -0
- package/src/memory/public-prune-inputs.ts +455 -0
- package/src/memory/public-prune-view.ts +139 -0
- package/src/memory/public-prune-witness.ts +168 -0
- package/src/memory/reader-lease.ts +46 -34
- package/src/memory/shard-migration.ts +42 -3
- package/src/memory/shard-sync-format.ts +40 -2
- package/src/memory/shard-sync-graph.ts +87 -0
- package/src/memory/shard-sync.ts +341 -53
- package/src/memory/shard.ts +50 -9
- package/src/memory/store.ts +51 -12
- package/src/memory/style.ts +3 -0
- package/src/memory/sync.ts +141 -5
- package/src/memory/vault.ts +31 -0
- package/src/setup.ts +59 -31
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
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](https://github.com/STAIxBWLB/agent-hub), with compatible storage and entry IDs.
|
|
4
4
|
|
|
5
|
-
Version: 0.
|
|
5
|
+
Version: 0.14.0. Requires Bun 1.4.2 or later; no runtime dependencies.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
|
-
Install the package, preview setup, then apply it for your client
|
|
9
|
+
Install the package, preview setup, then apply it for your client:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
bun add -g @staix/agent-mem
|
|
@@ -15,16 +15,16 @@ amem setup all
|
|
|
15
15
|
amem setup claude --apply
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Replace `claude` with `codex`, `kimi`, `pi`, `qwen` or `jcode` for that client.
|
|
18
|
+
Replace `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.
|
|
19
19
|
|
|
20
|
-
Agents submit pending memory candidates through MCP.
|
|
20
|
+
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:
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
23
|
amem review list
|
|
24
24
|
amem review accept CANDIDATE_ID # or: amem review reject CANDIDATE_ID
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Replace `CANDIDATE_ID` with an id from the list.
|
|
27
|
+
Replace `CANDIDATE_ID` with an id from the list. MCP remains proposal-only: it never accepts a candidate or writes a curated note.
|
|
28
28
|
|
|
29
29
|
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](#setup--plugin) describes the registration and native trust steps.
|
|
30
30
|
|
|
@@ -49,9 +49,9 @@ amem setup pi --apply
|
|
|
49
49
|
amem setup all --undo
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`--apply` and `--undo` require a
|
|
52
|
+
`--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.
|
|
53
53
|
|
|
54
|
-
Claude receives its seven bounded hooks. Codex and
|
|
54
|
+
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](https://learn.chatgpt.com/docs/hooks) 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.
|
|
55
55
|
|
|
56
56
|
For a Claude Code plugin installation instead of direct setup registration:
|
|
57
57
|
|
|
@@ -78,13 +78,13 @@ amem goal new "Ship a release" --milestone "Review changes"
|
|
|
78
78
|
|
|
79
79
|
`amem goal link` needs a host that has a task board; amem alone has none, so it refuses.
|
|
80
80
|
|
|
81
|
-
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
|
|
81
|
+
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).
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
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.
|
|
84
84
|
|
|
85
85
|
## Configuration and privacy policy
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
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.
|
|
88
88
|
|
|
89
89
|
The machine-local file `amem.config.json` in the memory home (see `amem status` for where that is) holds your own policy:
|
|
90
90
|
|
|
@@ -112,9 +112,9 @@ A repository may add `.amem/config` (same shape, only `denyPaths`, `piiPatterns`
|
|
|
112
112
|
|
|
113
113
|
## Writing notes and reviewing candidates
|
|
114
114
|
|
|
115
|
-
`amem note add [--kind decision|finding|contract|fail] [--tier short|long] [--scope project|shared] [--title T] [text... | -]` writes one curated note
|
|
115
|
+
`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.
|
|
116
116
|
|
|
117
|
-
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
|
|
117
|
+
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.
|
|
118
118
|
|
|
119
119
|
## Candidate review
|
|
120
120
|
|
|
@@ -127,13 +127,13 @@ amem review accept <full-candidate-id> --policy-free --json
|
|
|
127
127
|
amem review reject <full-candidate-id> --json
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
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. Listing does not open the ordinary memory index, and acceptance defers unrelated held-note promotions.
|
|
131
131
|
|
|
132
132
|
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.
|
|
133
133
|
|
|
134
|
-
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
|
|
134
|
+
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.
|
|
135
135
|
|
|
136
|
-
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
|
|
136
|
+
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).
|
|
137
137
|
|
|
138
138
|
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.
|
|
139
139
|
|
|
@@ -141,7 +141,7 @@ Acceptance normalizes its note once, including timestamp, content ID and short-t
|
|
|
141
141
|
|
|
142
142
|
## Shell completion
|
|
143
143
|
|
|
144
|
-
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
|
|
144
|
+
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.
|
|
145
145
|
|
|
146
146
|
### zsh
|
|
147
147
|
|
|
@@ -205,7 +205,7 @@ The JSONL segments are authoritative. `index.db` is derived and can be rebuilt.
|
|
|
205
205
|
|
|
206
206
|
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.
|
|
207
207
|
|
|
208
|
-
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;
|
|
208
|
+
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.
|
|
209
209
|
|
|
210
210
|
`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`).
|
|
211
211
|
|
|
@@ -215,7 +215,7 @@ Project identity (#15 D2) keeps `memoryProjectKey`: a worktree, a submodule and
|
|
|
215
215
|
|
|
216
216
|
External retrieval measurements for MCP, library search and a plain BM25 baseline are recorded in [LongMemEval S/M](docs/bench/longmemeval.md). The full datasets stay outside the repository; CI runs only synthetic harness fixtures.
|
|
217
217
|
|
|
218
|
-
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.
|
|
218
|
+
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.
|
|
219
219
|
|
|
220
220
|
## Repo scope (#15 T4)
|
|
221
221
|
|
|
@@ -229,11 +229,11 @@ amem repo status # the file, its entries, wh
|
|
|
229
229
|
amem repo attributes [--confirm] # add `.amem/log.jsonl merge=union` to .gitattributes, only when you ask
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
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.
|
|
233
233
|
|
|
234
234
|
## Managing the project shards (#15 T5)
|
|
235
235
|
|
|
236
|
-
One catalog view across every project shard of the memory home
|
|
236
|
+
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:
|
|
237
237
|
|
|
238
238
|
```sh
|
|
239
239
|
amem projects [--all] [--json] # key, state, aliases, capture and curated sizes, disk use; forgotten ones only with --all
|
|
@@ -247,7 +247,7 @@ amem vault-candidates [--all-projects | --project <key>]
|
|
|
247
247
|
amem capacity apply --split [--confirm] # each shard's share of the global capture budget, written to its own config after a preview
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
-
- `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
|
|
250
|
+
- `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.
|
|
251
251
|
- `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.
|
|
252
252
|
- `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`.
|
|
253
253
|
- `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.
|
|
@@ -290,28 +290,35 @@ Successful native project identities have a private, bounded derived cache (128
|
|
|
290
290
|
|
|
291
291
|
## Doctor and the sync cadence
|
|
292
292
|
|
|
293
|
-
`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).
|
|
293
|
+
`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.
|
|
294
294
|
|
|
295
|
-
`amem
|
|
295
|
+
**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.
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
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`.
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
`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.
|
|
300
|
+
|
|
301
|
+
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.
|
|
302
|
+
|
|
303
|
+
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](docs/contracts.md#per-shard-private-sync-15-t3)).
|
|
300
304
|
|
|
301
305
|
## Native transcript intake
|
|
302
306
|
|
|
303
|
-
`amem ingest` reads existing Codex, Kimi or
|
|
307
|
+
`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:
|
|
304
308
|
|
|
305
309
|
```sh
|
|
306
310
|
amem ingest codex --since 2026-10-01T00:00:00Z --json
|
|
307
311
|
amem ingest kimi --json
|
|
308
312
|
amem ingest pi --since 2026-10-01T00:00:00Z --json
|
|
313
|
+
amem ingest qwen --json
|
|
309
314
|
```
|
|
310
315
|
|
|
311
316
|
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.
|
|
312
317
|
|
|
313
318
|
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](docs/qwen-code.md) for source pins, cursor upgrade behavior and limitations.
|
|
314
319
|
|
|
320
|
+
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).
|
|
321
|
+
|
|
315
322
|
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.
|
|
316
323
|
|
|
317
324
|
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.
|
|
@@ -347,51 +354,51 @@ See [the contract and coverage tables](https://github.com/STAIxBWLB/agent-mem/bl
|
|
|
347
354
|
|
|
348
355
|
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](https://github.com/STAIxBWLB/agent-mem/issues/9)); until then a running hub keeps writing shared v1 and the person's notes would split.
|
|
349
356
|
|
|
350
|
-
Shared v1 remains the default until
|
|
357
|
+
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.
|
|
351
358
|
|
|
352
359
|
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.
|
|
353
360
|
|
|
354
361
|
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.
|
|
355
362
|
|
|
356
|
-
The following interfaces
|
|
363
|
+
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:
|
|
357
364
|
|
|
358
365
|
```sh
|
|
359
366
|
AMEM_HOME=/path/to/synthetic-memory amem migrate --dry-run
|
|
360
|
-
AMEM_HOME=/path/to/synthetic-memory amem migrate
|
|
367
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --apply
|
|
361
368
|
# If interrupted, resume the recorded generation rather than creating another:
|
|
362
369
|
AMEM_HOME=/path/to/synthetic-memory amem migrate --resume
|
|
363
370
|
umask 077
|
|
364
|
-
AMEM_HOME=/path/to/synthetic-memory amem prune --cutoff 2026-10-01T00:00:00.000Z --dry-run > prune-plan.json
|
|
371
|
+
AMEM_HOME=/path/to/synthetic-memory amem prune --cutoff 2026-10-01T00:00:00.000Z --dry-run --json > prune-plan.json
|
|
365
372
|
AMEM_HOME=/path/to/synthetic-memory amem prune --apply --plan prune-plan.json
|
|
366
373
|
# Explicitly select shared v1 again while preserving both histories:
|
|
367
374
|
AMEM_HOME=/path/to/synthetic-memory amem migrate --deactivate
|
|
368
375
|
```
|
|
369
376
|
|
|
370
|
-
|
|
377
|
+
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.
|
|
371
378
|
|
|
372
379
|
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.
|
|
373
380
|
|
|
374
|
-
A prune plan is a
|
|
381
|
+
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.
|
|
375
382
|
|
|
376
383
|
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.
|
|
377
384
|
|
|
378
385
|
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.
|
|
379
386
|
|
|
380
|
-
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
|
|
387
|
+
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.
|
|
381
388
|
|
|
382
389
|
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.
|
|
383
390
|
|
|
384
391
|
## Curated shard migration (#15 T2)
|
|
385
392
|
|
|
386
|
-
`amem migrate --shards` 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.
|
|
393
|
+
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.
|
|
387
394
|
|
|
388
395
|
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.
|
|
389
396
|
|
|
390
|
-
Use the same synthetic-home and
|
|
397
|
+
Use the same synthetic-home and separate real-data authorization boundary as generation migration:
|
|
391
398
|
|
|
392
399
|
```sh
|
|
393
400
|
AMEM_HOME=/path/to/synthetic-memory amem migrate --shards --dry-run
|
|
394
|
-
AMEM_HOME=/path/to/synthetic-memory amem migrate --shards
|
|
401
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --shards --apply
|
|
395
402
|
AMEM_HOME=/path/to/synthetic-memory amem status
|
|
396
403
|
# Later old-layout appends are reported by status and carried in idempotently:
|
|
397
404
|
AMEM_HOME=/path/to/synthetic-memory amem migrate --resume
|
|
@@ -403,13 +410,13 @@ Library callers open an already sealed shard directly with `openShardStore({ hom
|
|
|
403
410
|
|
|
404
411
|
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](docs/curated-shard-migration.md).
|
|
405
412
|
|
|
406
|
-
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
|
|
413
|
+
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.
|
|
407
414
|
|
|
408
415
|
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.
|
|
409
416
|
|
|
410
417
|
## Session MCP and work lists (#13 P2)
|
|
411
418
|
|
|
412
|
-
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.
|
|
419
|
+
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.
|
|
413
420
|
|
|
414
421
|
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](docs/contracts.md#mcp-candidate-reads-13-task-209) for the bounded-window and fallback contract.
|
|
415
422
|
|
|
@@ -423,19 +430,19 @@ The frozen nine-tool contract for dotfiles#255 and maru#458:
|
|
|
423
430
|
| `amem_graph` | Screened curated note neighbourhood, excluding capture and candidate data |
|
|
424
431
|
| `amem_work_state_read` | Project work lists, optional closed lists/items |
|
|
425
432
|
| `amem_work_state_write` | Append scalar fields, null clears, task selects an item |
|
|
426
|
-
| `amem_remember` | General memory candidate only
|
|
433
|
+
| `amem_remember` | General memory candidate only; review and accept or reject it through the CLI under current policy |
|
|
427
434
|
| `amem_vault_candidates` | Read-only screened durable curated notes |
|
|
428
435
|
| `amem_capacity` | Numeric machine capacity proposal; no apply capability |
|
|
429
436
|
|
|
430
437
|
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.
|
|
431
438
|
|
|
432
|
-
`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
|
|
439
|
+
`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.
|
|
433
440
|
|
|
434
|
-
`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
|
|
441
|
+
`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.
|
|
435
442
|
|
|
436
|
-
|
|
443
|
+
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](docs/contracts.md#generation-only-vault-links).
|
|
437
444
|
|
|
438
|
-
General memory candidates are separate from curated evidence. `amem review accept <id> --kind memory --policy-free`
|
|
445
|
+
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.
|
|
439
446
|
|
|
440
447
|
`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.
|
|
441
448
|
|
|
@@ -469,8 +476,27 @@ The library exports `buildIntentBrief`, `planGuardNodes`, `matchingPlanFails` an
|
|
|
469
476
|
|
|
470
477
|
## Consolidation and evidence
|
|
471
478
|
|
|
472
|
-
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
|
|
479
|
+
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.
|
|
473
480
|
|
|
474
481
|
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.
|
|
475
482
|
|
|
476
483
|
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`.
|
|
484
|
+
|
|
485
|
+
### Public prune view and v2 sync (opt-in)
|
|
486
|
+
|
|
487
|
+
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.
|
|
488
|
+
|
|
489
|
+
```sh
|
|
490
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --public-prune-view --dry-run
|
|
491
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --public-prune-view --apply
|
|
492
|
+
# Finish an interrupted activation, using its exact recorded forward state.
|
|
493
|
+
AMEM_HOME=/path/to/synthetic-memory amem migrate --public-prune-view --resume
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
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.
|
|
497
|
+
|
|
498
|
+
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.
|
|
499
|
+
|
|
500
|
+
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.
|
|
501
|
+
|
|
502
|
+
`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](docs/generation-neutral-prune-witness.md) states the owned-source integrity and resource ceilings; this source behavior does not establish release or live-host verification.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-mem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Curated, shared memory for coding agents: append-only logs, a derived index, tiers and a typed graph.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"typescript": "^5.8.0",
|
|
17
17
|
"@staix/agent-mem-v1": "npm:@staix/agent-mem@0.1.1",
|
|
18
18
|
"@staix/agent-mem-v02": "npm:@staix/agent-mem@0.2.0",
|
|
19
|
-
"@staix/agent-mem-pre-vault": "npm:@staix/agent-mem@0.3.1"
|
|
19
|
+
"@staix/agent-mem-pre-vault": "npm:@staix/agent-mem@0.3.1",
|
|
20
|
+
"@staix/agent-mem-v011": "npm:@staix/agent-mem@0.11.0",
|
|
21
|
+
"@staix/agent-mem-v012": "npm:@staix/agent-mem@0.12.0",
|
|
22
|
+
"@staix/agent-mem-v013": "npm:@staix/agent-mem@0.13.0"
|
|
20
23
|
},
|
|
21
24
|
"exports": {
|
|
22
25
|
".": "./src/index.ts",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { noteDisplayActor } from "./memory/cli-attribution.ts";
|
|
2
|
+
import { singleLine } from "./memory/style.ts";
|
|
3
|
+
import { createMemoryMcp } from "./mcp-memory.ts";
|
|
4
|
+
import { memoryHome } from "./memory/home.ts";
|
|
5
|
+
import type { ParsedCommand } from "./commands-table.ts";
|
|
6
|
+
|
|
7
|
+
const UNAVAILABLE = "Memory operation unavailable or refused; no partial result is shown.";
|
|
8
|
+
const RESULT_BYTES = 262_144;
|
|
9
|
+
type ReadCall = { tool: keyof ReturnType<typeof createMemoryMcp>["tools"]; args: Record<string, unknown> };
|
|
10
|
+
|
|
11
|
+
function values(argv: readonly string[], name: string): string[] {
|
|
12
|
+
const out: string[] = [];
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const token = argv[i]!;
|
|
15
|
+
if (token === name) {
|
|
16
|
+
const value = argv[++i];
|
|
17
|
+
if (value !== undefined && !value.startsWith("--")) out.push(value);
|
|
18
|
+
} else if (token.startsWith(`${name}=`)) out.push(token.slice(name.length + 1));
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function positiveLimit(value: string | boolean | undefined): number | undefined {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
const limit = Number(value);
|
|
26
|
+
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 100 ? limit : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function route(parsed: ParsedCommand, argv: readonly string[]): ReadCall | undefined {
|
|
30
|
+
const { path, options, positionals, definition } = parsed;
|
|
31
|
+
|
|
32
|
+
if (path[0] === "machine" && options["--label"] === undefined) throw new Error(UNAVAILABLE);
|
|
33
|
+
// Write/preview/confirmation paths continue through the guarded administrative handlers.
|
|
34
|
+
if (definition.mutates !== undefined) return undefined;
|
|
35
|
+
if (path[0] === "project" && path[1] === "export") throw new Error(UNAVAILABLE);
|
|
36
|
+
if (path[0] === "setup") return undefined;
|
|
37
|
+
|
|
38
|
+
const flags = Object.keys(options).filter(key => key !== "--json" && key !== "--policy-free");
|
|
39
|
+
const only = (...allowed: string[]) => flags.every(flag => allowed.includes(flag));
|
|
40
|
+
const limit = positiveLimit(options["--limit"]);
|
|
41
|
+
if (options["--limit"] !== undefined && limit === undefined) throw new Error(UNAVAILABLE);
|
|
42
|
+
|
|
43
|
+
if (path[0] === "search") {
|
|
44
|
+
if (!only("--limit", "--since", "--explain") || !positionals.join(" ").trim()) throw new Error(UNAVAILABLE);
|
|
45
|
+
return { tool: "amem_search", args: { query: positionals.join(" "), ...(limit === undefined ? {} : { limit }), ...(options["--since"] !== undefined ? { since: options["--since"] } : {}), ...(options["--explain"] === true ? { explain: true } : {}) } };
|
|
46
|
+
}
|
|
47
|
+
if (path[0] === "list") {
|
|
48
|
+
if (!only("--limit", "--since")) throw new Error(UNAVAILABLE);
|
|
49
|
+
return { tool: "amem_timeline", args: { ...(limit === undefined ? {} : { limit }), ...(options["--since"] !== undefined ? { since: options["--since"] } : {}) } };
|
|
50
|
+
}
|
|
51
|
+
if (path[0] === "show") {
|
|
52
|
+
if (!only() || positionals.length !== 1 || !positionals[0]!.trim()) throw new Error(UNAVAILABLE);
|
|
53
|
+
return { tool: "amem_show", args: { id: positionals[0] } };
|
|
54
|
+
}
|
|
55
|
+
if (path[0] === "brief") {
|
|
56
|
+
if (!only("--for", "--paths", "--symbols", "--bytes") || typeof options["--for"] !== "string") throw new Error(UNAVAILABLE);
|
|
57
|
+
const rawBytes = options["--bytes"];
|
|
58
|
+
if (rawBytes !== undefined && !/^[1-9][0-9]*$/.test(String(rawBytes))) throw new Error(UNAVAILABLE);
|
|
59
|
+
return { tool: "amem_brief", args: { intent: options["--for"], paths: values(argv, "--paths"), symbols: values(argv, "--symbols"), ...(rawBytes === undefined ? {} : { maxBytes: Number(rawBytes) }) } };
|
|
60
|
+
}
|
|
61
|
+
if (path[0] === "graph") {
|
|
62
|
+
if (!only("--hops") || positionals.length !== 1) throw new Error(UNAVAILABLE);
|
|
63
|
+
const hops = options["--hops"] === undefined ? 1 : Number(options["--hops"]);
|
|
64
|
+
if (hops !== 1 && hops !== 2) throw new Error(UNAVAILABLE);
|
|
65
|
+
return { tool: "amem_graph", args: { start: positionals[0], ...(options["--hops"] === undefined ? {} : { hops }) } };
|
|
66
|
+
}
|
|
67
|
+
if (path[0] === "vault-candidates") {
|
|
68
|
+
if (!only("--limit") || options["--all-projects"] !== undefined || options["--project"] !== undefined) throw new Error(UNAVAILABLE);
|
|
69
|
+
return { tool: "amem_vault_candidates", args: limit === undefined ? {} : { limit } };
|
|
70
|
+
}
|
|
71
|
+
if (path[0] === "capacity" && path.length === 1) {
|
|
72
|
+
if (!only()) throw new Error(UNAVAILABLE);
|
|
73
|
+
return { tool: "amem_capacity", args: {} };
|
|
74
|
+
}
|
|
75
|
+
throw new Error(UNAVAILABLE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Route marked-agent read commands through the same startup-bound MCP policy and handlers. */
|
|
79
|
+
export async function agentCliRead(
|
|
80
|
+
parsed: ParsedCommand,
|
|
81
|
+
argv: readonly string[],
|
|
82
|
+
context: { cwd?: string; env?: Readonly<Record<string, string | undefined>> },
|
|
83
|
+
): Promise<string | undefined> {
|
|
84
|
+
const call = route(parsed, argv);
|
|
85
|
+
if (!call) return undefined;
|
|
86
|
+
|
|
87
|
+
let memory: ReturnType<typeof createMemoryMcp> | undefined;
|
|
88
|
+
try {
|
|
89
|
+
const env = context.env ?? process.env;
|
|
90
|
+
memory = createMemoryMcp({ cwd: context.cwd ?? process.cwd(), base: memoryHome({ ...process.env, ...env }) });
|
|
91
|
+
const value = await memory.tools[call.tool](call.args);
|
|
92
|
+
const output = JSON.stringify(value);
|
|
93
|
+
if (output === undefined || Buffer.byteLength(output) > RESULT_BYTES) throw new Error(UNAVAILABLE);
|
|
94
|
+
if (parsed.path[0] === "show" && parsed.options["--json"] !== true && value && typeof value === "object" && "peer" in value && typeof value.peer === "string") {
|
|
95
|
+
const shown = `actor: ${noteDisplayActor(value.peer)}, peer: ${singleLine(value.peer)}\n${output}`;
|
|
96
|
+
if (Buffer.byteLength(shown) > RESULT_BYTES) throw new Error(UNAVAILABLE);
|
|
97
|
+
return shown;
|
|
98
|
+
}
|
|
99
|
+
return output;
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(UNAVAILABLE);
|
|
102
|
+
} finally {
|
|
103
|
+
try { memory?.close(); } catch { /* close failures do not expose partial reads */ }
|
|
104
|
+
}
|
|
105
|
+
}
|