@shadowforge0/aquifer-memory 1.6.0 → 1.8.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.
Files changed (44) hide show
  1. package/.env.example +8 -0
  2. package/README.md +72 -0
  3. package/README_CN.md +17 -0
  4. package/README_TW.md +4 -0
  5. package/aquifer.config.example.json +19 -0
  6. package/consumers/cli.js +259 -12
  7. package/consumers/codex-active-checkpoint.js +186 -0
  8. package/consumers/codex-current-memory.js +106 -0
  9. package/consumers/codex-handoff.js +551 -6
  10. package/consumers/codex.js +209 -25
  11. package/consumers/mcp.js +144 -6
  12. package/consumers/shared/config.js +60 -1
  13. package/consumers/shared/factory.js +10 -3
  14. package/core/aquifer.js +357 -838
  15. package/core/backends/capabilities.js +89 -0
  16. package/core/backends/local.js +430 -0
  17. package/core/legacy-bootstrap.js +140 -0
  18. package/core/mcp-manifest.js +66 -2
  19. package/core/memory-bootstrap.js +20 -8
  20. package/core/memory-consolidation.js +365 -11
  21. package/core/memory-promotion.js +157 -26
  22. package/core/memory-recall.js +341 -22
  23. package/core/memory-records.js +347 -11
  24. package/core/memory-serving.js +132 -0
  25. package/core/postgres-migrations.js +533 -0
  26. package/core/public-session-filter.js +40 -0
  27. package/core/recall-runtime.js +115 -0
  28. package/core/scope-attribution.js +279 -0
  29. package/core/session-checkpoint-producer.js +412 -0
  30. package/core/session-checkpoints.js +432 -0
  31. package/core/session-finalization.js +98 -2
  32. package/core/storage-checkpoints.js +546 -0
  33. package/core/storage.js +121 -8
  34. package/docs/getting-started.md +6 -0
  35. package/docs/setup.md +66 -3
  36. package/package.json +8 -4
  37. package/schema/014-v1-checkpoint-runs.sql +349 -0
  38. package/schema/015-v1-evidence-items.sql +92 -0
  39. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  40. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  41. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  42. package/scripts/codex-checkpoint-commands.js +464 -0
  43. package/scripts/codex-checkpoint-runtime.js +520 -0
  44. package/scripts/codex-recovery.js +246 -1
package/.env.example CHANGED
@@ -1,4 +1,7 @@
1
1
  DATABASE_URL=postgresql://aquifer:aquifer@localhost:5432/aquifer
2
+ AQUIFER_BACKEND=postgres
3
+ # AQUIFER_BACKEND=local
4
+ # AQUIFER_LOCAL_PATH=.aquifer/aquifer.local.json
2
5
  AQUIFER_SCHEMA=aquifer
3
6
  AQUIFER_TENANT_ID=default
4
7
 
@@ -9,6 +12,11 @@ AQUIFER_MEMORY_SERVING_MODE=legacy
9
12
  # AQUIFER_MEMORY_ACTIVE_SCOPE_KEY=project:example
10
13
  # AQUIFER_MEMORY_ACTIVE_SCOPE_PATH=global,project:example
11
14
 
15
+ # Optional Codex active-session checkpoint heartbeat policy.
16
+ # AQUIFER_CODEX_CHECKPOINT_CHECK_INTERVAL_MINUTES=10
17
+ # AQUIFER_CODEX_CHECKPOINT_EVERY_MESSAGES=20
18
+ # AQUIFER_CODEX_CHECKPOINT_QUIET_MS=3000
19
+
12
20
  AQUIFER_EMBED_BASE_URL=http://localhost:11434/v1
13
21
  AQUIFER_EMBED_MODEL=bge-m3
14
22
  # EMBED_PROVIDER=ollama
package/README.md CHANGED
@@ -74,12 +74,31 @@ Keep `AQUIFER_MEMORY_SERVING_MODE=legacy` for first rollout. Switch to `curated`
74
74
  | Goal | Command |
75
75
  |---|---|
76
76
  | Verify setup | `npx aquifer quickstart` |
77
+ | Inspect selected backend capabilities without DB connection | `AQUIFER_BACKEND=local npx aquifer backend-info --json` |
77
78
  | Start the MCP server | `npx aquifer mcp` |
78
79
  | Search memory manually | `npx aquifer recall "auth middleware"` |
79
80
  | Plan curated memory compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
81
+ | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
82
+ | Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
83
+ | Generate a finalized-session checkpoint prompt | `npx aquifer operator checkpoint --scope-key project:aquifer --min-finalizations 10 --include-synthesis-prompt --json` |
84
+ | Heartbeat-check an active Codex session for checkpoint work | `npx aquifer codex-recovery checkpoint-heartbeat --hook-stdin --scope-key project:aquifer` |
85
+ | Preview a Codex UserPromptSubmit heartbeat hook install | `npx aquifer codex-recovery checkpoint-heartbeat-hook --scope-key project:aquifer --hooks-path "$CODEX_HOME/hooks.json" --json` |
80
86
  | Inspect storage health | `npx aquifer stats` |
81
87
  | Enrich pending sessions | `npx aquifer backfill` |
82
88
 
89
+ Timer synthesis output is candidate material until an operator applies it with
90
+ `--promote-candidates`; it does not become active curated memory from the
91
+ prompt or summary file alone.
92
+
93
+ Checkpoint output follows the same boundary. `operator checkpoint` plans from
94
+ finalized session summaries and only writes `checkpoint_runs` when you pass an
95
+ explicit reviewed synthesis summary with `--apply`. `codex-recovery
96
+ checkpoint-heartbeat` is the active-session hook heartbeat for Codex JSONL
97
+ files: it first checks a tiny local scheduler marker, reads the transcript only
98
+ when the time window is due, then writes local spool process material only when
99
+ the message threshold is also due. It does not print prompt text by default and
100
+ does not write DB memory.
101
+
83
102
  Need LLM summarization, the knowledge graph, OpenAI embeddings, reranking, or operations details? See [docs/setup.md](docs/setup.md) and [Environment Variables](#environment-variables).
84
103
 
85
104
  ---
@@ -133,6 +152,8 @@ Sessions, summaries, turn-level embeddings, entity graph — all live in one dat
133
152
  | Variable | Required? | Purpose | Example |
134
153
  |----------|-----------|---------|---------|
135
154
  | `DATABASE_URL` | Yes | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/mydb` |
155
+ | `AQUIFER_BACKEND` | No | Backend profile selector: `postgres` full backend or explicit degraded `local` starter | `postgres` |
156
+ | `AQUIFER_LOCAL_PATH` | No | Local starter JSON store path | `.aquifer/aquifer.local.json` |
136
157
  | `AQUIFER_SCHEMA` | No | PG schema name (default: `aquifer`) | `memory` |
137
158
  | `AQUIFER_TENANT_ID` | No | Multi-tenant key (default: `default`) | `my-app` |
138
159
  | `AQUIFER_EMBED_BASE_URL` | Yes (for recall) | Embedding API base URL | `http://localhost:11434/v1` |
@@ -151,6 +172,10 @@ Sessions, summaries, turn-level embeddings, entity graph — all live in one dat
151
172
  | `AQUIFER_MEMORY_SERVING_MODE` | No | Public serving mode: `legacy` default, or opt-in `curated` | `curated` |
152
173
  | `AQUIFER_MEMORY_ACTIVE_SCOPE_KEY` | No | Default active curated scope for recall/bootstrap | `project:aquifer` |
153
174
  | `AQUIFER_MEMORY_ACTIVE_SCOPE_PATH` | No | Ordered curated scope path for inheritance | `global,project:aquifer` |
175
+ | `AQUIFER_CODEX_CHECKPOINT_CHECK_INTERVAL_MINUTES` | No | Active Codex checkpoint heartbeat time gate (default: `10`) | `10` |
176
+ | `AQUIFER_CODEX_CHECKPOINT_EVERY_MESSAGES` | No | Active Codex checkpoint message delta gate (default: `20`) | `20` |
177
+ | `AQUIFER_CODEX_CHECKPOINT_EVERY_USER_MESSAGES` | No | Optional user-message delta gate | `10` |
178
+ | `AQUIFER_CODEX_CHECKPOINT_QUIET_MS` | No | Quiet period before reading due transcripts (default: `3000`) | `3000` |
154
179
  | `AQUIFER_MIGRATIONS_MODE` | No | Startup handshake mode: `apply` (default), `check`, `off` | `apply` |
155
180
  | `AQUIFER_MIGRATION_LOCK_TIMEOUT_MS` | No | Advisory-lock wait before `AQ_MIGRATION_LOCK_TIMEOUT` (default 30000) | `30000` |
156
181
  | `AQUIFER_INSIGHTS_DEDUP_MODE` | No | Insights semantic dedup mode: `off` (default), `shadow`, `enforce` — env wins over code for this field only, so operators can kill-switch without redeploy | `shadow` |
@@ -213,6 +238,53 @@ Add to your project's `.claude.json` or user-level MCP config:
213
238
 
214
239
  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`.
215
240
 
241
+ For Codex long sessions, Aquifer exposes a UserPromptSubmit-friendly heartbeat
242
+ command instead of installing a daemon:
243
+
244
+ ```bash
245
+ npx aquifer codex-recovery checkpoint-heartbeat \
246
+ --hook-stdin \
247
+ --scope-key project:aquifer
248
+ ```
249
+
250
+ Run that from a host hook with Codex hook JSON on stdin. The heartbeat uses a
251
+ time-first gate: if the local marker says the next check is not due, it exits
252
+ without validating or reading the transcript. When due, it validates the
253
+ `transcript_path` realpath under the Codex sessions directory, waits for the
254
+ quiet period, checks the configured message delta, and writes a local spool file
255
+ for later review. Scheduler, claim, and spool files live under the Codex state
256
+ directory by default; they are process-control files, not DB memory.
257
+
258
+ Heartbeat policy resolves as command flags first, then Aquifer env/config, then
259
+ defaults. The default policy is 10 minutes, 20 safe messages, no user-message
260
+ gate, 3000 ms quiet period, and 60000 ms claim TTL. In config files this lives at
261
+ `codex.checkpoint`:
262
+
263
+ ```json
264
+ {
265
+ "codex": {
266
+ "checkpoint": {
267
+ "checkIntervalMinutes": 10,
268
+ "everyMessages": 20,
269
+ "quietMs": 3000
270
+ }
271
+ }
272
+ }
273
+ ```
274
+
275
+ To prepare the Codex hook entry, generate or apply the merged `hooks.json`:
276
+
277
+ ```bash
278
+ npx aquifer codex-recovery checkpoint-heartbeat-hook \
279
+ --scope-key project:aquifer \
280
+ --hooks-path "$CODEX_HOME/hooks.json" \
281
+ --json
282
+ ```
283
+
284
+ The hook installer is dry-run by default. Add `--apply` only after reviewing the
285
+ merged `UserPromptSubmit` command. `codex-recovery doctor --json` reports whether
286
+ the heartbeat hook is present.
287
+
216
288
  ### OpenClaw
217
289
 
218
290
  Add to `openclaw.json` under `mcp.servers`:
package/README_CN.md CHANGED
@@ -111,6 +111,23 @@ Claude Code、Claude Desktop 或任何支持 MCP 的 client——放进 `.mcp.js
111
111
 
112
112
  第一轮 rollout 先保持 `AQUIFER_MEMORY_SERVING_MODE=legacy`。只有在你要让 `session_recall` 和 `session_bootstrap` 提供 active curated memory 时,才切到 `curated`;`evidence_recall` 会保留为显式 audit/debug 路径。要 rollback 只要把 env 或 config 切回 `legacy`。
113
113
 
114
+ ### Common commands
115
+
116
+ | Goal | Command |
117
+ |---|---|
118
+ | Verify setup | `npx aquifer quickstart` |
119
+ | Start the MCP server | `npx aquifer mcp` |
120
+ | Search memory manually | `npx aquifer recall "auth middleware"` |
121
+ | Plan curated memory compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
122
+ | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
123
+ | Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
124
+ | Inspect storage health | `npx aquifer stats` |
125
+ | Enrich pending sessions | `npx aquifer backfill` |
126
+
127
+ Timer synthesis output is candidate material until an operator applies it with
128
+ `--promote-candidates`; it does not become active curated memory from the
129
+ prompt or summary file alone.
130
+
114
131
  需要 LLM 摘要、知识图谱、OpenAI embedding 或 reranker?往下看 [环境变量](#环境变量) 和 [docs/setup.md](docs/setup.md)。
115
132
 
116
133
  ---
package/README_TW.md CHANGED
@@ -77,9 +77,13 @@ Claude Code、Claude Desktop 或任何支援 MCP 的 client——放進 `.mcp.js
77
77
  | 啟動 MCP server | `npx aquifer mcp` |
78
78
  | 手動查記憶 | `npx aquifer recall "auth middleware"` |
79
79
  | 規劃 curated memory 壓縮 | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
80
+ | 產生 timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
81
+ | 套用已審核的 timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
80
82
  | 看儲存狀態 | `npx aquifer stats` |
81
83
  | 補跑 pending session | `npx aquifer backfill` |
82
84
 
85
+ Timer synthesis output 在 operator 用 `--promote-candidates` apply 前都只是 candidate material;光有 prompt 或 summary file 不會變成 active curated memory。
86
+
83
87
  需要 LLM 摘要、知識圖譜、OpenAI embedding、reranker 或維運細節,就往下看 [環境變數](#環境變數) 跟 [docs/setup.md](docs/setup.md)。
84
88
 
85
89
  ---
@@ -1,4 +1,14 @@
1
1
  {
2
+ "storage": {
3
+ "backend": "postgres",
4
+ "postgres": {
5
+ "url": "postgresql://user:password@localhost:5432/mydb",
6
+ "max": 10
7
+ },
8
+ "local": {
9
+ "path": ".aquifer/aquifer.local.json"
10
+ }
11
+ },
2
12
  "db": {
3
13
  "url": "postgresql://user:password@localhost:5432/mydb",
4
14
  "max": 10
@@ -14,6 +24,15 @@
14
24
  "activeScopeKey": null,
15
25
  "activeScopePath": null
16
26
  },
27
+ "codex": {
28
+ "checkpoint": {
29
+ "checkIntervalMinutes": 10,
30
+ "everyMessages": 20,
31
+ "everyUserMessages": null,
32
+ "quietMs": 3000,
33
+ "claimTtlMs": 60000
34
+ }
35
+ },
17
36
  "embed": {
18
37
  "baseUrl": "http://localhost:11434/v1",
19
38
  "model": "bge-m3",
package/consumers/cli.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * aquifer recall <query> [options] Search sessions
11
11
  * aquifer backfill [options] Enrich pending sessions
12
12
  * aquifer stats [options] Show database statistics
13
+ * aquifer backend-info [--json] Show selected backend capabilities
13
14
  * aquifer export [options] Export sessions
14
15
  * aquifer operator ... Run operator-safe consolidation jobs
15
16
  * aquifer mcp Start MCP server
@@ -17,7 +18,9 @@
17
18
 
18
19
  const fs = require('fs');
19
20
  const { createAquiferFromConfig } = require('./shared/factory');
21
+ const { loadConfig } = require('./shared/config');
20
22
  const { formatRecallResults } = require('./shared/recall-format');
23
+ const { backendCapabilities } = require('../core/backends/capabilities');
21
24
 
22
25
  function formatDate(value, fallback) {
23
26
  if (!value) return fallback;
@@ -51,6 +54,38 @@ function parseCsvList(value) {
51
54
  return parts.length > 0 ? parts : undefined;
52
55
  }
53
56
 
57
+ function readJsonFlagValue(value, label) {
58
+ if (!value || value === true) {
59
+ throw new Error(`${label} requires a JSON value`);
60
+ }
61
+ try {
62
+ return JSON.parse(String(value));
63
+ } catch (err) {
64
+ throw new Error(`${label} must be valid JSON: ${err.message}`);
65
+ }
66
+ }
67
+
68
+ function readJsonFlagFile(filePath, label) {
69
+ if (!filePath || filePath === true) {
70
+ throw new Error(`${label} requires a file path`);
71
+ }
72
+ try {
73
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
74
+ } catch (err) {
75
+ throw new Error(`${label} must point to valid JSON: ${err.message}`);
76
+ }
77
+ }
78
+
79
+ function readSynthesisSummaryFromFlags(flags = {}) {
80
+ if (flags['synthesis-summary-file']) {
81
+ return readJsonFlagFile(flags['synthesis-summary-file'], '--synthesis-summary-file');
82
+ }
83
+ if (flags['synthesis-summary']) {
84
+ return readJsonFlagValue(flags['synthesis-summary'], '--synthesis-summary');
85
+ }
86
+ return undefined;
87
+ }
88
+
54
89
  function hasQuickstartEmbedConfig(env) {
55
90
  return !!(
56
91
  env.EMBED_PROVIDER
@@ -77,6 +112,7 @@ function buildQuickstartSetupHints(env, detected, err) {
77
112
  if (!hasDb) {
78
113
  hints.push('If you expect local defaults, make sure PostgreSQL is running on localhost:5432.');
79
114
  hints.push('Otherwise set DATABASE_URL or AQUIFER_DB_URL explicitly and run quickstart again.');
115
+ hints.push('To inspect the degraded local starter profile without PostgreSQL, run `AQUIFER_BACKEND=local aquifer backend-info --json`.');
80
116
  }
81
117
  return hints;
82
118
  }
@@ -92,6 +128,7 @@ function buildQuickstartSetupHints(env, detected, err) {
92
128
  if (!hasDb) hints.push('PostgreSQL was not autodetected and no DATABASE_URL is set.');
93
129
  if (!hasEmbed) hints.push('No embedding provider was autodetected and no embed env is set.');
94
130
  hints.push('Try `docker compose up -d`, then run `npx aquifer quickstart` again.');
131
+ hints.push('To inspect the degraded local starter profile without PostgreSQL, run `AQUIFER_BACKEND=local aquifer backend-info --json`.');
95
132
  }
96
133
 
97
134
  hints.push(`Raw error: ${message}`);
@@ -144,7 +181,8 @@ function parseArgs(argv) {
144
181
  'verdict', 'feedback-type', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars',
145
182
  'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
146
183
  'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
147
- 'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
184
+ 'scope-id', 'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
185
+ 'synthesis-summary', 'synthesis-summary-file', 'min-finalizations', 'checkpoint-key',
148
186
  ]);
149
187
  for (let i = 0; i < argv.length; i++) {
150
188
  if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
@@ -348,15 +386,122 @@ async function cmdStats(aquifer, args) {
348
386
  if (args.flags.json) {
349
387
  console.log(JSON.stringify(stats, null, 2));
350
388
  } else {
389
+ console.log(`Backend: ${stats.backendKind || 'unknown'} (${stats.backendProfile || 'unknown'})`);
390
+ console.log(`Serving mode: ${stats.serving?.mode || 'legacy'}`);
391
+ console.log(`Active scope: ${stats.serving?.activeScopePath?.join(' > ') || stats.serving?.activeScopeKey || 'none'}`);
351
392
  console.log(`Sessions: ${stats.sessionTotal} (${Object.entries(stats.sessions).map(([k, v]) => `${k}: ${v}`).join(', ')})`);
352
393
  console.log(`Summaries: ${stats.summaries}`);
353
394
  console.log(`Turn embeddings: ${stats.turnEmbeddings}`);
354
395
  console.log(`Entities: ${stats.entities}`);
396
+ if (stats.memoryRecords) {
397
+ console.log(`Memory records: ${stats.memoryRecords.total} (${stats.memoryRecords.active} active, ${stats.memoryRecords.visibleInRecall} recall-visible, ${stats.memoryRecords.visibleInBootstrap} bootstrap-visible)`);
398
+ if (stats.memoryRecords.latest) console.log(`Memory record range: ${formatDate(stats.memoryRecords.earliest, '?')} — ${formatDate(stats.memoryRecords.latest, '?')}`);
399
+ }
400
+ if (stats.sessionFinalizations?.available) {
401
+ const statusText = Object.entries(stats.sessionFinalizations.statuses || {})
402
+ .map(([status, count]) => `${status}: ${count}`)
403
+ .join(', ') || 'none';
404
+ console.log(`Session finalizations: ${stats.sessionFinalizations.total} (${statusText})`);
405
+ if (stats.sessionFinalizations.latestFinalizedAt) console.log(`Latest finalization: ${formatDate(stats.sessionFinalizations.latestFinalizedAt, '?')}`);
406
+ }
355
407
  if (stats.earliest) console.log(`Range: ${formatDate(stats.earliest, '?')} — ${formatDate(stats.latest, '?')}`);
408
+ if ((stats.serving?.mode || 'legacy') !== 'curated') {
409
+ console.log('Warning: legacy serving returns session/evidence material; configure curated serving with an active scope for current-memory answers.');
410
+ }
411
+ }
412
+ }
413
+
414
+ function selectedBackendInfo(opts = {}) {
415
+ const config = loadConfig(opts);
416
+ const backendKind = config.storage.backend;
417
+ const capabilities = backendCapabilities(backendKind);
418
+ return {
419
+ backendKind,
420
+ backendProfile: capabilities.profile,
421
+ label: capabilities.label,
422
+ summary: capabilities.summary,
423
+ capabilities: capabilities.capabilities,
424
+ storage: {
425
+ localPath: config.storage.local.path,
426
+ postgresUrlConfigured: Boolean(config.storage.postgres.url || config.db.url),
427
+ },
428
+ upgradeHint: capabilities.upgradeHint,
429
+ };
430
+ }
431
+
432
+ async function cmdBackendInfo(args) {
433
+ const info = selectedBackendInfo();
434
+ if (args.flags.json) {
435
+ console.log(JSON.stringify(info, null, 2));
436
+ return;
437
+ }
438
+
439
+ console.log(`Backend: ${info.label} (${info.backendKind}/${info.backendProfile})`);
440
+ console.log(info.summary);
441
+ console.log(`PostgreSQL URL configured: ${info.storage.postgresUrlConfigured ? 'yes' : 'no'}`);
442
+ if (info.backendKind === 'local') {
443
+ console.log(`Local path: ${info.storage.localPath}`);
444
+ }
445
+ if (info.upgradeHint) {
446
+ console.log(`Upgrade: ${info.upgradeHint}`);
447
+ }
448
+ console.log('Capabilities:');
449
+ for (const [name, status] of Object.entries(info.capabilities)) {
450
+ console.log(` ${name}: ${status}`);
356
451
  }
357
452
  }
358
453
 
454
+ async function cmdLocalQuickstart(aquifer) {
455
+ const cfg = aquifer.getConfig();
456
+ console.log('Aquifer quickstart — verifying local starter backend.\n');
457
+
458
+ console.log('1/5 Preparing local store...');
459
+ await aquifer.init();
460
+ console.log(` OK — ${cfg.backendPath}\n`);
461
+
462
+ const sessionId = `quickstart-local-${Date.now()}`;
463
+ console.log('2/5 Committing test session...');
464
+ await aquifer.commit(sessionId, [
465
+ { role: 'user', content: 'Aquifer local starter can store a session without PostgreSQL.' },
466
+ { role: 'assistant', content: 'Local starter uses JSON persistence and degraded lexical recall.' },
467
+ ], { agentId: 'quickstart', source: 'quickstart' });
468
+ console.log(' OK\n');
469
+
470
+ console.log('3/5 Checking degraded enrich path...');
471
+ const enrichResult = await aquifer.enrich(sessionId, {
472
+ agentId: 'quickstart',
473
+ skipSummary: true,
474
+ skipEntities: true,
475
+ });
476
+ console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded (local starter is lexical only)\n`);
477
+
478
+ console.log('4/5 Recalling "JSON persistence"...');
479
+ const results = await aquifer.recall('JSON persistence', { agentId: 'quickstart', limit: 3 });
480
+ if (results.length === 0) {
481
+ printQuickstartFailure('local starter could not recall its own test session.', [
482
+ 'The write step succeeded, but lexical recall returned no matches.',
483
+ ]);
484
+ process.exitCode = 1;
485
+ return;
486
+ }
487
+ console.log(` OK — ${results.length} result(s), top score: ${results[0].score?.toFixed(3)}\n`);
488
+
489
+ console.log('5/5 Cleaning up test data...');
490
+ if (typeof aquifer.deleteSession === 'function') {
491
+ await aquifer.deleteSession(sessionId, { agentId: 'quickstart' });
492
+ }
493
+ console.log(' OK\n');
494
+
495
+ console.log('✓ Aquifer local starter is working.');
496
+ console.log(' This backend is degraded: use PostgreSQL quickstart for semantic recall, migrations, curated memory, and operator workflows.');
497
+ }
498
+
359
499
  async function cmdQuickstart(aquifer) {
500
+ if (aquifer.getConfig().backendKind === 'local') {
501
+ await cmdLocalQuickstart(aquifer);
502
+ return;
503
+ }
504
+
360
505
  console.log('Aquifer quickstart — verifying end-to-end setup.\n');
361
506
 
362
507
  // 1. Migrate
@@ -481,6 +626,51 @@ async function cmdOperator(aquifer, args) {
481
626
  const operatorVerb = args._[1] || 'compaction';
482
627
  const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
483
628
 
629
+ if (operatorVerb === 'checkpoint') {
630
+ const synthesisSummary = readSynthesisSummaryFromFlags(args.flags);
631
+ const result = await aquifer.checkpoints.runProducer({
632
+ scopeId: args.flags['scope-id'] || undefined,
633
+ scopeKind: args.flags['scope-kind'] || undefined,
634
+ scopeKey: args.flags['scope-key'] || undefined,
635
+ source: args.flags.source || undefined,
636
+ agentId: args.flags['agent-id'] || undefined,
637
+ minFinalizations: args.flags['min-finalizations']
638
+ ? parsePositiveInt(args.flags['min-finalizations'], 10)
639
+ : undefined,
640
+ limit: parsePositiveInt(args.flags.limit, 50),
641
+ checkpointKey: args.flags['checkpoint-key'] || undefined,
642
+ policyVersion: args.flags['policy-version'] || undefined,
643
+ force: args.flags.force === true,
644
+ apply: args.flags.apply === true,
645
+ finalize: args.flags.finalize === true,
646
+ includeSynthesisPrompt: args.flags['include-synthesis-prompt'] === true,
647
+ synthesisSummary,
648
+ });
649
+
650
+ if (args.flags.json) {
651
+ console.log(JSON.stringify(result, null, 2));
652
+ return;
653
+ }
654
+
655
+ console.log(result.due
656
+ ? `Checkpoint due for ${result.scope.scopeKey}: ${result.sourceFinalizationCount} finalized source(s).`
657
+ : `Checkpoint not ready for ${result.scope.scopeKey}: ${result.sourceFinalizationCount}/${result.minFinalizations} finalized source(s).`);
658
+ if (result.range) {
659
+ console.log(`Range: finalization ${result.range.fromFinalizationIdExclusive} -> ${result.range.toFinalizationIdInclusive}`);
660
+ }
661
+ if (result.synthesisPrompt) {
662
+ console.log('\nCheckpoint synthesis prompt:\n');
663
+ console.log(result.synthesisPrompt);
664
+ }
665
+ if (result.run) {
666
+ console.log(`Run: #${result.run.id} status=${result.run.status}`);
667
+ } else if (result.runInput) {
668
+ console.log(`Run input prepared: status=${result.runInput.status}`);
669
+ console.log('Mode: dry-run only. Re-run with --apply and an explicit synthesis summary to write checkpoint_runs.');
670
+ }
671
+ return;
672
+ }
673
+
484
674
  if (operatorVerb === 'archive-distill') {
485
675
  const inputPath = args.flags.input || args._[2];
486
676
  if (!inputPath) {
@@ -511,13 +701,14 @@ async function cmdOperator(aquifer, args) {
511
701
  }
512
702
 
513
703
  if (operatorVerb !== 'compaction' && operatorVerb !== 'compact' && !cadenceVerbs.has(operatorVerb)) {
514
- console.error('Usage: aquifer operator <compaction|archive-distill> [...]');
704
+ console.error('Usage: aquifer operator <compaction|checkpoint|archive-distill> [...]');
515
705
  process.exit(1);
516
706
  }
517
707
 
518
708
  const cadence = args.flags.cadence
519
709
  || (cadenceVerbs.has(operatorVerb) ? operatorVerb : args._[2])
520
710
  || 'manual';
711
+ const synthesisSummary = readSynthesisSummaryFromFlags(args.flags);
521
712
  const result = await aquifer.memory.consolidation.runJob({
522
713
  job: 'compaction',
523
714
  cadence,
@@ -531,8 +722,17 @@ async function cmdOperator(aquifer, args) {
531
722
  : undefined,
532
723
  snapshotAsOf: args.flags['snapshot-as-of'] || undefined,
533
724
  scopeKeys: parseCsvList(args.flags['scope-keys'] || args.flags['scope-key']),
725
+ scopeKind: args.flags['scope-kind'] || undefined,
726
+ scopeKey: args.flags['scope-key'] || undefined,
727
+ contextKey: args.flags['context-key'] || undefined,
728
+ topicKey: args.flags['topic-key'] || undefined,
729
+ activeScopeKey: args.flags['active-scope-key'] || undefined,
730
+ activeScopePath: parseScopePath(args.flags['active-scope-path']),
534
731
  limit: parsePositiveInt(args.flags.limit, 1000),
535
732
  apply: args.flags.apply === true,
733
+ promoteCandidates: args.flags['promote-candidates'] === true,
734
+ includeSynthesisPrompt: args.flags['include-synthesis-prompt'] === true,
735
+ synthesisSummary,
536
736
  });
537
737
 
538
738
  if (args.flags.json) {
@@ -542,7 +742,14 @@ async function cmdOperator(aquifer, args) {
542
742
 
543
743
  console.log(`${result.dryRun ? 'Planned' : 'Executed'} ${result.cadence} compaction window ${result.periodStart} -> ${result.periodEnd}`);
544
744
  console.log(`Snapshot: ${result.snapshotCount} active rows${result.snapshotTruncated ? ' (snapshot limit reached)' : ''}`);
545
- console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} aggregate candidates`);
745
+ console.log(`Plan: ${result.plan.statusUpdates.length} lifecycle updates, ${result.plan.candidates.length} candidates`);
746
+ if (result.synthesisPrompt) {
747
+ console.log('\nSynthesis prompt:\n');
748
+ console.log(result.synthesisPrompt);
749
+ }
750
+ if (result.promotionReview) {
751
+ console.log(`\n${result.promotionReview}`);
752
+ }
546
753
  if (result.dryRun) {
547
754
  console.log('Mode: dry-run only. Re-run with --apply to write compaction_runs and lifecycle changes.');
548
755
  return;
@@ -600,6 +807,7 @@ async function main() {
600
807
  Commands:
601
808
  quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
602
809
  migrate Run database migrations
810
+ backend-info Show selected backend capabilities without connecting to a database
603
811
  recall <query> Search sessions (requires embed config)
604
812
  evidence-recall <query> Search legacy session/evidence plane explicitly
605
813
  feedback Record trust feedback on a session
@@ -611,6 +819,7 @@ Commands:
611
819
  stats Show database statistics
612
820
  export Export sessions as JSONL
613
821
  bootstrap Show recent session context (for new session start)
822
+ codex-recovery ... Inspect or run Codex recovery/checkpoint flows
614
823
  ingest-opencode Import sessions from OpenCode's local SQLite DB
615
824
  mcp Start MCP server
616
825
 
@@ -642,7 +851,15 @@ Options:
642
851
  --period-start ISO Compaction window start
643
852
  --period-end ISO Compaction window end
644
853
  --apply Apply compaction; default is dry-run
854
+ --promote-candidates Promote compaction/synthesis candidates when applying
855
+ --include-synthesis-prompt Include timer synthesis prompt in operator output
856
+ --synthesis-summary JSON Timer synthesis summary JSON to attach to a compaction plan
857
+ --synthesis-summary-file P Read timer synthesis summary JSON from file
858
+ --min-finalizations N Min finalized session summaries before checkpoint proposal
859
+ --checkpoint-key KEY Explicit checkpoint key when applying checkpoint producer output
645
860
  --scope-key A,B Limit compaction snapshot to specific scope keys
861
+ --scope-id ID Target scope row id for checkpoint producer
862
+ --scope-kind KIND Explicit synthesis target scope kind
646
863
  --snapshot-as-of ISO Read active snapshot as of a specific instant
647
864
  --claim-lease-seconds N Override compaction apply lease
648
865
  --input PATH Archive distill input JSON path
@@ -652,7 +869,14 @@ Options:
652
869
 
653
870
  Operator examples:
654
871
  aquifer operator compaction daily --json
872
+ aquifer operator compaction daily --include-synthesis-prompt --json
655
873
  aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
874
+ aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json
875
+ aquifer operator checkpoint --scope-key project:aquifer --min-finalizations 10 --include-synthesis-prompt --json
876
+ aquifer operator checkpoint --scope-id 7 --synthesis-summary-file /tmp/checkpoint-summary.json --apply --finalize --json
877
+ AQUIFER_BACKEND=local aquifer backend-info --json
878
+ aquifer codex-recovery checkpoint-heartbeat --hook-stdin --scope-key project:aquifer
879
+ aquifer codex-recovery checkpoint-heartbeat-hook --scope-key project:aquifer --hooks-path "$CODEX_HOME/hooks.json" --json
656
880
  aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
657
881
  process.exit(0);
658
882
  }
@@ -661,6 +885,11 @@ Operator examples:
661
885
  const args = parseArgs(argv);
662
886
  let quickstartDetected = {};
663
887
 
888
+ if (command === 'codex-recovery') {
889
+ await require('../scripts/codex-recovery').main(argv.slice(1));
890
+ return;
891
+ }
892
+
664
893
  // MCP: delegate to mcp.js
665
894
  if (command === 'mcp') {
666
895
  require('./mcp').main().catch(err => {
@@ -680,6 +909,14 @@ Operator examples:
680
909
  return;
681
910
  }
682
911
 
912
+ if (command === 'backend-info') {
913
+ if (args.flags.config) {
914
+ process.env.AQUIFER_CONFIG = args.flags.config;
915
+ }
916
+ await cmdBackendInfo(args);
917
+ return;
918
+ }
919
+
683
920
  // All other commands need an Aquifer instance
684
921
  const configOverrides = {};
685
922
  if (args.flags.config) {
@@ -692,15 +929,18 @@ Operator examples:
692
929
  // Production commands (migrate, mcp, recall, ...) stay strict — they expect
693
930
  // the operator to have set env explicitly.
694
931
  if (command === 'quickstart') {
695
- const { autodetectForQuickstart } = require('./shared/autodetect');
696
- quickstartDetected = await autodetectForQuickstart(process.env);
697
- if (Object.keys(quickstartDetected).length > 0) {
698
- console.log('Autodetected localhost services (env not set):');
699
- for (const [k, v] of Object.entries(quickstartDetected)) {
700
- console.log(` ${k}=${v}`);
701
- process.env[k] = v;
932
+ const selected = loadConfig({ overrides: configOverrides });
933
+ if (selected.storage.backend !== 'local') {
934
+ const { autodetectForQuickstart } = require('./shared/autodetect');
935
+ quickstartDetected = await autodetectForQuickstart(process.env);
936
+ if (Object.keys(quickstartDetected).length > 0) {
937
+ console.log('Autodetected localhost services (env not set):');
938
+ for (const [k, v] of Object.entries(quickstartDetected)) {
939
+ console.log(` ${k}=${v}`);
940
+ process.env[k] = v;
941
+ }
942
+ console.log(' Export these in your shell (or MCP client env) to make them permanent.\n');
702
943
  }
703
- console.log(' Export these in your shell (or MCP client env) to make them permanent.\n');
704
944
  }
705
945
  }
706
946
 
@@ -772,7 +1012,14 @@ Operator examples:
772
1012
  }
773
1013
 
774
1014
  // Export for testing; execute only when run directly
775
- module.exports = { parseArgs };
1015
+ module.exports = {
1016
+ parseArgs,
1017
+ selectedBackendInfo,
1018
+ cmdBackendInfo,
1019
+ cmdLocalQuickstart,
1020
+ cmdOperator,
1021
+ readSynthesisSummaryFromFlags,
1022
+ };
776
1023
 
777
1024
  if (require.main === module) {
778
1025
  main().catch(err => {