@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.
- package/.env.example +8 -0
- package/README.md +72 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +259 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +551 -6
- package/consumers/codex.js +209 -25
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +357 -838
- 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-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +347 -11
- 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 +98 -2
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +66 -3
- 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 +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}
|
|
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 {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
console.log(
|
|
701
|
-
|
|
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 = {
|
|
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 => {
|