@shadowforge0/aquifer-memory 1.8.1 → 1.9.1

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.
Files changed (57) hide show
  1. package/.env.example +1 -0
  2. package/README.md +82 -26
  3. package/README_CN.md +33 -23
  4. package/README_TW.md +25 -24
  5. package/aquifer.config.example.json +2 -1
  6. package/consumers/cli.js +587 -33
  7. package/consumers/codex-active-checkpoint.js +3 -1
  8. package/consumers/codex-current-memory.js +10 -6
  9. package/consumers/codex.js +6 -3
  10. package/consumers/default/daily-entries.js +2 -2
  11. package/consumers/default/index.js +40 -30
  12. package/consumers/default/prompts/summary.js +2 -2
  13. package/consumers/mcp.js +56 -46
  14. package/consumers/openclaw-ext/index.js +65 -7
  15. package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
  16. package/consumers/openclaw-ext/package.json +1 -1
  17. package/consumers/openclaw-install.js +326 -0
  18. package/consumers/openclaw-plugin.js +105 -24
  19. package/consumers/shared/compat-recall.js +101 -0
  20. package/consumers/shared/config.js +2 -0
  21. package/consumers/shared/openclaw-product-tools.js +130 -0
  22. package/consumers/shared/recall-format.js +2 -2
  23. package/core/aquifer.js +553 -41
  24. package/core/backends/local.js +169 -1
  25. package/core/doctor.js +924 -0
  26. package/core/finalization-inspector.js +164 -0
  27. package/core/finalization-review.js +88 -42
  28. package/core/interface.js +629 -0
  29. package/core/mcp-manifest.js +11 -3
  30. package/core/memory-bootstrap.js +25 -27
  31. package/core/memory-consolidation.js +564 -42
  32. package/core/memory-explain.js +593 -0
  33. package/core/memory-promotion.js +392 -55
  34. package/core/memory-recall.js +75 -71
  35. package/core/memory-records.js +107 -108
  36. package/core/memory-review.js +891 -0
  37. package/core/memory-serving.js +61 -4
  38. package/core/memory-type-policy.js +298 -0
  39. package/core/operator-observability.js +249 -0
  40. package/core/postgres-migrations.js +22 -0
  41. package/core/session-checkpoint-producer.js +3 -1
  42. package/core/session-checkpoints.js +1 -1
  43. package/core/session-finalization.js +78 -3
  44. package/core/storage.js +124 -8
  45. package/docs/getting-started.md +50 -4
  46. package/docs/setup.md +163 -24
  47. package/package.json +5 -4
  48. package/schema/004-completion.sql +4 -4
  49. package/schema/010-v1-finalization-review.sql +72 -0
  50. package/schema/019-v1-memory-review-resolutions.sql +53 -0
  51. package/schema/020-v1-assistant-shaping-memory.sql +30 -0
  52. package/scripts/backfill-canonical-key.js +1 -1
  53. package/scripts/codex-checkpoint-commands.js +28 -0
  54. package/scripts/codex-checkpoint-runtime.js +109 -0
  55. package/scripts/codex-recovery.js +16 -4
  56. package/scripts/diagnose-fts-zh.js +1 -1
  57. package/scripts/extract-insights-from-recent-sessions.js +4 -4
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.1",
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-assistant-shaping-memory.test.js test/v1-assistant-shaping-review-explain.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": {
@@ -9,8 +9,8 @@
9
9
  -- * consumer_profiles table — consumer schema registry with composite primary key
10
10
  -- (tenant_id, consumer_id, version) for future multi-tenant safety
11
11
  --
12
- -- All identifiers stay parameterised on ${schema} so P4 schema rename
13
- -- (miranda → aquifer) is a one-line config change rather than a DDL rewrite.
12
+ -- All identifiers stay parameterised on ${schema} so schema renames remain a
13
+ -- config change rather than a DDL rewrite.
14
14
 
15
15
  -- Ensure pg_trgm available (used by existing migrations; re-declared for independent
16
16
  -- run safety).
@@ -141,8 +141,8 @@ CREATE TRIGGER trg_consumer_profiles_updated_at
141
141
  EXECUTE FUNCTION ${schema}.set_updated_at();
142
142
 
143
143
  -- timeline_events: append-only event log keyed by (tenant, agent, occurred_at).
144
- -- category vocabulary is consumer-owned (focus/todo/mood/handoff/narrative/cli
145
- -- for Miranda default), event shape is strict core. idempotency_key UNIQUE
144
+ -- category vocabulary is consumer-owned, event shape is strict core.
145
+ -- idempotency_key UNIQUE
146
146
  -- across the table to make caller-driven dedupe safe.
147
147
  CREATE TABLE IF NOT EXISTS ${schema}.timeline_events (
148
148
  id BIGSERIAL PRIMARY KEY,
@@ -43,6 +43,78 @@ ALTER TABLE ${schema}.fact_assertions_v1
43
43
  'tombstoned','quarantined','archived','incorrect'
44
44
  ));
45
45
 
46
+ ALTER TABLE ${schema}.memory_records
47
+ DROP CONSTRAINT IF EXISTS memory_records_lifecycle_consistency_check;
48
+
49
+ ALTER TABLE ${schema}.memory_records
50
+ ADD CONSTRAINT memory_records_lifecycle_consistency_check
51
+ CHECK (
52
+ (valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from)
53
+ AND (revoked_at IS NULL OR accepted_at IS NULL OR revoked_at >= accepted_at)
54
+ AND (superseded_at IS NULL OR accepted_at IS NULL OR superseded_at >= accepted_at)
55
+ AND NOT (revoked_at IS NOT NULL AND superseded_at IS NOT NULL)
56
+ AND (status <> 'active' OR (
57
+ revoked_at IS NULL
58
+ AND superseded_at IS NULL
59
+ AND superseded_by IS NULL
60
+ ))
61
+ AND (status <> 'superseded' OR superseded_at IS NOT NULL)
62
+ AND (status <> 'revoked' OR revoked_at IS NOT NULL)
63
+ ) NOT VALID;
64
+
65
+ ALTER TABLE ${schema}.fact_assertions_v1
66
+ DROP CONSTRAINT IF EXISTS fact_assertions_v1_lifecycle_consistency_check;
67
+
68
+ ALTER TABLE ${schema}.fact_assertions_v1
69
+ ADD CONSTRAINT fact_assertions_v1_lifecycle_consistency_check
70
+ CHECK (
71
+ (valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from)
72
+ AND (revoked_at IS NULL OR accepted_at IS NULL OR revoked_at >= accepted_at)
73
+ AND (superseded_at IS NULL OR accepted_at IS NULL OR superseded_at >= accepted_at)
74
+ AND NOT (revoked_at IS NOT NULL AND superseded_at IS NOT NULL)
75
+ AND (status <> 'active' OR (
76
+ revoked_at IS NULL
77
+ AND superseded_at IS NULL
78
+ AND superseded_by IS NULL
79
+ ))
80
+ AND (status <> 'superseded' OR superseded_at IS NOT NULL)
81
+ AND (status <> 'revoked' OR revoked_at IS NOT NULL)
82
+ ) NOT VALID;
83
+
84
+ DO $$
85
+ BEGIN
86
+ IF NOT EXISTS (
87
+ SELECT 1
88
+ FROM pg_constraint
89
+ WHERE conrelid = '${schema}.memory_records'::regclass
90
+ AND conname = 'memory_records_superseded_by_tenant_fk'
91
+ ) THEN
92
+ ALTER TABLE ${schema}.memory_records
93
+ ADD CONSTRAINT memory_records_superseded_by_tenant_fk
94
+ FOREIGN KEY (tenant_id, superseded_by)
95
+ REFERENCES ${schema}.memory_records (tenant_id, id)
96
+ NOT VALID;
97
+ END IF;
98
+ END;
99
+ $$;
100
+
101
+ DO $$
102
+ BEGIN
103
+ IF NOT EXISTS (
104
+ SELECT 1
105
+ FROM pg_constraint
106
+ WHERE conrelid = '${schema}.fact_assertions_v1'::regclass
107
+ AND conname = 'fact_assertions_v1_superseded_by_tenant_fk'
108
+ ) THEN
109
+ ALTER TABLE ${schema}.fact_assertions_v1
110
+ ADD CONSTRAINT fact_assertions_v1_superseded_by_tenant_fk
111
+ FOREIGN KEY (tenant_id, superseded_by)
112
+ REFERENCES ${schema}.fact_assertions_v1 (tenant_id, id)
113
+ NOT VALID;
114
+ END IF;
115
+ END;
116
+ $$;
117
+
46
118
  -- =========================================================================
47
119
  -- Row-level finalization lineage
48
120
  -- =========================================================================
@@ -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.';
@@ -0,0 +1,30 @@
1
+ -- Aquifer v1 assistant-shaping memory layer
2
+ -- Requires: 007-v1-foundation.sql
3
+ -- Usage: replace ${schema} with actual schema name
4
+ --
5
+ -- Adds a product-level memory type for user-facing assistant shaping:
6
+ -- response style, retrieval policy, interaction boundaries, and tool routing.
7
+ -- This remains curated memory, not a host/persona-specific schema concept.
8
+
9
+ ALTER TABLE ${schema}.memory_records
10
+ DROP CONSTRAINT IF EXISTS memory_records_memory_type_check;
11
+
12
+ ALTER TABLE ${schema}.memory_records
13
+ ADD CONSTRAINT memory_records_memory_type_check
14
+ CHECK (memory_type IN (
15
+ 'assistant_shaping',
16
+ 'fact','state','decision','preference','constraint',
17
+ 'entity_note','open_loop','conclusion'
18
+ ));
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_memory_records_assistant_shaping_bootstrap
21
+ ON ${schema}.memory_records (tenant_id, scope_id, status, accepted_at DESC, id)
22
+ WHERE status = 'active'
23
+ AND memory_type = 'assistant_shaping'
24
+ AND visible_in_bootstrap;
25
+
26
+ COMMENT ON CONSTRAINT memory_records_memory_type_check ON ${schema}.memory_records IS
27
+ 'Allowed curated memory types. assistant_shaping stores user-facing assistant behavior and retrieval policy, not host-specific persona code.';
28
+
29
+ COMMENT ON INDEX ${schema}.idx_memory_records_assistant_shaping_bootstrap IS
30
+ 'Fast path for high-priority assistant-shaping memories during bootstrap materialization.';
@@ -32,7 +32,7 @@ function printUsageAndExit(code = 0) {
32
32
  'Usage: node scripts/backfill-canonical-key.js --schema <name> [options]',
33
33
  '',
34
34
  'Required:',
35
- ' --schema <name> Target schema (e.g. miranda, jenny)',
35
+ ' --schema <name> Target schema (e.g. aquifer, app_memory)',
36
36
  ' --agent <id> Limit to one agent (or use --all-agents)',
37
37
  '',
38
38
  'Optional:',
@@ -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,
@@ -27,7 +27,7 @@ const SCHEMA = process.env.AQUIFER_SCHEMA || 'public';
27
27
 
28
28
  const DEFAULT_QUERIES = [
29
29
  // latin
30
- 'afterburn', 'bootstrap', 'session', 'recall', 'entity', 'OpenCode', 'Jenny', 'Aquifer',
30
+ 'afterburn', 'bootstrap', 'session', 'recall', 'entity', 'OpenCode', 'ExampleUser', 'Aquifer',
31
31
  // CJK short tokens — 最容易暴露 tokenizer 問題
32
32
  '記憶', '時區', '去重', '架構', '修復',
33
33
  // CJK phrase
@@ -17,7 +17,7 @@
17
17
  * [--days 14] \
18
18
  * [--max-sessions 50] \
19
19
  * [--types preference,pattern,frustration,workflow] \
20
- * [--schema miranda] \
20
+ * [--schema aquifer] \
21
21
  * [--tenant-id default] \
22
22
  * [--dry-run]
23
23
  *
@@ -71,7 +71,7 @@ function parseArgs(argv) {
71
71
  days: 14,
72
72
  maxSessions: 50,
73
73
  types: ['preference', 'pattern', 'frustration', 'workflow'],
74
- schema: process.env.AQUIFER_SCHEMA || 'miranda',
74
+ schema: process.env.AQUIFER_SCHEMA || 'aquifer',
75
75
  tenantId: process.env.AQUIFER_TENANT_ID || 'default',
76
76
  dryRun: false,
77
77
  };
@@ -109,8 +109,8 @@ themes. Returning only 2-3 on a rich window means you're under-extracting.
109
109
  Returning 0 is only correct when the window is genuinely sparse.
110
110
 
111
111
  ## Insight types
112
- - preference: stable user preference (e.g. "MK prefers terse responses with no trailing summaries")
113
- - pattern: recurring behaviour or decision (e.g. "MK runs /develop before any non-trivial schema change")
112
+ - preference: stable user preference (e.g. "The user prefers terse responses with no trailing summaries")
113
+ - pattern: recurring behaviour or decision (e.g. "The user runs a planning workflow before non-trivial schema changes")
114
114
  - frustration: repeated pain point (e.g. "Cron jobs.json prompt parse keeps breaking on minor LLM output drift")
115
115
  - workflow: reusable procedure that worked (e.g. "Aquifer release: pack tarball -> bump gateway pkg -> migrate -> restart")
116
116