@shadowforge0/aquifer-memory 1.7.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.
- package/.env.example +8 -0
- package/README.md +66 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +192 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +442 -3
- package/consumers/codex.js +164 -107
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +351 -840
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +128 -8
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +82 -1
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/setup.md +22 -0
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- 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; }
|
|
@@ -381,15 +386,122 @@ async function cmdStats(aquifer, args) {
|
|
|
381
386
|
if (args.flags.json) {
|
|
382
387
|
console.log(JSON.stringify(stats, null, 2));
|
|
383
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'}`);
|
|
384
392
|
console.log(`Sessions: ${stats.sessionTotal} (${Object.entries(stats.sessions).map(([k, v]) => `${k}: ${v}`).join(', ')})`);
|
|
385
393
|
console.log(`Summaries: ${stats.summaries}`);
|
|
386
394
|
console.log(`Turn embeddings: ${stats.turnEmbeddings}`);
|
|
387
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
|
+
}
|
|
388
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}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
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;
|
|
389
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.');
|
|
390
497
|
}
|
|
391
498
|
|
|
392
499
|
async function cmdQuickstart(aquifer) {
|
|
500
|
+
if (aquifer.getConfig().backendKind === 'local') {
|
|
501
|
+
await cmdLocalQuickstart(aquifer);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
393
505
|
console.log('Aquifer quickstart — verifying end-to-end setup.\n');
|
|
394
506
|
|
|
395
507
|
// 1. Migrate
|
|
@@ -514,6 +626,51 @@ async function cmdOperator(aquifer, args) {
|
|
|
514
626
|
const operatorVerb = args._[1] || 'compaction';
|
|
515
627
|
const cadenceVerbs = new Set(['manual', 'daily', 'weekly', 'monthly']);
|
|
516
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
|
+
|
|
517
674
|
if (operatorVerb === 'archive-distill') {
|
|
518
675
|
const inputPath = args.flags.input || args._[2];
|
|
519
676
|
if (!inputPath) {
|
|
@@ -544,7 +701,7 @@ async function cmdOperator(aquifer, args) {
|
|
|
544
701
|
}
|
|
545
702
|
|
|
546
703
|
if (operatorVerb !== 'compaction' && operatorVerb !== 'compact' && !cadenceVerbs.has(operatorVerb)) {
|
|
547
|
-
console.error('Usage: aquifer operator <compaction|archive-distill> [...]');
|
|
704
|
+
console.error('Usage: aquifer operator <compaction|checkpoint|archive-distill> [...]');
|
|
548
705
|
process.exit(1);
|
|
549
706
|
}
|
|
550
707
|
|
|
@@ -650,6 +807,7 @@ async function main() {
|
|
|
650
807
|
Commands:
|
|
651
808
|
quickstart Verify end-to-end setup (migrate → commit → enrich → recall)
|
|
652
809
|
migrate Run database migrations
|
|
810
|
+
backend-info Show selected backend capabilities without connecting to a database
|
|
653
811
|
recall <query> Search sessions (requires embed config)
|
|
654
812
|
evidence-recall <query> Search legacy session/evidence plane explicitly
|
|
655
813
|
feedback Record trust feedback on a session
|
|
@@ -661,7 +819,7 @@ Commands:
|
|
|
661
819
|
stats Show database statistics
|
|
662
820
|
export Export sessions as JSONL
|
|
663
821
|
bootstrap Show recent session context (for new session start)
|
|
664
|
-
codex-recovery ... Inspect or run Codex
|
|
822
|
+
codex-recovery ... Inspect or run Codex recovery/checkpoint flows
|
|
665
823
|
ingest-opencode Import sessions from OpenCode's local SQLite DB
|
|
666
824
|
mcp Start MCP server
|
|
667
825
|
|
|
@@ -697,7 +855,10 @@ Options:
|
|
|
697
855
|
--include-synthesis-prompt Include timer synthesis prompt in operator output
|
|
698
856
|
--synthesis-summary JSON Timer synthesis summary JSON to attach to a compaction plan
|
|
699
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
|
|
700
860
|
--scope-key A,B Limit compaction snapshot to specific scope keys
|
|
861
|
+
--scope-id ID Target scope row id for checkpoint producer
|
|
701
862
|
--scope-kind KIND Explicit synthesis target scope kind
|
|
702
863
|
--snapshot-as-of ISO Read active snapshot as of a specific instant
|
|
703
864
|
--claim-lease-seconds N Override compaction apply lease
|
|
@@ -711,6 +872,11 @@ Operator examples:
|
|
|
711
872
|
aquifer operator compaction daily --include-synthesis-prompt --json
|
|
712
873
|
aquifer operator compaction manual --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z --apply
|
|
713
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
|
|
714
880
|
aquifer operator archive-distill --input /tmp/archive-snapshot.json --json`);
|
|
715
881
|
process.exit(0);
|
|
716
882
|
}
|
|
@@ -743,6 +909,14 @@ Operator examples:
|
|
|
743
909
|
return;
|
|
744
910
|
}
|
|
745
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
|
+
|
|
746
920
|
// All other commands need an Aquifer instance
|
|
747
921
|
const configOverrides = {};
|
|
748
922
|
if (args.flags.config) {
|
|
@@ -755,15 +929,18 @@ Operator examples:
|
|
|
755
929
|
// Production commands (migrate, mcp, recall, ...) stay strict — they expect
|
|
756
930
|
// the operator to have set env explicitly.
|
|
757
931
|
if (command === 'quickstart') {
|
|
758
|
-
const {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
console.log(
|
|
764
|
-
|
|
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');
|
|
765
943
|
}
|
|
766
|
-
console.log(' Export these in your shell (or MCP client env) to make them permanent.\n');
|
|
767
944
|
}
|
|
768
945
|
}
|
|
769
946
|
|
|
@@ -837,6 +1014,9 @@ Operator examples:
|
|
|
837
1014
|
// Export for testing; execute only when run directly
|
|
838
1015
|
module.exports = {
|
|
839
1016
|
parseArgs,
|
|
1017
|
+
selectedBackendInfo,
|
|
1018
|
+
cmdBackendInfo,
|
|
1019
|
+
cmdLocalQuickstart,
|
|
840
1020
|
cmdOperator,
|
|
841
1021
|
readSynthesisSummaryFromFlags,
|
|
842
1022
|
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
buildCheckpointCoverageFromView,
|
|
5
|
+
hashSnapshot,
|
|
6
|
+
promptSafeSynthesisInput,
|
|
7
|
+
stableJson,
|
|
8
|
+
} = require('../core/session-checkpoint-producer');
|
|
9
|
+
const { compactCurrentMemorySnapshot } = require('./codex-current-memory');
|
|
10
|
+
|
|
11
|
+
function positiveInt(value, fallback, max = 100000) {
|
|
12
|
+
const n = Number(value === undefined || value === null || value === '' ? fallback : value);
|
|
13
|
+
if (!Number.isFinite(n)) return fallback;
|
|
14
|
+
return Math.max(1, Math.min(max, Math.trunc(n)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildActiveCheckpointScopeEnvelope(opts = {}) {
|
|
18
|
+
const envelope = opts.scopeEnvelope || opts.scope_envelope;
|
|
19
|
+
if (envelope && typeof envelope === 'object') return envelope;
|
|
20
|
+
const scopeKey = String(opts.activeScopeKey || opts.scopeKey || opts.scope_key || '').trim();
|
|
21
|
+
if (!scopeKey) {
|
|
22
|
+
throw new Error('active session checkpoint requires activeScopeKey or scopeKey');
|
|
23
|
+
}
|
|
24
|
+
const scopeKind = String(opts.activeScopeKind || opts.scopeKind || opts.scope_kind || 'project').trim();
|
|
25
|
+
const slotId = scopeKind === 'host_runtime' ? 'host' : (
|
|
26
|
+
['workspace', 'project', 'repo', 'task', 'session'].includes(scopeKind) ? scopeKind : 'target'
|
|
27
|
+
);
|
|
28
|
+
const promotable = !['session', 'task'].includes(slotId);
|
|
29
|
+
const slot = {
|
|
30
|
+
id: slotId,
|
|
31
|
+
slot: slotId,
|
|
32
|
+
scopeKind,
|
|
33
|
+
scopeKey,
|
|
34
|
+
label: scopeKey,
|
|
35
|
+
promotable,
|
|
36
|
+
allowedScopeKeys: promotable ? ['global', scopeKey] : ['global'],
|
|
37
|
+
};
|
|
38
|
+
if (!promotable) {
|
|
39
|
+
throw new Error(`active session checkpoint target scope is not promotable: ${scopeKind}`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
policyVersion: 'scope_envelope_v1',
|
|
43
|
+
activeSlotId: slot.id,
|
|
44
|
+
activeScopeKey: scopeKey,
|
|
45
|
+
allowedScopeKeys: slot.allowedScopeKeys,
|
|
46
|
+
slots: [slot],
|
|
47
|
+
scopeById: { [slot.id]: slot },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildActiveSessionCheckpointInput(view = {}, opts = {}) {
|
|
52
|
+
if (!view || view.status !== 'ok') {
|
|
53
|
+
throw new Error(`active session checkpoint requires an ok transcript view; got ${view && view.status ? view.status : 'missing'}`);
|
|
54
|
+
}
|
|
55
|
+
const messageCount = Number.isFinite(Number(view.counts?.safeMessageCount))
|
|
56
|
+
? Number(view.counts.safeMessageCount)
|
|
57
|
+
: (Array.isArray(view.messages) ? view.messages.length : 0);
|
|
58
|
+
const userCount = Number(view.counts?.userCount || view.messages?.filter?.(m => m.role === 'user').length || 0);
|
|
59
|
+
const everyMessages = positiveInt(opts.checkpointEveryMessages || opts.everyMessages, 20, 1000);
|
|
60
|
+
const everyUserMessages = opts.checkpointEveryUserMessages || opts.everyUserMessages
|
|
61
|
+
? positiveInt(opts.checkpointEveryUserMessages || opts.everyUserMessages, 10, 1000)
|
|
62
|
+
: null;
|
|
63
|
+
const force = opts.force === true;
|
|
64
|
+
const due = force
|
|
65
|
+
|| messageCount >= everyMessages
|
|
66
|
+
|| (everyUserMessages !== null && userCount >= everyUserMessages);
|
|
67
|
+
const base = {
|
|
68
|
+
kind: 'codex_active_session_checkpoint_input_v1',
|
|
69
|
+
policyVersion: opts.policyVersion || 'codex_active_session_checkpoint_v1',
|
|
70
|
+
sourceOfTruth: 'codex_sanitized_live_transcript_view',
|
|
71
|
+
triggerKind: opts.triggerKind || 'message_count',
|
|
72
|
+
promotion: {
|
|
73
|
+
default: 'checkpoint_proposal_only',
|
|
74
|
+
requires: 'handoff_or_operator_review',
|
|
75
|
+
},
|
|
76
|
+
guards: {
|
|
77
|
+
checkpointIsProcessMaterial: true,
|
|
78
|
+
activeMemoryCommitExcluded: true,
|
|
79
|
+
dbWriteExcluded: true,
|
|
80
|
+
rawToolOutputExcluded: true,
|
|
81
|
+
debugIdsExcluded: true,
|
|
82
|
+
},
|
|
83
|
+
threshold: {
|
|
84
|
+
everyMessages,
|
|
85
|
+
everyUserMessages,
|
|
86
|
+
messageCount,
|
|
87
|
+
userCount,
|
|
88
|
+
due,
|
|
89
|
+
},
|
|
90
|
+
targetScope: {
|
|
91
|
+
activeScopeKey: opts.activeScopeKey || opts.scopeKey || null,
|
|
92
|
+
activeScopePath: opts.activeScopePath || null,
|
|
93
|
+
},
|
|
94
|
+
scopeEnvelope: buildActiveCheckpointScopeEnvelope(opts),
|
|
95
|
+
coverage: buildCheckpointCoverageFromView(view, opts),
|
|
96
|
+
transcript: {
|
|
97
|
+
sessionId: view.sessionId || null,
|
|
98
|
+
charCount: view.charCount ?? String(view.text || '').length,
|
|
99
|
+
fullCharCount: view.fullCharCount ?? view.counts?.fullCharCount ?? view.charCount ?? String(view.text || '').length,
|
|
100
|
+
approxPromptTokens: view.approxPromptTokens || Math.ceil(String(view.text || '').length / 3),
|
|
101
|
+
fullApproxPromptTokens: view.fullApproxPromptTokens || view.counts?.fullApproxPromptTokens || null,
|
|
102
|
+
truncated: Boolean(view.truncated || view.transcriptWindow?.truncated),
|
|
103
|
+
transcriptWindow: view.transcriptWindow || null,
|
|
104
|
+
text: view.text || '',
|
|
105
|
+
},
|
|
106
|
+
currentMemory: compactCurrentMemorySnapshot(opts.currentMemory || null, opts),
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
...base,
|
|
110
|
+
inputHash: hashSnapshot(base),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildActiveSessionCheckpointPrompt(checkpointInput = {}, opts = {}) {
|
|
115
|
+
if (!checkpointInput || checkpointInput.kind !== 'codex_active_session_checkpoint_input_v1') {
|
|
116
|
+
throw new Error('buildActiveSessionCheckpointPrompt requires an active session checkpoint input');
|
|
117
|
+
}
|
|
118
|
+
const promptInput = promptSafeSynthesisInput(checkpointInput);
|
|
119
|
+
const maxFacts = Math.max(1, Math.min(24, opts.maxFacts || 8));
|
|
120
|
+
return [
|
|
121
|
+
'You are producing an Aquifer active-session checkpoint proposal for Codex.',
|
|
122
|
+
'Use only the <active_checkpoint_input> block. Do not use hidden tool output, injected context, or debug material.',
|
|
123
|
+
'This checkpoint is process material for later handoff. It is not active current memory and must not be treated as final truth.',
|
|
124
|
+
'Return compact JSON with this shape:',
|
|
125
|
+
'{"summaryText":"...","structuredSummary":{"facts":[],"decisions":[],"open_loops":[],"preferences":[],"constraints":[],"conclusions":[],"entity_notes":[],"states":[]},"coverage":{"coordinateSystem":"codex_sanitized_view_v1","coveredUntilMessageIndex":0,"coveredUntilChar":0}}',
|
|
126
|
+
`Keep facts/decisions/open_loops concrete and scoped. Use at most ${maxFacts} facts.`,
|
|
127
|
+
'Preserve the coverage object so a later handoff can skip the already-covered transcript prefix.',
|
|
128
|
+
'',
|
|
129
|
+
'<active_checkpoint_input>',
|
|
130
|
+
stableJson(promptInput),
|
|
131
|
+
'</active_checkpoint_input>',
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function prepareActiveSessionCheckpoint(aquifer, opts = {}, deps = {}) {
|
|
136
|
+
const materializeRecoveryTranscriptView = deps.materializeRecoveryTranscriptView;
|
|
137
|
+
const resolveCurrentMemoryForFinalization = deps.resolveCurrentMemoryForFinalization || (async () => null);
|
|
138
|
+
const view = opts.view || (opts.filePath && typeof materializeRecoveryTranscriptView === 'function'
|
|
139
|
+
? materializeRecoveryTranscriptView({
|
|
140
|
+
filePath: opts.filePath,
|
|
141
|
+
sessionId: opts.sessionId,
|
|
142
|
+
}, {
|
|
143
|
+
...opts,
|
|
144
|
+
maxRecoveryBytes: opts.maxCheckpointBytes ?? opts.maxRecoveryBytes,
|
|
145
|
+
maxRecoveryMessages: opts.maxCheckpointMessages ?? opts.maxRecoveryMessages,
|
|
146
|
+
maxRecoveryChars: opts.maxCheckpointChars ?? opts.maxRecoveryChars,
|
|
147
|
+
maxRecoveryPromptTokens: opts.maxCheckpointPromptTokens ?? opts.maxRecoveryPromptTokens,
|
|
148
|
+
})
|
|
149
|
+
: null);
|
|
150
|
+
if (!view || view.status !== 'ok') {
|
|
151
|
+
return {
|
|
152
|
+
status: view?.status || 'missing_view',
|
|
153
|
+
reason: view?.reason || null,
|
|
154
|
+
view,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const currentMemory = await resolveCurrentMemoryForFinalization(aquifer, opts);
|
|
158
|
+
const checkpointInput = buildActiveSessionCheckpointInput(view, {
|
|
159
|
+
...opts,
|
|
160
|
+
currentMemory,
|
|
161
|
+
});
|
|
162
|
+
if (!checkpointInput.threshold.due) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'not_ready',
|
|
165
|
+
due: false,
|
|
166
|
+
checkpointInput,
|
|
167
|
+
view,
|
|
168
|
+
currentMemory,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
status: 'needs_agent_checkpoint',
|
|
173
|
+
due: true,
|
|
174
|
+
outputSchemaVersion: 'codex_active_session_checkpoint_v1',
|
|
175
|
+
checkpointInput,
|
|
176
|
+
view,
|
|
177
|
+
currentMemory,
|
|
178
|
+
prompt: buildActiveSessionCheckpointPrompt(checkpointInput, opts),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
buildActiveSessionCheckpointInput,
|
|
184
|
+
buildActiveSessionCheckpointPrompt,
|
|
185
|
+
prepareActiveSessionCheckpoint,
|
|
186
|
+
};
|