@shadowforge0/aquifer-memory 1.7.0 → 1.8.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 (39) hide show
  1. package/.env.example +8 -0
  2. package/README.md +66 -0
  3. package/aquifer.config.example.json +19 -0
  4. package/consumers/cli.js +217 -14
  5. package/consumers/codex-active-checkpoint.js +186 -0
  6. package/consumers/codex-current-memory.js +106 -0
  7. package/consumers/codex-handoff.js +442 -3
  8. package/consumers/codex.js +164 -107
  9. package/consumers/mcp.js +144 -6
  10. package/consumers/shared/config.js +60 -1
  11. package/consumers/shared/factory.js +10 -3
  12. package/core/aquifer.js +351 -840
  13. package/core/backends/capabilities.js +89 -0
  14. package/core/backends/local.js +430 -0
  15. package/core/legacy-bootstrap.js +140 -0
  16. package/core/mcp-manifest.js +66 -2
  17. package/core/memory-promotion.js +157 -26
  18. package/core/memory-recall.js +341 -22
  19. package/core/memory-records.js +128 -8
  20. package/core/memory-serving.js +132 -0
  21. package/core/postgres-migrations.js +533 -0
  22. package/core/public-session-filter.js +40 -0
  23. package/core/recall-runtime.js +115 -0
  24. package/core/scope-attribution.js +279 -0
  25. package/core/session-checkpoint-producer.js +412 -0
  26. package/core/session-checkpoints.js +432 -0
  27. package/core/session-finalization.js +82 -1
  28. package/core/storage-checkpoints.js +546 -0
  29. package/core/storage.js +121 -8
  30. package/docs/setup.md +22 -0
  31. package/package.json +8 -4
  32. package/schema/014-v1-checkpoint-runs.sql +349 -0
  33. package/schema/015-v1-evidence-items.sql +92 -0
  34. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  35. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  36. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  37. package/scripts/codex-checkpoint-commands.js +464 -0
  38. package/scripts/codex-checkpoint-runtime.js +520 -0
  39. package/scripts/codex-recovery.js +105 -0
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,11 +74,15 @@ 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` |
80
81
  | Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
81
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` |
82
86
  | Inspect storage health | `npx aquifer stats` |
83
87
  | Enrich pending sessions | `npx aquifer backfill` |
84
88
 
@@ -86,6 +90,15 @@ Timer synthesis output is candidate material until an operator applies it with
86
90
  `--promote-candidates`; it does not become active curated memory from the
87
91
  prompt or summary file alone.
88
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
+
89
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).
90
103
 
91
104
  ---
@@ -139,6 +152,8 @@ Sessions, summaries, turn-level embeddings, entity graph — all live in one dat
139
152
  | Variable | Required? | Purpose | Example |
140
153
  |----------|-----------|---------|---------|
141
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` |
142
157
  | `AQUIFER_SCHEMA` | No | PG schema name (default: `aquifer`) | `memory` |
143
158
  | `AQUIFER_TENANT_ID` | No | Multi-tenant key (default: `default`) | `my-app` |
144
159
  | `AQUIFER_EMBED_BASE_URL` | Yes (for recall) | Embedding API base URL | `http://localhost:11434/v1` |
@@ -157,6 +172,10 @@ Sessions, summaries, turn-level embeddings, entity graph — all live in one dat
157
172
  | `AQUIFER_MEMORY_SERVING_MODE` | No | Public serving mode: `legacy` default, or opt-in `curated` | `curated` |
158
173
  | `AQUIFER_MEMORY_ACTIVE_SCOPE_KEY` | No | Default active curated scope for recall/bootstrap | `project:aquifer` |
159
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` |
160
179
  | `AQUIFER_MIGRATIONS_MODE` | No | Startup handshake mode: `apply` (default), `check`, `off` | `apply` |
161
180
  | `AQUIFER_MIGRATION_LOCK_TIMEOUT_MS` | No | Advisory-lock wait before `AQ_MIGRATION_LOCK_TIMEOUT` (default 30000) | `30000` |
162
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` |
@@ -219,6 +238,53 @@ Add to your project's `.claude.json` or user-level MCP config:
219
238
 
220
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`.
221
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
+
222
288
  ### OpenClaw
223
289
 
224
290
  Add to `openclaw.json` under `mcp.servers`:
@@ -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;
@@ -109,6 +112,7 @@ function buildQuickstartSetupHints(env, detected, err) {
109
112
  if (!hasDb) {
110
113
  hints.push('If you expect local defaults, make sure PostgreSQL is running on localhost:5432.');
111
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`.');
112
116
  }
113
117
  return hints;
114
118
  }
@@ -124,6 +128,7 @@ function buildQuickstartSetupHints(env, detected, err) {
124
128
  if (!hasDb) hints.push('PostgreSQL was not autodetected and no DATABASE_URL is set.');
125
129
  if (!hasEmbed) hints.push('No embedding provider was autodetected and no embed env is set.');
126
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`.');
127
132
  }
128
133
 
129
134
  hints.push(`Raw error: ${message}`);
@@ -176,8 +181,8 @@ function parseArgs(argv) {
176
181
  'verdict', 'feedback-type', 'note', 'db', 'since', 'min-messages', 'lookback-days', 'max-chars',
177
182
  'out', 'active-scope-key', 'active-scope-path', 'cadence', 'period-start', 'period-end',
178
183
  'policy-version', 'worker-id', 'apply-token', 'claim-lease-seconds', 'snapshot-as-of',
179
- 'scope-key', 'scope-keys', 'scope-kind', 'context-key', 'topic-key', 'authority', 'input',
180
- 'synthesis-summary', 'synthesis-summary-file',
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',
181
186
  ]);
182
187
  for (let i = 0; i < argv.length; i++) {
183
188
  if (argv[i] === '--') { args._.push(...argv.slice(i + 1)); break; }
@@ -199,7 +204,29 @@ function parseArgs(argv) {
199
204
  // Commands
200
205
  // ---------------------------------------------------------------------------
201
206
 
202
- async function cmdMigrate(aquifer) {
207
+ async function cmdMigrate(aquifer, args = { flags: {} }) {
208
+ if (args.flags && args.flags.json) {
209
+ const notices = [];
210
+ const originalStderrWrite = process.stderr.write;
211
+ process.stderr.write = function writeCapturedStderr(chunk, encoding, callback) {
212
+ notices.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
213
+ if (typeof encoding === 'function') encoding();
214
+ if (typeof callback === 'function') callback();
215
+ return true;
216
+ };
217
+ try {
218
+ await aquifer.migrate();
219
+ } finally {
220
+ process.stderr.write = originalStderrWrite;
221
+ }
222
+ console.log(JSON.stringify({
223
+ ok: true,
224
+ migrated: true,
225
+ notices: notices.join('').split(/\r?\n/).map(line => line.trim()).filter(Boolean),
226
+ }, null, 2));
227
+ return;
228
+ }
229
+
203
230
  await aquifer.migrate();
204
231
  console.log('Migrations applied successfully.');
205
232
  }
@@ -381,15 +408,122 @@ async function cmdStats(aquifer, args) {
381
408
  if (args.flags.json) {
382
409
  console.log(JSON.stringify(stats, null, 2));
383
410
  } else {
411
+ console.log(`Backend: ${stats.backendKind || 'unknown'} (${stats.backendProfile || 'unknown'})`);
412
+ console.log(`Serving mode: ${stats.serving?.mode || 'legacy'}`);
413
+ console.log(`Active scope: ${stats.serving?.activeScopePath?.join(' > ') || stats.serving?.activeScopeKey || 'none'}`);
384
414
  console.log(`Sessions: ${stats.sessionTotal} (${Object.entries(stats.sessions).map(([k, v]) => `${k}: ${v}`).join(', ')})`);
385
415
  console.log(`Summaries: ${stats.summaries}`);
386
416
  console.log(`Turn embeddings: ${stats.turnEmbeddings}`);
387
417
  console.log(`Entities: ${stats.entities}`);
418
+ if (stats.memoryRecords) {
419
+ console.log(`Memory records: ${stats.memoryRecords.total} (${stats.memoryRecords.active} active, ${stats.memoryRecords.visibleInRecall} recall-visible, ${stats.memoryRecords.visibleInBootstrap} bootstrap-visible)`);
420
+ if (stats.memoryRecords.latest) console.log(`Memory record range: ${formatDate(stats.memoryRecords.earliest, '?')} — ${formatDate(stats.memoryRecords.latest, '?')}`);
421
+ }
422
+ if (stats.sessionFinalizations?.available) {
423
+ const statusText = Object.entries(stats.sessionFinalizations.statuses || {})
424
+ .map(([status, count]) => `${status}: ${count}`)
425
+ .join(', ') || 'none';
426
+ console.log(`Session finalizations: ${stats.sessionFinalizations.total} (${statusText})`);
427
+ if (stats.sessionFinalizations.latestFinalizedAt) console.log(`Latest finalization: ${formatDate(stats.sessionFinalizations.latestFinalizedAt, '?')}`);
428
+ }
388
429
  if (stats.earliest) console.log(`Range: ${formatDate(stats.earliest, '?')} — ${formatDate(stats.latest, '?')}`);
430
+ if ((stats.serving?.mode || 'legacy') !== 'curated') {
431
+ console.log('Warning: legacy serving returns session/evidence material; configure curated serving with an active scope for current-memory answers.');
432
+ }
433
+ }
434
+ }
435
+
436
+ function selectedBackendInfo(opts = {}) {
437
+ const config = loadConfig(opts);
438
+ const backendKind = config.storage.backend;
439
+ const capabilities = backendCapabilities(backendKind);
440
+ return {
441
+ backendKind,
442
+ backendProfile: capabilities.profile,
443
+ label: capabilities.label,
444
+ summary: capabilities.summary,
445
+ capabilities: capabilities.capabilities,
446
+ storage: {
447
+ localPath: config.storage.local.path,
448
+ postgresUrlConfigured: Boolean(config.storage.postgres.url || config.db.url),
449
+ },
450
+ upgradeHint: capabilities.upgradeHint,
451
+ };
452
+ }
453
+
454
+ async function cmdBackendInfo(args) {
455
+ const info = selectedBackendInfo();
456
+ if (args.flags.json) {
457
+ console.log(JSON.stringify(info, null, 2));
458
+ return;
459
+ }
460
+
461
+ console.log(`Backend: ${info.label} (${info.backendKind}/${info.backendProfile})`);
462
+ console.log(info.summary);
463
+ console.log(`PostgreSQL URL configured: ${info.storage.postgresUrlConfigured ? 'yes' : 'no'}`);
464
+ if (info.backendKind === 'local') {
465
+ console.log(`Local path: ${info.storage.localPath}`);
466
+ }
467
+ if (info.upgradeHint) {
468
+ console.log(`Upgrade: ${info.upgradeHint}`);
469
+ }
470
+ console.log('Capabilities:');
471
+ for (const [name, status] of Object.entries(info.capabilities)) {
472
+ console.log(` ${name}: ${status}`);
389
473
  }
390
474
  }
391
475
 
476
+ async function cmdLocalQuickstart(aquifer) {
477
+ const cfg = aquifer.getConfig();
478
+ console.log('Aquifer quickstart — verifying local starter backend.\n');
479
+
480
+ console.log('1/5 Preparing local store...');
481
+ await aquifer.init();
482
+ console.log(` OK — ${cfg.backendPath}\n`);
483
+
484
+ const sessionId = `quickstart-local-${Date.now()}`;
485
+ console.log('2/5 Committing test session...');
486
+ await aquifer.commit(sessionId, [
487
+ { role: 'user', content: 'Aquifer local starter can store a session without PostgreSQL.' },
488
+ { role: 'assistant', content: 'Local starter uses JSON persistence and degraded lexical recall.' },
489
+ ], { agentId: 'quickstart', source: 'quickstart' });
490
+ console.log(' OK\n');
491
+
492
+ console.log('3/5 Checking degraded enrich path...');
493
+ const enrichResult = await aquifer.enrich(sessionId, {
494
+ agentId: 'quickstart',
495
+ skipSummary: true,
496
+ skipEntities: true,
497
+ });
498
+ console.log(` OK — ${enrichResult.turnsEmbedded} turns embedded (local starter is lexical only)\n`);
499
+
500
+ console.log('4/5 Recalling "JSON persistence"...');
501
+ const results = await aquifer.recall('JSON persistence', { agentId: 'quickstart', limit: 3 });
502
+ if (results.length === 0) {
503
+ printQuickstartFailure('local starter could not recall its own test session.', [
504
+ 'The write step succeeded, but lexical recall returned no matches.',
505
+ ]);
506
+ process.exitCode = 1;
507
+ return;
508
+ }
509
+ console.log(` OK — ${results.length} result(s), top score: ${results[0].score?.toFixed(3)}\n`);
510
+
511
+ console.log('5/5 Cleaning up test data...');
512
+ if (typeof aquifer.deleteSession === 'function') {
513
+ await aquifer.deleteSession(sessionId, { agentId: 'quickstart' });
514
+ }
515
+ console.log(' OK\n');
516
+
517
+ console.log('✓ Aquifer local starter is working.');
518
+ console.log(' This backend is degraded: use PostgreSQL quickstart for semantic recall, migrations, curated memory, and operator workflows.');
519
+ }
520
+
392
521
  async function cmdQuickstart(aquifer) {
522
+ if (aquifer.getConfig().backendKind === 'local') {
523
+ await cmdLocalQuickstart(aquifer);
524
+ return;
525
+ }
526
+
393
527
  console.log('Aquifer quickstart — verifying end-to-end setup.\n');
394
528
 
395
529
  // 1. Migrate
@@ -514,6 +648,51 @@ async function cmdOperator(aquifer, args) {
514
648
  const operatorVerb = args._[1] || 'compaction';
515
649
  const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
516
650
 
651
+ if (operatorVerb === 'checkpoint') {
652
+ const synthesisSummary = readSynthesisSummaryFromFlags(args.flags);
653
+ const result = await aquifer.checkpoints.runProducer({
654
+ scopeId: args.flags['scope-id'] || undefined,
655
+ scopeKind: args.flags['scope-kind'] || undefined,
656
+ scopeKey: args.flags['scope-key'] || undefined,
657
+ source: args.flags.source || undefined,
658
+ agentId: args.flags['agent-id'] || undefined,
659
+ minFinalizations: args.flags['min-finalizations']
660
+ ? parsePositiveInt(args.flags['min-finalizations'], 10)
661
+ : undefined,
662
+ limit: parsePositiveInt(args.flags.limit, 50),
663
+ checkpointKey: args.flags['checkpoint-key'] || undefined,
664
+ policyVersion: args.flags['policy-version'] || undefined,
665
+ force: args.flags.force === true,
666
+ apply: args.flags.apply === true,
667
+ finalize: args.flags.finalize === true,
668
+ includeSynthesisPrompt: args.flags['include-synthesis-prompt'] === true,
669
+ synthesisSummary,
670
+ });
671
+
672
+ if (args.flags.json) {
673
+ console.log(JSON.stringify(result, null, 2));
674
+ return;
675
+ }
676
+
677
+ console.log(result.due
678
+ ? `Checkpoint due for ${result.scope.scopeKey}: ${result.sourceFinalizationCount} finalized source(s).`
679
+ : `Checkpoint not ready for ${result.scope.scopeKey}: ${result.sourceFinalizationCount}/${result.minFinalizations} finalized source(s).`);
680
+ if (result.range) {
681
+ console.log(`Range: finalization ${result.range.fromFinalizationIdExclusive} -> ${result.range.toFinalizationIdInclusive}`);
682
+ }
683
+ if (result.synthesisPrompt) {
684
+ console.log('\nCheckpoint synthesis prompt:\n');
685
+ console.log(result.synthesisPrompt);
686
+ }
687
+ if (result.run) {
688
+ console.log(`Run: #${result.run.id} status=${result.run.status}`);
689
+ } else if (result.runInput) {
690
+ console.log(`Run input prepared: status=${result.runInput.status}`);
691
+ console.log('Mode: dry-run only. Re-run with --apply and an explicit synthesis summary to write checkpoint_runs.');
692
+ }
693
+ return;
694
+ }
695
+
517
696
  if (operatorVerb === 'archive-distill') {
518
697
  const inputPath = args.flags.input || args._[2];
519
698
  if (!inputPath) {
@@ -544,7 +723,7 @@ async function cmdOperator(aquifer, args) {
544
723
  }
545
724
 
546
725
  if (operatorVerb !== 'compaction' && operatorVerb !== 'compact' && !cadenceVerbs.has(operatorVerb)) {
547
- console.error('Usage: aquifer operator <compaction|archive-distill> [...]');
726
+ console.error('Usage: aquifer operator <compaction|checkpoint|archive-distill> [...]');
548
727
  process.exit(1);
549
728
  }
550
729
 
@@ -650,6 +829,7 @@ async function main() {
650
829
  Commands:
651
830
  quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
652
831
  migrate Run database migrations
832
+ backend-info Show selected backend capabilities without connecting to a database
653
833
  recall <query> Search sessions (requires embed config)
654
834
  evidence-recall <query> Search legacy session/evidence plane explicitly
655
835
  feedback Record trust feedback on a session
@@ -661,7 +841,7 @@ Commands:
661
841
  stats Show database statistics
662
842
  export Export sessions as JSONL
663
843
  bootstrap Show recent session context (for new session start)
664
- codex-recovery ... Inspect or run Codex SessionStart recovery flow
844
+ codex-recovery ... Inspect or run Codex recovery/checkpoint flows
665
845
  ingest-opencode Import sessions from OpenCode's local SQLite DB
666
846
  mcp Start MCP server
667
847
 
@@ -697,7 +877,10 @@ Options:
697
877
  --include-synthesis-prompt Include timer synthesis prompt in operator output
698
878
  --synthesis-summary JSON Timer synthesis summary JSON to attach to a compaction plan
699
879
  --synthesis-summary-file P Read timer synthesis summary JSON from file
880
+ --min-finalizations N Min finalized session summaries before checkpoint proposal
881
+ --checkpoint-key KEY Explicit checkpoint key when applying checkpoint producer output
700
882
  --scope-key A,B Limit compaction snapshot to specific scope keys
883
+ --scope-id ID Target scope row id for checkpoint producer
701
884
  --scope-kind KIND Explicit synthesis target scope kind
702
885
  --snapshot-as-of ISO Read active snapshot as of a specific instant
703
886
  --claim-lease-seconds N Override compaction apply lease
@@ -711,6 +894,11 @@ Operator examples:
711
894
  aquifer operator compaction daily --include-synthesis-prompt --json
712
895
  aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
713
896
  aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json
897
+ aquifer operator checkpoint --scope-key project:aquifer --min-finalizations 10 --include-synthesis-prompt --json
898
+ aquifer operator checkpoint --scope-id 7 --synthesis-summary-file /tmp/checkpoint-summary.json --apply --finalize --json
899
+ AQUIFER_BACKEND=local aquifer backend-info --json
900
+ aquifer codex-recovery checkpoint-heartbeat --hook-stdin --scope-key project:aquifer
901
+ aquifer codex-recovery checkpoint-heartbeat-hook --scope-key project:aquifer --hooks-path "$CODEX_HOME/hooks.json" --json
714
902
  aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
715
903
  process.exit(0);
716
904
  }
@@ -743,6 +931,14 @@ Operator examples:
743
931
  return;
744
932
  }
745
933
 
934
+ if (command === 'backend-info') {
935
+ if (args.flags.config) {
936
+ process.env.AQUIFER_CONFIG = args.flags.config;
937
+ }
938
+ await cmdBackendInfo(args);
939
+ return;
940
+ }
941
+
746
942
  // All other commands need an Aquifer instance
747
943
  const configOverrides = {};
748
944
  if (args.flags.config) {
@@ -755,15 +951,18 @@ Operator examples:
755
951
  // Production commands (migrate, mcp, recall, ...) stay strict — they expect
756
952
  // the operator to have set env explicitly.
757
953
  if (command === 'quickstart') {
758
- const { autodetectForQuickstart } = require('./shared/autodetect');
759
- quickstartDetected = await autodetectForQuickstart(process.env);
760
- if (Object.keys(quickstartDetected).length > 0) {
761
- console.log('Autodetected localhost services (env not set):');
762
- for (const [k, v] of Object.entries(quickstartDetected)) {
763
- console.log(` ${k}=${v}`);
764
- process.env[k] = v;
954
+ const selected = loadConfig({ overrides: configOverrides });
955
+ if (selected.storage.backend !== 'local') {
956
+ const { autodetectForQuickstart } = require('./shared/autodetect');
957
+ quickstartDetected = await autodetectForQuickstart(process.env);
958
+ if (Object.keys(quickstartDetected).length > 0) {
959
+ console.log('Autodetected localhost services (env not set):');
960
+ for (const [k, v] of Object.entries(quickstartDetected)) {
961
+ console.log(` ${k}=${v}`);
962
+ process.env[k] = v;
963
+ }
964
+ console.log(' Export these in your shell (or MCP client env) to make them permanent.\n');
765
965
  }
766
- console.log(' Export these in your shell (or MCP client env) to make them permanent.\n');
767
966
  }
768
967
  }
769
968
 
@@ -785,7 +984,7 @@ Operator examples:
785
984
  await cmdQuickstart(aquifer);
786
985
  break;
787
986
  case 'migrate':
788
- await cmdMigrate(aquifer);
987
+ await cmdMigrate(aquifer, args);
789
988
  break;
790
989
  case 'recall':
791
990
  await cmdRecall(aquifer, args);
@@ -837,6 +1036,10 @@ Operator examples:
837
1036
  // Export for testing; execute only when run directly
838
1037
  module.exports = {
839
1038
  parseArgs,
1039
+ selectedBackendInfo,
1040
+ cmdBackendInfo,
1041
+ cmdMigrate,
1042
+ cmdLocalQuickstart,
840
1043
  cmdOperator,
841
1044
  readSynthesisSummaryFromFlags,
842
1045
  };