@shadowforge0/aquifer-memory 1.8.1 → 1.9.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.
@@ -73,20 +73,40 @@ Or run the server directly:
73
73
  DATABASE_URL=... EMBED_PROVIDER=ollama npx aquifer mcp
74
74
  ```
75
75
 
76
- For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated` only when you want `session_recall` and `session_bootstrap` to serve active curated memory; `evidence_recall` remains the explicit evidence/debug tool in both modes. Rollback is just setting env or config back to `legacy`.
76
+ For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated` only when you want compatibility `session_recall` and `session_bootstrap` to serve active curated memory. Use `memory_recall` for explicit current-memory lookup, `historical_recall` for the historical/session plane, and `evidence_recall` for the audit/debug lane. Rollback is just setting env or config back to `legacy`.
77
+
78
+ ## Connect OpenClaw
79
+
80
+ For OpenClaw, use the host installer instead of wiring each consumer by hand:
81
+
82
+ ```bash
83
+ npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
84
+ node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
85
+ ```
86
+
87
+ The installer links and enables the optional OpenClaw extension, updates `plugins.load.paths` / `plugins.entries["aquifer-memory"]`, and points `mcp.servers.aquifer` at the package's `consumers/mcp.js` while preserving existing MCP env values. For source checkouts, `scripts/install-openclaw.sh` first installs the current package into `$OPENCLAW_HOME/node_modules`, then runs the same packaged installer.
88
+
89
+ Run with `--dry-run --json` to verify the active package version, extension path, plugin config, and MCP target without changing files.
77
90
 
78
91
  ## Most common commands
79
92
 
80
93
  | Goal | Command |
81
94
  |---|---|
82
95
  | Verify setup | `npx aquifer quickstart` |
96
+ | Run read-only diagnostics | `npx aquifer doctor --json` |
83
97
  | Start MCP server | `npx aquifer mcp` |
98
+ | Install/update OpenClaw wiring | `node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"` |
84
99
  | Search memory | `npx aquifer recall "auth middleware"` |
100
+ | Explain current-memory selection | `npx aquifer explain bootstrap --active-scope-key project:aquifer --json` |
101
+ | Inspect finalization ledger | `npx aquifer finalization list --status finalized --json` |
102
+ | Inspect operator ledgers | `npx aquifer operator status --json` |
103
+ | Review current-memory feedback issues | `npx aquifer review queue --scope-key project:aquifer --json` |
85
104
  | Plan curated compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
86
105
  | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
87
106
  | Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
88
107
  | Show stats | `npx aquifer stats` |
89
108
  | Enrich pending sessions | `npx aquifer backfill` |
109
+ | Resolve a reviewed memory issue | `npx aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --expected-latest-issue-feedback-id 9 --json` |
90
110
 
91
111
  Timer synthesis is an operator-reviewed candidate workflow. The prompt output
92
112
  and summary JSON do not become active curated memory unless the apply step is
@@ -102,4 +122,17 @@ If you see `type "vector" does not exist`, `pgvector` is not installed.
102
122
 
103
123
  If recall returns no results, the embedding endpoint is usually unreachable or misconfigured.
104
124
 
125
+ For continuity or operator questions, use the read-only governance commands
126
+ first: `doctor`, `finalization list|inspect`, `explain bootstrap|memory`, and
127
+ `operator status|inspect`. Use `review queue|inspect` when curated memory
128
+ feedback has marked visible current-memory rows as `incorrect`, `stale`,
129
+ `scope_mismatch`, or similar issue types. These commands diagnose the DB and
130
+ serving state without changing memory truth, finalization status, MCP tools, or
131
+ operator leases. The review surface does not print raw transcripts, feedback
132
+ notes, feedback metadata, or memory payloads; `review queue|inspect` still uses
133
+ the normal Aquifer migration gate because it depends on the resolution ledger
134
+ schema. After human review, `review resolve` appends a resolution ledger row
135
+ only; it does not edit current memory truth, and newer issue feedback makes the
136
+ item show up in the queue again.
137
+
105
138
  If you want the full setup matrix, host-specific examples, and advanced configuration for summarization, entities, reranking, or operations, continue to [docs/setup.md](setup.md).
package/docs/setup.md CHANGED
@@ -49,7 +49,7 @@ Aquifer reads configuration from three sources (in priority order):
49
49
  2. Environment variables (see below)
50
50
  3. Programmatic overrides via `createAquifer()`
51
51
 
52
- Default public serving mode is `legacy`. Opt into `curated` only when you want `session_recall` and `session_bootstrap` to read active curated memory. `evidence_recall` remains the explicit audit/debug lane in both modes, and rollback is just setting env or config back to `legacy`.
52
+ Default public serving mode is `legacy`. Opt into `curated` only when you want compatibility `session_recall` and `session_bootstrap` to read active curated memory. Use `memory_recall` for explicit current-memory lookup, `historical_recall` for the historical/session plane, and `evidence_recall` for the audit/debug lane. Rollback is just setting env or config back to `legacy`.
53
53
 
54
54
  Backend profiles are explicit. `postgres` is the full backend and remains required for semantic recall, migrations, curated memory, and operator workflows. `local` is a zero-config starter profile with JSON-file persistence, raw session writes, lexical recall, bootstrap, stats, and export. It is intentionally degraded and does not create embeddings or run operator workflows:
55
55
 
@@ -76,7 +76,8 @@ AQUIFER_BACKEND=local npx aquifer backend-info --json
76
76
  "memory": {
77
77
  "servingMode": "legacy",
78
78
  "activeScopeKey": "project:aquifer",
79
- "activeScopePath": ["global", "project:aquifer"]
79
+ "activeScopePath": ["global", "project:aquifer"],
80
+ "allowedScopeKeys": ["global", "project:aquifer"]
80
81
  },
81
82
  "embed": {
82
83
  "baseUrl": "http://localhost:11434/v1",
@@ -113,6 +114,7 @@ export AQUIFER_MEMORY_SERVING_MODE="legacy"
113
114
  # export AQUIFER_MEMORY_SERVING_MODE="curated"
114
115
  # export AQUIFER_MEMORY_ACTIVE_SCOPE_KEY="project:aquifer"
115
116
  # export AQUIFER_MEMORY_ACTIVE_SCOPE_PATH="global,project:aquifer"
117
+ # export AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS="global,project:aquifer"
116
118
 
117
119
  # Optional Codex active-session checkpoint heartbeat policy.
118
120
  # Command flags still take precedence over these env vars.
@@ -123,6 +125,34 @@ export AQUIFER_MEMORY_SERVING_MODE="legacy"
123
125
 
124
126
  Copy `.env.example` from the repo root for a full annotated list.
125
127
 
128
+ ### Curated scope contract
129
+
130
+ Current-memory serving uses two separate scope concepts. `activeScopePath` is
131
+ the ordered inheritance path used by bootstrap, recall, and explain. Broader
132
+ entries can feed defaults into narrower scopes only when they appear in that
133
+ path. `allowedScopeKeys` is the caller boundary. If a runtime request asks for
134
+ an `activeScopeKey`, `activeScopePath`, `scopeKey`, or resolved `scopeId`
135
+ outside `allowedScopeKeys`, Aquifer rejects the request before reading current
136
+ memory rows. When `activeScopePath` is omitted, it defaults to `global` plus the
137
+ configured `activeScopeKey`, or to `global` alone when no active scope is
138
+ configured. When `allowedScopeKeys` is omitted, it defaults to that active scope
139
+ path.
140
+
141
+ Serving inheritance is deterministic:
142
+
143
+ - `defaultable`: broader rows are defaults, narrower rows with the same
144
+ `canonicalKey` win.
145
+ - `exclusive`: currently an explicit narrowest-wins alias for serving. It keeps
146
+ the producer label visible in explain output but does not add a separate
147
+ runtime behavior.
148
+ - `additive`: all applicable rows are merged after scope filtering.
149
+ - `non_inheritable`: only the exact active scope can serve the row.
150
+
151
+ `aquifer explain bootstrap|memory --json` returns selected/excluded reasons
152
+ and a `scopeInheritance` block for each row. Non-selected rows retain safe
153
+ identity and reason fields but redact `canonicalKey`, `title`, and `summary`,
154
+ so explain remains diagnostic instead of becoming a cross-scope content probe.
155
+
126
156
  ## Step 4: Verify everything works
127
157
 
128
158
  ```bash
@@ -141,6 +171,25 @@ npx aquifer mcp
141
171
 
142
172
  The server starts on stdio and waits for MCP client connections. There is no visible output on success — the server is ready when the process stays running without error.
143
173
 
174
+ ### OpenClaw host install/update
175
+
176
+ OpenClaw should be updated through one host-level installer, not by editing each Aquifer consumer path separately:
177
+
178
+ ```bash
179
+ npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
180
+ node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
181
+ ```
182
+
183
+ The command links `$OPENCLAW_HOME/extensions/aquifer-memory` to the package's OpenClaw extension, enables `plugins.entries["aquifer-memory"]`, adds the extension to `plugins.load.paths`, and updates `openclaw.json` so `mcp.servers.aquifer` runs the same package's `consumers/mcp.js`. Existing `mcp.servers.aquifer.env` values are preserved, and `openclaw.json` is backed up before writing.
184
+
185
+ For a source checkout, run the repo wrapper:
186
+
187
+ ```bash
188
+ bash scripts/install-openclaw.sh "$OPENCLAW_HOME"
189
+ ```
190
+
191
+ That wrapper installs the current package into `$OPENCLAW_HOME/node_modules` before delegating to the packaged installer. Use `--dry-run --json` to inspect the package version, extension link, plugin config, and MCP target without changing files. `--link-current-package` is a development-only escape hatch for wiring a source checkout directly.
192
+
144
193
  ### Verify with the library API (optional)
145
194
 
146
195
  If you want to test the library directly instead of the CLI:
@@ -203,33 +252,25 @@ Add to `.claude.json` (project-level) or user-level MCP config:
203
252
  }
204
253
  ```
205
254
 
206
- Tools appear as `mcp__aquifer__session_recall`, `mcp__aquifer__evidence_recall`, `mcp__aquifer__session_bootstrap`, `mcp__aquifer__session_feedback`, `mcp__aquifer__memory_feedback`, `mcp__aquifer__feedback_stats`, `mcp__aquifer__memory_stats`, `mcp__aquifer__memory_pending`.
255
+ Tools appear as `mcp__aquifer__memory_recall`, `mcp__aquifer__historical_recall`, `mcp__aquifer__session_recall`, `mcp__aquifer__evidence_recall`, `mcp__aquifer__session_feedback`, `mcp__aquifer__memory_feedback`, `mcp__aquifer__memory_stats`, `mcp__aquifer__memory_pending`, `mcp__aquifer__feedback_stats`, `mcp__aquifer__session_bootstrap`.
207
256
 
208
- `evidence_recall` is an explicit audit/debug tool. Use `session_recall` for normal memory lookup; broad evidence searches require an audit boundary filter such as `agentId`, `source`, or `dateFrom/dateTo`, unless the caller explicitly opts into unsafe debug mode.
257
+ Use `memory_recall` for explicit current-memory lookup, `historical_recall` for older session detail, and compatibility `session_recall` when the host should follow the configured serving mode. `evidence_recall` is the explicit audit/debug tool; broad evidence searches require an audit boundary filter such as `agentId`, `source`, or `dateFrom/dateTo`, unless the caller explicitly opts into unsafe debug mode.
209
258
 
210
259
  ### OpenClaw
211
260
 
212
- Add to `openclaw.json`:
261
+ Install or update Aquifer inside the OpenClaw host root, then let the installer
262
+ wire both the MCP server and the optional extension from the same package root:
213
263
 
214
- ```json
215
- {
216
- "mcp": {
217
- "servers": {
218
- "aquifer": {
219
- "command": "node",
220
- "args": ["/absolute/path/to/aquifer/consumers/mcp.js"],
221
- "env": {
222
- "DATABASE_URL": "postgresql://...",
223
- "AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
224
- "AQUIFER_EMBED_MODEL": "bge-m3"
225
- }
226
- }
227
- }
228
- }
229
- }
264
+ ```bash
265
+ npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
266
+ node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
230
267
  ```
231
268
 
232
- Tools materialize as `aquifer__session_recall`, `aquifer__evidence_recall`, `aquifer__session_bootstrap`, `aquifer__session_feedback`, `aquifer__memory_feedback`, `aquifer__feedback_stats`, `aquifer__memory_stats`, `aquifer__memory_pending`.
269
+ The installer preserves existing `mcp.servers.aquifer.env` values and backs up
270
+ `openclaw.json` before writing. Use `--dry-run --json` to inspect package
271
+ version, MCP target, and extension link without changing files.
272
+
273
+ Tools materialize as `aquifer__memory_recall`, `aquifer__historical_recall`, `aquifer__session_recall`, `aquifer__evidence_recall`, `aquifer__session_feedback`, `aquifer__memory_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending`, `aquifer__feedback_stats`, `aquifer__session_bootstrap`.
233
274
 
234
275
  Do **not** use the OpenClaw plugin (`consumers/openclaw-plugin.js`) for tool delivery. The plugin is retained for session capture via `before_reset` only.
235
276
 
@@ -274,6 +315,45 @@ Without `--promote-candidates`, synthesis output is recorded as candidate
274
315
  ledger material only. The prompt and summary file are producer material; active
275
316
  curated memory still requires the explicit promotion gate.
276
317
 
318
+ ## Read-only governance diagnostics
319
+
320
+ Use the governance commands before running write paths when continuity or
321
+ operator state looks wrong:
322
+
323
+ ```bash
324
+ npx aquifer doctor --json
325
+ npx aquifer finalization list --status failed --json
326
+ npx aquifer finalization inspect --id 42 --json
327
+ npx aquifer explain bootstrap --active-scope-key project:aquifer --json
328
+ npx aquifer explain memory --query "serving contract" --active-scope-key project:aquifer --json
329
+ npx aquifer review queue --scope-key project:aquifer --json
330
+ npx aquifer review inspect --memory-id 42 --json
331
+ npx aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --expected-latest-issue-feedback-id 9 --json
332
+ npx aquifer operator status --json
333
+ npx aquifer operator inspect --run-id 42 --kind compaction --json
334
+ ```
335
+
336
+ `doctor` reports package/runtime, backend, DB/schema/tenant readiness,
337
+ migration readiness, serving mode, MCP tool count, session/finalization
338
+ backlog, and stale operator claims. Host checks are scoped: OpenClaw wiring is
339
+ checked only with `--host openclaw` or `--openclaw-home`, and Codex checkpoint
340
+ hook state is checked only with `--host codex`.
341
+
342
+ The finalization, explain, review queue/inspect, and operator inspect commands
343
+ read committed ledger and current-memory projection material only. `review
344
+ queue|inspect` derives its worklist from curated memory feedback events on
345
+ active visible memory records and uses the normal Aquifer migration gate because
346
+ it depends on the resolution ledger schema. It summarizes feedback counts and
347
+ safe identity fields; it does not print raw transcripts, feedback notes,
348
+ feedback metadata, evidence text, or memory payloads. `review resolve` is
349
+ append-only: it records a resolution snapshot through the latest issue feedback
350
+ id, leaves `memory_records` and feedback history untouched, and lets newer
351
+ issue feedback reopen the item. These commands do not promote memory, mutate
352
+ finalization status, reclaim leases, or change the ten-tool MCP surface.
353
+ Explain output is also redacted for non-selected rows: it reports safe identity,
354
+ selection reason, and scope inheritance details, but not hidden, shadowed,
355
+ out-of-scope, inactive, or trimmed row content.
356
+
277
357
  ## Release verification gates
278
358
 
279
359
  For the publish-surface checks:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadowforge0/aquifer-memory",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "PG-native long-term memory for AI agents. Turn-level embedding, hybrid RRF ranking, optional knowledge graph. MCP server, CLI, and library API.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -18,6 +18,7 @@
18
18
  "consumers/codex-active-checkpoint.js",
19
19
  "consumers/codex-current-memory.js",
20
20
  "consumers/codex-handoff.js",
21
+ "consumers/openclaw-install.js",
21
22
  "consumers/openclaw-plugin.js",
22
23
  "consumers/opencode.js",
23
24
  "consumers/shared/",
@@ -71,9 +72,9 @@
71
72
  "scripts": {
72
73
  "test": "node --test test/*.test.js",
73
74
  "test:integration": "node --test test/integration.test.js",
74
- "test:release:package": "node --test test/package-surface.test.js test/mcp-manifest.test.js test/local-backend.test.js test/scope-attribution.test.js test/v1-checkpoint-ledger-schema.test.js test/v1-finalization-envelope-schema.test.js test/v1-evidence-items.test.js test/v1-curated-semantic-recall.test.js test/session-checkpoints.test.js test/session-checkpoint-producer.test.js test/session-checkpoint-planner.test.js test/storage-checkpoint-ranges.test.js test/v1-serving-cutover.test.js test/v1-current-memory-contract.test.js test/v1-scope-inheritance.golden.test.js test/v1-bootstrap-determinism.test.js test/consumer-codex.test.js test/codex-recovery-script.test.js test/codex-handoff.test.js",
75
- "test:release:db": "node -e \"if (!process.env.AQUIFER_TEST_DB_URL) { console.error('AQUIFER_TEST_DB_URL is required for test:release:db'); process.exit(1); }\" && node --test test/v1-evidence-items.test.js test/consumer-mcp.integration.test.js test/consumer-cli.integration.test.js test/codex-finalization-serving.integration.test.js",
76
- "lint": "eslint index.js core/*.js core/backends/*.js consumers/cli.js consumers/mcp.js consumers/claude-code.js consumers/codex.js consumers/codex-active-checkpoint.js consumers/codex-current-memory.js consumers/codex-handoff.js consumers/openclaw-plugin.js consumers/opencode.js consumers/shared/*.js consumers/default/*.js consumers/default/prompts/*.js consumers/openclaw-ext/*.js pipeline/*.js pipeline/consolidation/*.js scripts/*.js test/*.js",
75
+ "test:release:package": "node --test test/package-surface.test.js test/mcp-manifest.test.js test/local-backend.test.js test/scope-attribution.test.js test/doctor.test.js test/finalization-inspector.test.js test/memory-explain.test.js test/memory-review.test.js test/operator-observability.test.js test/storage-finalization-status.test.js test/cli-parseargs.test.js test/v1-checkpoint-ledger-schema.test.js test/v1-finalization-envelope-schema.test.js test/v1-memory-review-resolutions-schema.test.js test/v1-evidence-items.test.js test/v1-curated-semantic-recall.test.js test/session-checkpoints.test.js test/session-checkpoint-producer.test.js test/session-checkpoint-planner.test.js test/storage-checkpoint-ranges.test.js test/v1-serving-cutover.test.js test/v1-current-memory-contract.test.js test/v1-scope-inheritance.golden.test.js test/v1-bootstrap-determinism.test.js test/consumer-codex.test.js test/codex-recovery-script.test.js test/codex-handoff.test.js",
76
+ "test:release:db": "node -e \"if (!process.env.AQUIFER_TEST_DB_URL) { console.error('AQUIFER_TEST_DB_URL is required for test:release:db'); process.exit(1); }\" && node --test --test-concurrency=1 test/v1-evidence-items.test.js test/memory-review.integration.test.js test/consumer-mcp.integration.test.js test/consumer-cli.integration.test.js test/codex-finalization-serving.integration.test.js",
77
+ "lint": "eslint index.js core/*.js core/backends/*.js consumers/cli.js consumers/mcp.js consumers/claude-code.js consumers/codex.js consumers/codex-active-checkpoint.js consumers/codex-current-memory.js consumers/codex-handoff.js consumers/openclaw-install.js consumers/openclaw-plugin.js consumers/opencode.js consumers/shared/*.js consumers/default/*.js consumers/default/prompts/*.js consumers/openclaw-ext/*.js pipeline/*.js pipeline/consolidation/*.js scripts/*.js test/*.js",
77
78
  "hooks:install": "git config core.hooksPath .githooks"
78
79
  },
79
80
  "dependencies": {
@@ -0,0 +1,53 @@
1
+ -- Aquifer v1 memory review resolution ledger
2
+ -- Requires: 007-v1-foundation.sql
3
+ --
4
+ -- This migration is additive. Review resolutions close or defer operator queue
5
+ -- items without mutating memory_records or rewriting feedback history.
6
+
7
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_records_tenant_id_id
8
+ ON ${schema}.memory_records (tenant_id, id);
9
+
10
+ CREATE INDEX IF NOT EXISTS idx_feedback_memory_review_latest
11
+ ON ${schema}.feedback (tenant_id, target_id, feedback_type, created_at DESC, id DESC)
12
+ WHERE target_kind = 'memory_record';
13
+
14
+ CREATE TABLE IF NOT EXISTS ${schema}.memory_review_resolutions (
15
+ id BIGSERIAL PRIMARY KEY,
16
+ tenant_id TEXT NOT NULL DEFAULT 'default',
17
+ memory_id BIGINT NOT NULL,
18
+ canonical_key TEXT NOT NULL CHECK (btrim(canonical_key) <> ''),
19
+ scope_id BIGINT,
20
+ resolution TEXT NOT NULL
21
+ CHECK (resolution IN ('resolved','ignored','deferred')),
22
+ reason TEXT,
23
+ actor_kind TEXT NOT NULL DEFAULT 'user'
24
+ CHECK (actor_kind IN ('user','agent','system','curator')),
25
+ actor_id TEXT,
26
+ issue_feedback_types TEXT[] NOT NULL DEFAULT '{}'::text[],
27
+ resolved_through_feedback_id BIGINT,
28
+ resolved_through_feedback_at TIMESTAMPTZ,
29
+ defer_until TIMESTAMPTZ,
30
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
31
+ resolved_at TIMESTAMPTZ NOT NULL DEFAULT now(),
32
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
33
+ FOREIGN KEY (tenant_id, memory_id)
34
+ REFERENCES ${schema}.memory_records (tenant_id, id) ON DELETE RESTRICT,
35
+ FOREIGN KEY (scope_id)
36
+ REFERENCES ${schema}.scopes(id) ON DELETE SET NULL,
37
+ CHECK (jsonb_typeof(metadata) = 'object'),
38
+ CHECK (defer_until IS NULL OR resolution = 'deferred'),
39
+ CHECK (defer_until IS NULL OR defer_until > resolved_at)
40
+ );
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_memory_review_resolutions_memory_latest
43
+ ON ${schema}.memory_review_resolutions (tenant_id, memory_id, resolved_at DESC, id DESC);
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_memory_review_resolutions_canonical_latest
46
+ ON ${schema}.memory_review_resolutions (tenant_id, canonical_key, resolved_at DESC, id DESC);
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_memory_review_resolutions_defer_until
49
+ ON ${schema}.memory_review_resolutions (tenant_id, defer_until)
50
+ WHERE resolution = 'deferred' AND defer_until IS NOT NULL;
51
+
52
+ COMMENT ON TABLE ${schema}.memory_review_resolutions IS
53
+ 'Append-only operator ledger for memory review queue resolutions. Does not mutate curated memory truth.';
@@ -21,6 +21,7 @@ const {
21
21
  defaultHooksPath,
22
22
  findNewestJsonlFile,
23
23
  isoAt,
24
+ listCheckpointSpool,
24
25
  loadRuntimeConfig,
25
26
  mergeCheckpointHeartbeatHook,
26
27
  readCheckpointMarker,
@@ -215,6 +216,32 @@ function emitCheckpointHeartbeatResult(result, flags = {}) {
215
216
  if (flags.json) console.log(JSON.stringify(result, null, 2));
216
217
  }
217
218
 
219
+ async function cmdCheckpointSpoolStatus(aquifer, flags, opts) {
220
+ const result = listCheckpointSpool(flags, opts);
221
+ if (flags.json) {
222
+ console.log(JSON.stringify(result, null, 2));
223
+ return result;
224
+ }
225
+ const lines = [
226
+ `Spool: ${result.spoolDir}`,
227
+ `Pending proposals: ${result.count} across ${result.sessionCount} sessions`,
228
+ ];
229
+ if (result.latestMtime) lines.push(`Latest: ${result.latestMtime}`);
230
+ for (const row of result.files) {
231
+ const covered = row.coverage?.coveredUntilMessageIndex;
232
+ lines.push([
233
+ `- ${row.fileName}`,
234
+ `session=${row.sessionId || '?'}`,
235
+ `mtime=${row.mtime || '?'}`,
236
+ `bytes=${row.bytes || 0}`,
237
+ `coveredUntilMessageIndex=${covered ?? '?'}`,
238
+ `promptChars=${row.promptChars || 0}`,
239
+ ].join(' '));
240
+ }
241
+ console.log(lines.join('\n'));
242
+ return result;
243
+ }
244
+
218
245
  async function cmdCheckpointHeartbeat(aquifer, flags, opts, hookInputArg) {
219
246
  const hookInput = hookInputArg || (flags['hook-stdin'] === true ? readHookInputFromStdin() : {});
220
247
  const input = checkpointHeartbeatInput(flags, hookInput);
@@ -457,6 +484,7 @@ module.exports = {
457
484
  cmdCheckpointHeartbeat,
458
485
  cmdCheckpointHeartbeatHook,
459
486
  cmdCheckpointPrompt,
487
+ cmdCheckpointSpoolStatus,
460
488
  cmdCheckpointTick,
461
489
  emitCheckpointHeartbeatResult,
462
490
  parseScopePath,
@@ -377,6 +377,114 @@ function spoolCheckpointProposal(dir, prepared = {}, meta = {}) {
377
377
  return { filePath, createdAt: payload.createdAt };
378
378
  }
379
379
 
380
+ function summarizeCheckpointSpoolFile(filePath) {
381
+ const parsed = readJsonFile(filePath);
382
+ let stat;
383
+ try {
384
+ stat = fs.statSync(filePath);
385
+ } catch {
386
+ return null;
387
+ }
388
+ if (!parsed || parsed.kind !== 'codex_active_checkpoint_pending_v1') {
389
+ return {
390
+ filePath,
391
+ fileName: path.basename(filePath),
392
+ bytes: stat.size,
393
+ mtime: stat.mtime.toISOString(),
394
+ parseable: !!parsed,
395
+ kind: parsed?.kind || null,
396
+ ignored: true,
397
+ };
398
+ }
399
+ return {
400
+ filePath,
401
+ fileName: path.basename(filePath),
402
+ bytes: stat.size,
403
+ mtime: stat.mtime.toISOString(),
404
+ parseable: true,
405
+ ignored: false,
406
+ kind: parsed.kind,
407
+ createdAt: parsed.createdAt || null,
408
+ sessionId: parsed.sessionId || null,
409
+ source: parsed.source || null,
410
+ hookEventName: parsed.hookEventName || null,
411
+ triggerKind: parsed.triggerKind || null,
412
+ threshold: parsed.threshold || null,
413
+ coverage: parsed.coverage || null,
414
+ guards: parsed.guards || null,
415
+ hasPrompt: typeof parsed.prompt === 'string' && parsed.prompt.length > 0,
416
+ promptChars: typeof parsed.prompt === 'string' ? parsed.prompt.length : 0,
417
+ };
418
+ }
419
+
420
+ function listCheckpointSpool(flags = {}, opts = {}) {
421
+ const dir = checkpointSpoolDir(flags, opts);
422
+ const limit = Math.max(1, Math.min(200, parseIntFlag(flags.limit, 20)));
423
+ let entries = [];
424
+ try {
425
+ entries = fs.readdirSync(dir);
426
+ } catch (err) {
427
+ if (err && err.code === 'ENOENT') {
428
+ return {
429
+ status: 'ok',
430
+ spoolDir: dir,
431
+ count: 0,
432
+ returned: 0,
433
+ sessionCount: 0,
434
+ totalBytes: 0,
435
+ latestMtime: null,
436
+ files: [],
437
+ sessions: [],
438
+ };
439
+ }
440
+ throw err;
441
+ }
442
+ const sessionFilter = flags['session-id'] && flags['session-id'] !== true
443
+ ? String(flags['session-id'])
444
+ : null;
445
+ const rows = entries
446
+ .filter(fileName => fileName.endsWith('.json'))
447
+ .map(fileName => summarizeCheckpointSpoolFile(path.join(dir, fileName)))
448
+ .filter(Boolean)
449
+ .filter(row => !sessionFilter || row.sessionId === sessionFilter)
450
+ .sort((a, b) => String(b.mtime || '').localeCompare(String(a.mtime || '')));
451
+ const validRows = rows.filter(row => !row.ignored);
452
+ const sessions = new Map();
453
+ for (const row of validRows) {
454
+ const sessionId = row.sessionId || 'unknown';
455
+ const current = sessions.get(sessionId) || {
456
+ sessionId,
457
+ count: 0,
458
+ latestMtime: null,
459
+ maxCoveredUntilMessageIndex: null,
460
+ };
461
+ current.count += 1;
462
+ current.latestMtime = !current.latestMtime || String(row.mtime).localeCompare(current.latestMtime) > 0
463
+ ? row.mtime
464
+ : current.latestMtime;
465
+ const covered = row.coverage?.coveredUntilMessageIndex;
466
+ if (Number.isFinite(Number(covered))) {
467
+ current.maxCoveredUntilMessageIndex = Math.max(
468
+ current.maxCoveredUntilMessageIndex ?? Number(covered),
469
+ Number(covered),
470
+ );
471
+ }
472
+ sessions.set(sessionId, current);
473
+ }
474
+ return {
475
+ status: 'ok',
476
+ spoolDir: dir,
477
+ count: rows.length,
478
+ returned: Math.min(rows.length, limit),
479
+ sessionCount: sessions.size,
480
+ totalBytes: rows.reduce((sum, row) => sum + Number(row.bytes || 0), 0),
481
+ latestMtime: rows[0]?.mtime || null,
482
+ files: rows.slice(0, limit),
483
+ sessions: Array.from(sessions.values())
484
+ .sort((a, b) => String(b.latestMtime || '').localeCompare(String(a.latestMtime || ''))),
485
+ };
486
+ }
487
+
380
488
  function shellQuote(value) {
381
489
  return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
382
490
  }
@@ -506,6 +614,7 @@ module.exports = {
506
614
  inspectCheckpointHeartbeatHook,
507
615
  isoAt,
508
616
  loadRuntimeConfig,
617
+ listCheckpointSpool,
509
618
  mergeCheckpointHeartbeatHook,
510
619
  readCheckpointMarker,
511
620
  readHooksConfig,
@@ -11,6 +11,7 @@ const {
11
11
  cmdCheckpointHeartbeat,
12
12
  cmdCheckpointHeartbeatHook,
13
13
  cmdCheckpointPrompt,
14
+ cmdCheckpointSpoolStatus,
14
15
  cmdCheckpointTick,
15
16
  } = require('./codex-checkpoint-commands');
16
17
  const {
@@ -29,6 +30,7 @@ const {
29
30
  defaultHooksPath,
30
31
  findNewestJsonlFile,
31
32
  inspectCheckpointHeartbeatHook,
33
+ listCheckpointSpool,
32
34
  loadRuntimeConfig,
33
35
  mergeCheckpointHeartbeatHook,
34
36
  readCheckpointMarker,
@@ -38,6 +40,7 @@ const {
38
40
  writeSchedulerMarker,
39
41
  } = require('./codex-checkpoint-runtime');
40
42
  const DB_ENV_KEYS = new Set(['DATABASE_URL', 'AQUIFER_DB_URL', 'AQUIFER_SCHEMA', 'AQUIFER_TENANT_ID']);
43
+ const DECISION_VERDICTS = ['declined', 'deferred', 'skipped'];
41
44
 
42
45
  const VALUE_FLAGS = new Set([
43
46
  'agent-id',
@@ -59,6 +62,7 @@ const VALUE_FLAGS = new Set([
59
62
  'hook-event-name',
60
63
  'hooks-path',
61
64
  'idle-ms',
65
+ 'limit',
62
66
  'max-checkpoint-bytes',
63
67
  'max-checkpoint-chars',
64
68
  'max-checkpoint-messages',
@@ -575,8 +579,8 @@ async function cmdFinalize(aquifer, flags, opts) {
575
579
 
576
580
  async function cmdDecision(aquifer, flags, opts) {
577
581
  const verdict = flags.verdict;
578
- if (!['declined', 'deferred'].includes(verdict)) {
579
- throw new Error('decision requires --verdict declined|deferred');
582
+ if (!DECISION_VERDICTS.includes(verdict)) {
583
+ throw new Error(`decision requires --verdict ${DECISION_VERDICTS.join('|')}`);
580
584
  }
581
585
  const candidates = await listOperationalCandidates(aquifer, opts);
582
586
  if (flags.all === true) {
@@ -664,10 +668,11 @@ async function main(argv = process.argv.slice(2)) {
664
668
  node scripts/codex-recovery.js checkpoint-prompt --file-path FILE --scope-key KEY [options]
665
669
  node scripts/codex-recovery.js checkpoint-tick --scope-key KEY [--file-path FILE|--sessions-dir DIR] [options]
666
670
  node scripts/codex-recovery.js checkpoint-heartbeat --hook-stdin --scope-key KEY [options]
671
+ node scripts/codex-recovery.js checkpoint-spool-status [--json] [--limit N] [--session-id ID]
667
672
  node scripts/codex-recovery.js checkpoint-heartbeat-hook --scope-key KEY [--hooks-path FILE] [--apply]
668
673
  node scripts/codex-recovery.js finalize --session-id ID --summary-stdin [options]
669
- node scripts/codex-recovery.js decision --session-id ID --verdict declined|deferred [options]
670
- node scripts/codex-recovery.js decision --all --verdict declined|deferred [options]
674
+ node scripts/codex-recovery.js decision --session-id ID --verdict declined|deferred|skipped [options]
675
+ node scripts/codex-recovery.js decision --all --verdict declined|deferred|skipped [options]
671
676
  node scripts/codex-recovery.js doctor [--strict-wrapper-env] [--json]`);
672
677
  return;
673
678
  }
@@ -693,6 +698,11 @@ async function main(argv = process.argv.slice(2)) {
693
698
  return;
694
699
  }
695
700
 
701
+ if (command === 'checkpoint-spool-status') {
702
+ await cmdCheckpointSpoolStatus(null, args.flags, opts);
703
+ return;
704
+ }
705
+
696
706
  await withAquifer(async (aquifer) => {
697
707
  switch (command) {
698
708
  case 'preview':
@@ -733,6 +743,7 @@ module.exports = {
733
743
  cmdCheckpointHeartbeat,
734
744
  cmdCheckpointHeartbeatHook,
735
745
  cmdCheckpointPrompt,
746
+ cmdCheckpointSpoolStatus,
736
747
  cmdCheckpointTick,
737
748
  cmdPrompt,
738
749
  acquireHeartbeatClaim,
@@ -750,6 +761,7 @@ module.exports = {
750
761
  defaultHooksPath,
751
762
  findNewestJsonlFile,
752
763
  inspectCheckpointHeartbeatHook,
764
+ listCheckpointSpool,
753
765
  loadRuntimeConfig,
754
766
  loadCodexEnv,
755
767
  main,