@shadowforge0/aquifer-memory 1.8.1 → 1.9.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 +1 -0
- package/README.md +49 -22
- package/README_CN.md +24 -22
- package/README_TW.md +20 -22
- package/aquifer.config.example.json +2 -1
- package/consumers/cli.js +535 -2
- package/consumers/codex.js +1 -1
- package/consumers/mcp.js +3 -0
- package/consumers/openclaw-ext/index.js +64 -6
- package/consumers/openclaw-ext/openclaw.plugin.json +1 -1
- package/consumers/openclaw-ext/package.json +1 -1
- package/consumers/openclaw-install.js +326 -0
- package/consumers/openclaw-plugin.js +39 -1
- package/consumers/shared/config.js +2 -0
- package/core/aquifer.js +180 -33
- package/core/backends/local.js +109 -0
- package/core/doctor.js +924 -0
- package/core/finalization-inspector.js +164 -0
- package/core/memory-explain.js +624 -0
- package/core/memory-recall.js +49 -23
- package/core/memory-records.js +16 -5
- package/core/memory-review.js +891 -0
- package/core/memory-serving.js +61 -4
- package/core/operator-observability.js +249 -0
- package/core/postgres-migrations.js +13 -0
- package/core/session-finalization.js +76 -1
- package/core/storage.js +124 -8
- package/docs/getting-started.md +34 -1
- package/docs/setup.md +102 -22
- package/package.json +5 -4
- package/schema/019-v1-memory-review-resolutions.sql +53 -0
- package/scripts/codex-checkpoint-commands.js +28 -0
- package/scripts/codex-checkpoint-runtime.js +109 -0
- package/scripts/codex-recovery.js +16 -4
package/docs/getting-started.md
CHANGED
|
@@ -73,20 +73,40 @@ Or run the server directly:
|
|
|
73
73
|
DATABASE_URL=... EMBED_PROVIDER=ollama npx aquifer mcp
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated` only when you want `session_recall` and `session_bootstrap` to serve active curated memory
|
|
76
|
+
For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated` only when you want compatibility `session_recall` and `session_bootstrap` to serve active curated memory. Use `memory_recall` for explicit current-memory lookup, `historical_recall` for the historical/session plane, and `evidence_recall` for the audit/debug lane. Rollback is just setting env or config back to `legacy`.
|
|
77
|
+
|
|
78
|
+
## Connect OpenClaw
|
|
79
|
+
|
|
80
|
+
For OpenClaw, use the host installer instead of wiring each consumer by hand:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
|
|
84
|
+
node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The installer links and enables the optional OpenClaw extension, updates `plugins.load.paths` / `plugins.entries["aquifer-memory"]`, and points `mcp.servers.aquifer` at the package's `consumers/mcp.js` while preserving existing MCP env values. For source checkouts, `scripts/install-openclaw.sh` first installs the current package into `$OPENCLAW_HOME/node_modules`, then runs the same packaged installer.
|
|
88
|
+
|
|
89
|
+
Run with `--dry-run --json` to verify the active package version, extension path, plugin config, and MCP target without changing files.
|
|
77
90
|
|
|
78
91
|
## Most common commands
|
|
79
92
|
|
|
80
93
|
| Goal | Command |
|
|
81
94
|
|---|---|
|
|
82
95
|
| Verify setup | `npx aquifer quickstart` |
|
|
96
|
+
| Run read-only diagnostics | `npx aquifer doctor --json` |
|
|
83
97
|
| Start MCP server | `npx aquifer mcp` |
|
|
98
|
+
| Install/update OpenClaw wiring | `node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"` |
|
|
84
99
|
| Search memory | `npx aquifer recall "auth middleware"` |
|
|
100
|
+
| Explain current-memory selection | `npx aquifer explain bootstrap --active-scope-key project:aquifer --json` |
|
|
101
|
+
| Inspect finalization ledger | `npx aquifer finalization list --status finalized --json` |
|
|
102
|
+
| Inspect operator ledgers | `npx aquifer operator status --json` |
|
|
103
|
+
| Review current-memory feedback issues | `npx aquifer review queue --scope-key project:aquifer --json` |
|
|
85
104
|
| Plan curated compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
|
|
86
105
|
| Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
|
|
87
106
|
| Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
|
|
88
107
|
| Show stats | `npx aquifer stats` |
|
|
89
108
|
| Enrich pending sessions | `npx aquifer backfill` |
|
|
109
|
+
| Resolve a reviewed memory issue | `npx aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --expected-latest-issue-feedback-id 9 --json` |
|
|
90
110
|
|
|
91
111
|
Timer synthesis is an operator-reviewed candidate workflow. The prompt output
|
|
92
112
|
and summary JSON do not become active curated memory unless the apply step is
|
|
@@ -102,4 +122,17 @@ If you see `type "vector" does not exist`, `pgvector` is not installed.
|
|
|
102
122
|
|
|
103
123
|
If recall returns no results, the embedding endpoint is usually unreachable or misconfigured.
|
|
104
124
|
|
|
125
|
+
For continuity or operator questions, use the read-only governance commands
|
|
126
|
+
first: `doctor`, `finalization list|inspect`, `explain bootstrap|memory`, and
|
|
127
|
+
`operator status|inspect`. Use `review queue|inspect` when curated memory
|
|
128
|
+
feedback has marked visible current-memory rows as `incorrect`, `stale`,
|
|
129
|
+
`scope_mismatch`, or similar issue types. These commands diagnose the DB and
|
|
130
|
+
serving state without changing memory truth, finalization status, MCP tools, or
|
|
131
|
+
operator leases. The review surface does not print raw transcripts, feedback
|
|
132
|
+
notes, feedback metadata, or memory payloads; `review queue|inspect` still uses
|
|
133
|
+
the normal Aquifer migration gate because it depends on the resolution ledger
|
|
134
|
+
schema. After human review, `review resolve` appends a resolution ledger row
|
|
135
|
+
only; it does not edit current memory truth, and newer issue feedback makes the
|
|
136
|
+
item show up in the queue again.
|
|
137
|
+
|
|
105
138
|
If you want the full setup matrix, host-specific examples, and advanced configuration for summarization, entities, reranking, or operations, continue to [docs/setup.md](setup.md).
|
package/docs/setup.md
CHANGED
|
@@ -49,7 +49,7 @@ Aquifer reads configuration from three sources (in priority order):
|
|
|
49
49
|
2. Environment variables (see below)
|
|
50
50
|
3. Programmatic overrides via `createAquifer()`
|
|
51
51
|
|
|
52
|
-
Default public serving mode is `legacy`. Opt into `curated` only when you want `session_recall` and `session_bootstrap` to read active curated memory. `
|
|
52
|
+
Default public serving mode is `legacy`. Opt into `curated` only when you want compatibility `session_recall` and `session_bootstrap` to read active curated memory. Use `memory_recall` for explicit current-memory lookup, `historical_recall` for the historical/session plane, and `evidence_recall` for the audit/debug lane. Rollback is just setting env or config back to `legacy`.
|
|
53
53
|
|
|
54
54
|
Backend profiles are explicit. `postgres` is the full backend and remains required for semantic recall, migrations, curated memory, and operator workflows. `local` is a zero-config starter profile with JSON-file persistence, raw session writes, lexical recall, bootstrap, stats, and export. It is intentionally degraded and does not create embeddings or run operator workflows:
|
|
55
55
|
|
|
@@ -76,7 +76,8 @@ AQUIFER_BACKEND=local npx aquifer backend-info --json
|
|
|
76
76
|
"memory": {
|
|
77
77
|
"servingMode": "legacy",
|
|
78
78
|
"activeScopeKey": "project:aquifer",
|
|
79
|
-
"activeScopePath": ["global", "project:aquifer"]
|
|
79
|
+
"activeScopePath": ["global", "project:aquifer"],
|
|
80
|
+
"allowedScopeKeys": ["global", "project:aquifer"]
|
|
80
81
|
},
|
|
81
82
|
"embed": {
|
|
82
83
|
"baseUrl": "http://localhost:11434/v1",
|
|
@@ -113,6 +114,7 @@ export AQUIFER_MEMORY_SERVING_MODE="legacy"
|
|
|
113
114
|
# export AQUIFER_MEMORY_SERVING_MODE="curated"
|
|
114
115
|
# export AQUIFER_MEMORY_ACTIVE_SCOPE_KEY="project:aquifer"
|
|
115
116
|
# export AQUIFER_MEMORY_ACTIVE_SCOPE_PATH="global,project:aquifer"
|
|
117
|
+
# export AQUIFER_MEMORY_ALLOWED_SCOPE_KEYS="global,project:aquifer"
|
|
116
118
|
|
|
117
119
|
# Optional Codex active-session checkpoint heartbeat policy.
|
|
118
120
|
# Command flags still take precedence over these env vars.
|
|
@@ -123,6 +125,34 @@ export AQUIFER_MEMORY_SERVING_MODE="legacy"
|
|
|
123
125
|
|
|
124
126
|
Copy `.env.example` from the repo root for a full annotated list.
|
|
125
127
|
|
|
128
|
+
### Curated scope contract
|
|
129
|
+
|
|
130
|
+
Current-memory serving uses two separate scope concepts. `activeScopePath` is
|
|
131
|
+
the ordered inheritance path used by bootstrap, recall, and explain. Broader
|
|
132
|
+
entries can feed defaults into narrower scopes only when they appear in that
|
|
133
|
+
path. `allowedScopeKeys` is the caller boundary. If a runtime request asks for
|
|
134
|
+
an `activeScopeKey`, `activeScopePath`, `scopeKey`, or resolved `scopeId`
|
|
135
|
+
outside `allowedScopeKeys`, Aquifer rejects the request before reading current
|
|
136
|
+
memory rows. When `activeScopePath` is omitted, it defaults to `global` plus the
|
|
137
|
+
configured `activeScopeKey`, or to `global` alone when no active scope is
|
|
138
|
+
configured. When `allowedScopeKeys` is omitted, it defaults to that active scope
|
|
139
|
+
path.
|
|
140
|
+
|
|
141
|
+
Serving inheritance is deterministic:
|
|
142
|
+
|
|
143
|
+
- `defaultable`: broader rows are defaults, narrower rows with the same
|
|
144
|
+
`canonicalKey` win.
|
|
145
|
+
- `exclusive`: currently an explicit narrowest-wins alias for serving. It keeps
|
|
146
|
+
the producer label visible in explain output but does not add a separate
|
|
147
|
+
runtime behavior.
|
|
148
|
+
- `additive`: all applicable rows are merged after scope filtering.
|
|
149
|
+
- `non_inheritable`: only the exact active scope can serve the row.
|
|
150
|
+
|
|
151
|
+
`aquifer explain bootstrap|memory --json` returns selected/excluded reasons
|
|
152
|
+
and a `scopeInheritance` block for each row. Non-selected rows retain safe
|
|
153
|
+
identity and reason fields but redact `canonicalKey`, `title`, and `summary`,
|
|
154
|
+
so explain remains diagnostic instead of becoming a cross-scope content probe.
|
|
155
|
+
|
|
126
156
|
## Step 4: Verify everything works
|
|
127
157
|
|
|
128
158
|
```bash
|
|
@@ -141,6 +171,25 @@ npx aquifer mcp
|
|
|
141
171
|
|
|
142
172
|
The server starts on stdio and waits for MCP client connections. There is no visible output on success — the server is ready when the process stays running without error.
|
|
143
173
|
|
|
174
|
+
### OpenClaw host install/update
|
|
175
|
+
|
|
176
|
+
OpenClaw should be updated through one host-level installer, not by editing each Aquifer consumer path separately:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
|
|
180
|
+
node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The command links `$OPENCLAW_HOME/extensions/aquifer-memory` to the package's OpenClaw extension, enables `plugins.entries["aquifer-memory"]`, adds the extension to `plugins.load.paths`, and updates `openclaw.json` so `mcp.servers.aquifer` runs the same package's `consumers/mcp.js`. Existing `mcp.servers.aquifer.env` values are preserved, and `openclaw.json` is backed up before writing.
|
|
184
|
+
|
|
185
|
+
For a source checkout, run the repo wrapper:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
bash scripts/install-openclaw.sh "$OPENCLAW_HOME"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
That wrapper installs the current package into `$OPENCLAW_HOME/node_modules` before delegating to the packaged installer. Use `--dry-run --json` to inspect the package version, extension link, plugin config, and MCP target without changing files. `--link-current-package` is a development-only escape hatch for wiring a source checkout directly.
|
|
192
|
+
|
|
144
193
|
### Verify with the library API (optional)
|
|
145
194
|
|
|
146
195
|
If you want to test the library directly instead of the CLI:
|
|
@@ -203,33 +252,25 @@ Add to `.claude.json` (project-level) or user-level MCP config:
|
|
|
203
252
|
}
|
|
204
253
|
```
|
|
205
254
|
|
|
206
|
-
Tools appear as `
|
|
255
|
+
Tools appear as `mcp__aquifer__memory_recall`, `mcp__aquifer__historical_recall`, `mcp__aquifer__session_recall`, `mcp__aquifer__evidence_recall`, `mcp__aquifer__session_feedback`, `mcp__aquifer__memory_feedback`, `mcp__aquifer__memory_stats`, `mcp__aquifer__memory_pending`, `mcp__aquifer__feedback_stats`, `mcp__aquifer__session_bootstrap`.
|
|
207
256
|
|
|
208
|
-
`
|
|
257
|
+
Use `memory_recall` for explicit current-memory lookup, `historical_recall` for older session detail, and compatibility `session_recall` when the host should follow the configured serving mode. `evidence_recall` is the explicit audit/debug tool; broad evidence searches require an audit boundary filter such as `agentId`, `source`, or `dateFrom/dateTo`, unless the caller explicitly opts into unsafe debug mode.
|
|
209
258
|
|
|
210
259
|
### OpenClaw
|
|
211
260
|
|
|
212
|
-
|
|
261
|
+
Install or update Aquifer inside the OpenClaw host root, then let the installer
|
|
262
|
+
wire both the MCP server and the optional extension from the same package root:
|
|
213
263
|
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"servers": {
|
|
218
|
-
"aquifer": {
|
|
219
|
-
"command": "node",
|
|
220
|
-
"args": ["/absolute/path/to/aquifer/consumers/mcp.js"],
|
|
221
|
-
"env": {
|
|
222
|
-
"DATABASE_URL": "postgresql://...",
|
|
223
|
-
"AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
|
|
224
|
-
"AQUIFER_EMBED_MODEL": "bge-m3"
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
264
|
+
```bash
|
|
265
|
+
npm install --prefix "$OPENCLAW_HOME" @shadowforge0/aquifer-memory@latest
|
|
266
|
+
node "$OPENCLAW_HOME/node_modules/@shadowforge0/aquifer-memory/consumers/cli.js" install-openclaw --openclaw-home "$OPENCLAW_HOME"
|
|
230
267
|
```
|
|
231
268
|
|
|
232
|
-
|
|
269
|
+
The installer preserves existing `mcp.servers.aquifer.env` values and backs up
|
|
270
|
+
`openclaw.json` before writing. Use `--dry-run --json` to inspect package
|
|
271
|
+
version, MCP target, and extension link without changing files.
|
|
272
|
+
|
|
273
|
+
Tools materialize as `aquifer__memory_recall`, `aquifer__historical_recall`, `aquifer__session_recall`, `aquifer__evidence_recall`, `aquifer__session_feedback`, `aquifer__memory_feedback`, `aquifer__memory_stats`, `aquifer__memory_pending`, `aquifer__feedback_stats`, `aquifer__session_bootstrap`.
|
|
233
274
|
|
|
234
275
|
Do **not** use the OpenClaw plugin (`consumers/openclaw-plugin.js`) for tool delivery. The plugin is retained for session capture via `before_reset` only.
|
|
235
276
|
|
|
@@ -274,6 +315,45 @@ Without `--promote-candidates`, synthesis output is recorded as candidate
|
|
|
274
315
|
ledger material only. The prompt and summary file are producer material; active
|
|
275
316
|
curated memory still requires the explicit promotion gate.
|
|
276
317
|
|
|
318
|
+
## Read-only governance diagnostics
|
|
319
|
+
|
|
320
|
+
Use the governance commands before running write paths when continuity or
|
|
321
|
+
operator state looks wrong:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npx aquifer doctor --json
|
|
325
|
+
npx aquifer finalization list --status failed --json
|
|
326
|
+
npx aquifer finalization inspect --id 42 --json
|
|
327
|
+
npx aquifer explain bootstrap --active-scope-key project:aquifer --json
|
|
328
|
+
npx aquifer explain memory --query "serving contract" --active-scope-key project:aquifer --json
|
|
329
|
+
npx aquifer review queue --scope-key project:aquifer --json
|
|
330
|
+
npx aquifer review inspect --memory-id 42 --json
|
|
331
|
+
npx aquifer review resolve --memory-id 42 --resolution resolved --reason "verified current" --expected-latest-issue-feedback-id 9 --json
|
|
332
|
+
npx aquifer operator status --json
|
|
333
|
+
npx aquifer operator inspect --run-id 42 --kind compaction --json
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`doctor` reports package/runtime, backend, DB/schema/tenant readiness,
|
|
337
|
+
migration readiness, serving mode, MCP tool count, session/finalization
|
|
338
|
+
backlog, and stale operator claims. Host checks are scoped: OpenClaw wiring is
|
|
339
|
+
checked only with `--host openclaw` or `--openclaw-home`, and Codex checkpoint
|
|
340
|
+
hook state is checked only with `--host codex`.
|
|
341
|
+
|
|
342
|
+
The finalization, explain, review queue/inspect, and operator inspect commands
|
|
343
|
+
read committed ledger and current-memory projection material only. `review
|
|
344
|
+
queue|inspect` derives its worklist from curated memory feedback events on
|
|
345
|
+
active visible memory records and uses the normal Aquifer migration gate because
|
|
346
|
+
it depends on the resolution ledger schema. It summarizes feedback counts and
|
|
347
|
+
safe identity fields; it does not print raw transcripts, feedback notes,
|
|
348
|
+
feedback metadata, evidence text, or memory payloads. `review resolve` is
|
|
349
|
+
append-only: it records a resolution snapshot through the latest issue feedback
|
|
350
|
+
id, leaves `memory_records` and feedback history untouched, and lets newer
|
|
351
|
+
issue feedback reopen the item. These commands do not promote memory, mutate
|
|
352
|
+
finalization status, reclaim leases, or change the ten-tool MCP surface.
|
|
353
|
+
Explain output is also redacted for non-selected rows: it reports safe identity,
|
|
354
|
+
selection reason, and scope inheritance details, but not hidden, shadowed,
|
|
355
|
+
out-of-scope, inactive, or trimmed row content.
|
|
356
|
+
|
|
277
357
|
## Release verification gates
|
|
278
358
|
|
|
279
359
|
For the publish-surface checks:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "PG-native long-term memory for AI agents. Turn-level embedding, hybrid RRF ranking, optional knowledge graph. MCP server, CLI, and library API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"consumers/codex-active-checkpoint.js",
|
|
19
19
|
"consumers/codex-current-memory.js",
|
|
20
20
|
"consumers/codex-handoff.js",
|
|
21
|
+
"consumers/openclaw-install.js",
|
|
21
22
|
"consumers/openclaw-plugin.js",
|
|
22
23
|
"consumers/opencode.js",
|
|
23
24
|
"consumers/shared/",
|
|
@@ -71,9 +72,9 @@
|
|
|
71
72
|
"scripts": {
|
|
72
73
|
"test": "node --test test/*.test.js",
|
|
73
74
|
"test:integration": "node --test test/integration.test.js",
|
|
74
|
-
"test:release:package": "node --test test/package-surface.test.js test/mcp-manifest.test.js test/local-backend.test.js test/scope-attribution.test.js test/v1-checkpoint-ledger-schema.test.js test/v1-finalization-envelope-schema.test.js test/v1-evidence-items.test.js test/v1-curated-semantic-recall.test.js test/session-checkpoints.test.js test/session-checkpoint-producer.test.js test/session-checkpoint-planner.test.js test/storage-checkpoint-ranges.test.js test/v1-serving-cutover.test.js test/v1-current-memory-contract.test.js test/v1-scope-inheritance.golden.test.js test/v1-bootstrap-determinism.test.js test/consumer-codex.test.js test/codex-recovery-script.test.js test/codex-handoff.test.js",
|
|
75
|
-
"test:release:db": "node -e \"if (!process.env.AQUIFER_TEST_DB_URL) { console.error('AQUIFER_TEST_DB_URL is required for test:release:db'); process.exit(1); }\" && node --test test/v1-evidence-items.test.js test/consumer-mcp.integration.test.js test/consumer-cli.integration.test.js test/codex-finalization-serving.integration.test.js",
|
|
76
|
-
"lint": "eslint index.js core/*.js core/backends/*.js consumers/cli.js consumers/mcp.js consumers/claude-code.js consumers/codex.js consumers/codex-active-checkpoint.js consumers/codex-current-memory.js consumers/codex-handoff.js consumers/openclaw-plugin.js consumers/opencode.js consumers/shared/*.js consumers/default/*.js consumers/default/prompts/*.js consumers/openclaw-ext/*.js pipeline/*.js pipeline/consolidation/*.js scripts/*.js test/*.js",
|
|
75
|
+
"test:release:package": "node --test test/package-surface.test.js test/mcp-manifest.test.js test/local-backend.test.js test/scope-attribution.test.js test/doctor.test.js test/finalization-inspector.test.js test/memory-explain.test.js test/memory-review.test.js test/operator-observability.test.js test/storage-finalization-status.test.js test/cli-parseargs.test.js test/v1-checkpoint-ledger-schema.test.js test/v1-finalization-envelope-schema.test.js test/v1-memory-review-resolutions-schema.test.js test/v1-evidence-items.test.js test/v1-curated-semantic-recall.test.js test/session-checkpoints.test.js test/session-checkpoint-producer.test.js test/session-checkpoint-planner.test.js test/storage-checkpoint-ranges.test.js test/v1-serving-cutover.test.js test/v1-current-memory-contract.test.js test/v1-scope-inheritance.golden.test.js test/v1-bootstrap-determinism.test.js test/consumer-codex.test.js test/codex-recovery-script.test.js test/codex-handoff.test.js",
|
|
76
|
+
"test:release:db": "node -e \"if (!process.env.AQUIFER_TEST_DB_URL) { console.error('AQUIFER_TEST_DB_URL is required for test:release:db'); process.exit(1); }\" && node --test --test-concurrency=1 test/v1-evidence-items.test.js test/memory-review.integration.test.js test/consumer-mcp.integration.test.js test/consumer-cli.integration.test.js test/codex-finalization-serving.integration.test.js",
|
|
77
|
+
"lint": "eslint index.js core/*.js core/backends/*.js consumers/cli.js consumers/mcp.js consumers/claude-code.js consumers/codex.js consumers/codex-active-checkpoint.js consumers/codex-current-memory.js consumers/codex-handoff.js consumers/openclaw-install.js consumers/openclaw-plugin.js consumers/opencode.js consumers/shared/*.js consumers/default/*.js consumers/default/prompts/*.js consumers/openclaw-ext/*.js pipeline/*.js pipeline/consolidation/*.js scripts/*.js test/*.js",
|
|
77
78
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
78
79
|
},
|
|
79
80
|
"dependencies": {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- Aquifer v1 memory review resolution ledger
|
|
2
|
+
-- Requires: 007-v1-foundation.sql
|
|
3
|
+
--
|
|
4
|
+
-- This migration is additive. Review resolutions close or defer operator queue
|
|
5
|
+
-- items without mutating memory_records or rewriting feedback history.
|
|
6
|
+
|
|
7
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_records_tenant_id_id
|
|
8
|
+
ON ${schema}.memory_records (tenant_id, id);
|
|
9
|
+
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_memory_review_latest
|
|
11
|
+
ON ${schema}.feedback (tenant_id, target_id, feedback_type, created_at DESC, id DESC)
|
|
12
|
+
WHERE target_kind = 'memory_record';
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS ${schema}.memory_review_resolutions (
|
|
15
|
+
id BIGSERIAL PRIMARY KEY,
|
|
16
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
17
|
+
memory_id BIGINT NOT NULL,
|
|
18
|
+
canonical_key TEXT NOT NULL CHECK (btrim(canonical_key) <> ''),
|
|
19
|
+
scope_id BIGINT,
|
|
20
|
+
resolution TEXT NOT NULL
|
|
21
|
+
CHECK (resolution IN ('resolved','ignored','deferred')),
|
|
22
|
+
reason TEXT,
|
|
23
|
+
actor_kind TEXT NOT NULL DEFAULT 'user'
|
|
24
|
+
CHECK (actor_kind IN ('user','agent','system','curator')),
|
|
25
|
+
actor_id TEXT,
|
|
26
|
+
issue_feedback_types TEXT[] NOT NULL DEFAULT '{}'::text[],
|
|
27
|
+
resolved_through_feedback_id BIGINT,
|
|
28
|
+
resolved_through_feedback_at TIMESTAMPTZ,
|
|
29
|
+
defer_until TIMESTAMPTZ,
|
|
30
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
31
|
+
resolved_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
32
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
33
|
+
FOREIGN KEY (tenant_id, memory_id)
|
|
34
|
+
REFERENCES ${schema}.memory_records (tenant_id, id) ON DELETE RESTRICT,
|
|
35
|
+
FOREIGN KEY (scope_id)
|
|
36
|
+
REFERENCES ${schema}.scopes(id) ON DELETE SET NULL,
|
|
37
|
+
CHECK (jsonb_typeof(metadata) = 'object'),
|
|
38
|
+
CHECK (defer_until IS NULL OR resolution = 'deferred'),
|
|
39
|
+
CHECK (defer_until IS NULL OR defer_until > resolved_at)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_memory_review_resolutions_memory_latest
|
|
43
|
+
ON ${schema}.memory_review_resolutions (tenant_id, memory_id, resolved_at DESC, id DESC);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_memory_review_resolutions_canonical_latest
|
|
46
|
+
ON ${schema}.memory_review_resolutions (tenant_id, canonical_key, resolved_at DESC, id DESC);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_memory_review_resolutions_defer_until
|
|
49
|
+
ON ${schema}.memory_review_resolutions (tenant_id, defer_until)
|
|
50
|
+
WHERE resolution = 'deferred' AND defer_until IS NOT NULL;
|
|
51
|
+
|
|
52
|
+
COMMENT ON TABLE ${schema}.memory_review_resolutions IS
|
|
53
|
+
'Append-only operator ledger for memory review queue resolutions. Does not mutate curated memory truth.';
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
defaultHooksPath,
|
|
22
22
|
findNewestJsonlFile,
|
|
23
23
|
isoAt,
|
|
24
|
+
listCheckpointSpool,
|
|
24
25
|
loadRuntimeConfig,
|
|
25
26
|
mergeCheckpointHeartbeatHook,
|
|
26
27
|
readCheckpointMarker,
|
|
@@ -215,6 +216,32 @@ function emitCheckpointHeartbeatResult(result, flags = {}) {
|
|
|
215
216
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
async function cmdCheckpointSpoolStatus(aquifer, flags, opts) {
|
|
220
|
+
const result = listCheckpointSpool(flags, opts);
|
|
221
|
+
if (flags.json) {
|
|
222
|
+
console.log(JSON.stringify(result, null, 2));
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
const lines = [
|
|
226
|
+
`Spool: ${result.spoolDir}`,
|
|
227
|
+
`Pending proposals: ${result.count} across ${result.sessionCount} sessions`,
|
|
228
|
+
];
|
|
229
|
+
if (result.latestMtime) lines.push(`Latest: ${result.latestMtime}`);
|
|
230
|
+
for (const row of result.files) {
|
|
231
|
+
const covered = row.coverage?.coveredUntilMessageIndex;
|
|
232
|
+
lines.push([
|
|
233
|
+
`- ${row.fileName}`,
|
|
234
|
+
`session=${row.sessionId || '?'}`,
|
|
235
|
+
`mtime=${row.mtime || '?'}`,
|
|
236
|
+
`bytes=${row.bytes || 0}`,
|
|
237
|
+
`coveredUntilMessageIndex=${covered ?? '?'}`,
|
|
238
|
+
`promptChars=${row.promptChars || 0}`,
|
|
239
|
+
].join(' '));
|
|
240
|
+
}
|
|
241
|
+
console.log(lines.join('\n'));
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
218
245
|
async function cmdCheckpointHeartbeat(aquifer, flags, opts, hookInputArg) {
|
|
219
246
|
const hookInput = hookInputArg || (flags['hook-stdin'] === true ? readHookInputFromStdin() : {});
|
|
220
247
|
const input = checkpointHeartbeatInput(flags, hookInput);
|
|
@@ -457,6 +484,7 @@ module.exports = {
|
|
|
457
484
|
cmdCheckpointHeartbeat,
|
|
458
485
|
cmdCheckpointHeartbeatHook,
|
|
459
486
|
cmdCheckpointPrompt,
|
|
487
|
+
cmdCheckpointSpoolStatus,
|
|
460
488
|
cmdCheckpointTick,
|
|
461
489
|
emitCheckpointHeartbeatResult,
|
|
462
490
|
parseScopePath,
|
|
@@ -377,6 +377,114 @@ function spoolCheckpointProposal(dir, prepared = {}, meta = {}) {
|
|
|
377
377
|
return { filePath, createdAt: payload.createdAt };
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
function summarizeCheckpointSpoolFile(filePath) {
|
|
381
|
+
const parsed = readJsonFile(filePath);
|
|
382
|
+
let stat;
|
|
383
|
+
try {
|
|
384
|
+
stat = fs.statSync(filePath);
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (!parsed || parsed.kind !== 'codex_active_checkpoint_pending_v1') {
|
|
389
|
+
return {
|
|
390
|
+
filePath,
|
|
391
|
+
fileName: path.basename(filePath),
|
|
392
|
+
bytes: stat.size,
|
|
393
|
+
mtime: stat.mtime.toISOString(),
|
|
394
|
+
parseable: !!parsed,
|
|
395
|
+
kind: parsed?.kind || null,
|
|
396
|
+
ignored: true,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
filePath,
|
|
401
|
+
fileName: path.basename(filePath),
|
|
402
|
+
bytes: stat.size,
|
|
403
|
+
mtime: stat.mtime.toISOString(),
|
|
404
|
+
parseable: true,
|
|
405
|
+
ignored: false,
|
|
406
|
+
kind: parsed.kind,
|
|
407
|
+
createdAt: parsed.createdAt || null,
|
|
408
|
+
sessionId: parsed.sessionId || null,
|
|
409
|
+
source: parsed.source || null,
|
|
410
|
+
hookEventName: parsed.hookEventName || null,
|
|
411
|
+
triggerKind: parsed.triggerKind || null,
|
|
412
|
+
threshold: parsed.threshold || null,
|
|
413
|
+
coverage: parsed.coverage || null,
|
|
414
|
+
guards: parsed.guards || null,
|
|
415
|
+
hasPrompt: typeof parsed.prompt === 'string' && parsed.prompt.length > 0,
|
|
416
|
+
promptChars: typeof parsed.prompt === 'string' ? parsed.prompt.length : 0,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function listCheckpointSpool(flags = {}, opts = {}) {
|
|
421
|
+
const dir = checkpointSpoolDir(flags, opts);
|
|
422
|
+
const limit = Math.max(1, Math.min(200, parseIntFlag(flags.limit, 20)));
|
|
423
|
+
let entries = [];
|
|
424
|
+
try {
|
|
425
|
+
entries = fs.readdirSync(dir);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (err && err.code === 'ENOENT') {
|
|
428
|
+
return {
|
|
429
|
+
status: 'ok',
|
|
430
|
+
spoolDir: dir,
|
|
431
|
+
count: 0,
|
|
432
|
+
returned: 0,
|
|
433
|
+
sessionCount: 0,
|
|
434
|
+
totalBytes: 0,
|
|
435
|
+
latestMtime: null,
|
|
436
|
+
files: [],
|
|
437
|
+
sessions: [],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
const sessionFilter = flags['session-id'] && flags['session-id'] !== true
|
|
443
|
+
? String(flags['session-id'])
|
|
444
|
+
: null;
|
|
445
|
+
const rows = entries
|
|
446
|
+
.filter(fileName => fileName.endsWith('.json'))
|
|
447
|
+
.map(fileName => summarizeCheckpointSpoolFile(path.join(dir, fileName)))
|
|
448
|
+
.filter(Boolean)
|
|
449
|
+
.filter(row => !sessionFilter || row.sessionId === sessionFilter)
|
|
450
|
+
.sort((a, b) => String(b.mtime || '').localeCompare(String(a.mtime || '')));
|
|
451
|
+
const validRows = rows.filter(row => !row.ignored);
|
|
452
|
+
const sessions = new Map();
|
|
453
|
+
for (const row of validRows) {
|
|
454
|
+
const sessionId = row.sessionId || 'unknown';
|
|
455
|
+
const current = sessions.get(sessionId) || {
|
|
456
|
+
sessionId,
|
|
457
|
+
count: 0,
|
|
458
|
+
latestMtime: null,
|
|
459
|
+
maxCoveredUntilMessageIndex: null,
|
|
460
|
+
};
|
|
461
|
+
current.count += 1;
|
|
462
|
+
current.latestMtime = !current.latestMtime || String(row.mtime).localeCompare(current.latestMtime) > 0
|
|
463
|
+
? row.mtime
|
|
464
|
+
: current.latestMtime;
|
|
465
|
+
const covered = row.coverage?.coveredUntilMessageIndex;
|
|
466
|
+
if (Number.isFinite(Number(covered))) {
|
|
467
|
+
current.maxCoveredUntilMessageIndex = Math.max(
|
|
468
|
+
current.maxCoveredUntilMessageIndex ?? Number(covered),
|
|
469
|
+
Number(covered),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
sessions.set(sessionId, current);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
status: 'ok',
|
|
476
|
+
spoolDir: dir,
|
|
477
|
+
count: rows.length,
|
|
478
|
+
returned: Math.min(rows.length, limit),
|
|
479
|
+
sessionCount: sessions.size,
|
|
480
|
+
totalBytes: rows.reduce((sum, row) => sum + Number(row.bytes || 0), 0),
|
|
481
|
+
latestMtime: rows[0]?.mtime || null,
|
|
482
|
+
files: rows.slice(0, limit),
|
|
483
|
+
sessions: Array.from(sessions.values())
|
|
484
|
+
.sort((a, b) => String(b.latestMtime || '').localeCompare(String(a.latestMtime || ''))),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
380
488
|
function shellQuote(value) {
|
|
381
489
|
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
|
382
490
|
}
|
|
@@ -506,6 +614,7 @@ module.exports = {
|
|
|
506
614
|
inspectCheckpointHeartbeatHook,
|
|
507
615
|
isoAt,
|
|
508
616
|
loadRuntimeConfig,
|
|
617
|
+
listCheckpointSpool,
|
|
509
618
|
mergeCheckpointHeartbeatHook,
|
|
510
619
|
readCheckpointMarker,
|
|
511
620
|
readHooksConfig,
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
cmdCheckpointHeartbeat,
|
|
12
12
|
cmdCheckpointHeartbeatHook,
|
|
13
13
|
cmdCheckpointPrompt,
|
|
14
|
+
cmdCheckpointSpoolStatus,
|
|
14
15
|
cmdCheckpointTick,
|
|
15
16
|
} = require('./codex-checkpoint-commands');
|
|
16
17
|
const {
|
|
@@ -29,6 +30,7 @@ const {
|
|
|
29
30
|
defaultHooksPath,
|
|
30
31
|
findNewestJsonlFile,
|
|
31
32
|
inspectCheckpointHeartbeatHook,
|
|
33
|
+
listCheckpointSpool,
|
|
32
34
|
loadRuntimeConfig,
|
|
33
35
|
mergeCheckpointHeartbeatHook,
|
|
34
36
|
readCheckpointMarker,
|
|
@@ -38,6 +40,7 @@ const {
|
|
|
38
40
|
writeSchedulerMarker,
|
|
39
41
|
} = require('./codex-checkpoint-runtime');
|
|
40
42
|
const DB_ENV_KEYS = new Set(['DATABASE_URL', 'AQUIFER_DB_URL', 'AQUIFER_SCHEMA', 'AQUIFER_TENANT_ID']);
|
|
43
|
+
const DECISION_VERDICTS = ['declined', 'deferred', 'skipped'];
|
|
41
44
|
|
|
42
45
|
const VALUE_FLAGS = new Set([
|
|
43
46
|
'agent-id',
|
|
@@ -59,6 +62,7 @@ const VALUE_FLAGS = new Set([
|
|
|
59
62
|
'hook-event-name',
|
|
60
63
|
'hooks-path',
|
|
61
64
|
'idle-ms',
|
|
65
|
+
'limit',
|
|
62
66
|
'max-checkpoint-bytes',
|
|
63
67
|
'max-checkpoint-chars',
|
|
64
68
|
'max-checkpoint-messages',
|
|
@@ -575,8 +579,8 @@ async function cmdFinalize(aquifer, flags, opts) {
|
|
|
575
579
|
|
|
576
580
|
async function cmdDecision(aquifer, flags, opts) {
|
|
577
581
|
const verdict = flags.verdict;
|
|
578
|
-
if (!
|
|
579
|
-
throw new Error(
|
|
582
|
+
if (!DECISION_VERDICTS.includes(verdict)) {
|
|
583
|
+
throw new Error(`decision requires --verdict ${DECISION_VERDICTS.join('|')}`);
|
|
580
584
|
}
|
|
581
585
|
const candidates = await listOperationalCandidates(aquifer, opts);
|
|
582
586
|
if (flags.all === true) {
|
|
@@ -664,10 +668,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
664
668
|
node scripts/codex-recovery.js checkpoint-prompt --file-path FILE --scope-key KEY [options]
|
|
665
669
|
node scripts/codex-recovery.js checkpoint-tick --scope-key KEY [--file-path FILE|--sessions-dir DIR] [options]
|
|
666
670
|
node scripts/codex-recovery.js checkpoint-heartbeat --hook-stdin --scope-key KEY [options]
|
|
671
|
+
node scripts/codex-recovery.js checkpoint-spool-status [--json] [--limit N] [--session-id ID]
|
|
667
672
|
node scripts/codex-recovery.js checkpoint-heartbeat-hook --scope-key KEY [--hooks-path FILE] [--apply]
|
|
668
673
|
node scripts/codex-recovery.js finalize --session-id ID --summary-stdin [options]
|
|
669
|
-
node scripts/codex-recovery.js decision --session-id ID --verdict declined|deferred [options]
|
|
670
|
-
node scripts/codex-recovery.js decision --all --verdict declined|deferred [options]
|
|
674
|
+
node scripts/codex-recovery.js decision --session-id ID --verdict declined|deferred|skipped [options]
|
|
675
|
+
node scripts/codex-recovery.js decision --all --verdict declined|deferred|skipped [options]
|
|
671
676
|
node scripts/codex-recovery.js doctor [--strict-wrapper-env] [--json]`);
|
|
672
677
|
return;
|
|
673
678
|
}
|
|
@@ -693,6 +698,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
693
698
|
return;
|
|
694
699
|
}
|
|
695
700
|
|
|
701
|
+
if (command === 'checkpoint-spool-status') {
|
|
702
|
+
await cmdCheckpointSpoolStatus(null, args.flags, opts);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
696
706
|
await withAquifer(async (aquifer) => {
|
|
697
707
|
switch (command) {
|
|
698
708
|
case 'preview':
|
|
@@ -733,6 +743,7 @@ module.exports = {
|
|
|
733
743
|
cmdCheckpointHeartbeat,
|
|
734
744
|
cmdCheckpointHeartbeatHook,
|
|
735
745
|
cmdCheckpointPrompt,
|
|
746
|
+
cmdCheckpointSpoolStatus,
|
|
736
747
|
cmdCheckpointTick,
|
|
737
748
|
cmdPrompt,
|
|
738
749
|
acquireHeartbeatClaim,
|
|
@@ -750,6 +761,7 @@ module.exports = {
|
|
|
750
761
|
defaultHooksPath,
|
|
751
762
|
findNewestJsonlFile,
|
|
752
763
|
inspectCheckpointHeartbeatHook,
|
|
764
|
+
listCheckpointSpool,
|
|
753
765
|
loadRuntimeConfig,
|
|
754
766
|
loadCodexEnv,
|
|
755
767
|
main,
|