@shadowforge0/aquifer-memory 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/README.md +82 -26
- package/README_CN.md +33 -23
- package/README_TW.md +25 -24
- package/aquifer.config.example.json +2 -1
- package/consumers/cli.js +587 -33
- package/consumers/codex-active-checkpoint.js +3 -1
- package/consumers/codex-current-memory.js +10 -6
- package/consumers/codex.js +6 -3
- package/consumers/default/daily-entries.js +2 -2
- package/consumers/default/index.js +40 -30
- package/consumers/default/prompts/summary.js +2 -2
- package/consumers/mcp.js +56 -46
- package/consumers/openclaw-ext/index.js +65 -7
- 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 +105 -24
- package/consumers/shared/compat-recall.js +101 -0
- package/consumers/shared/config.js +2 -0
- package/consumers/shared/openclaw-product-tools.js +130 -0
- package/consumers/shared/recall-format.js +2 -2
- package/core/aquifer.js +553 -41
- package/core/backends/local.js +169 -1
- package/core/doctor.js +924 -0
- package/core/finalization-inspector.js +164 -0
- package/core/finalization-review.js +88 -42
- package/core/interface.js +629 -0
- package/core/mcp-manifest.js +11 -3
- package/core/memory-bootstrap.js +25 -27
- package/core/memory-consolidation.js +564 -42
- package/core/memory-explain.js +593 -0
- package/core/memory-promotion.js +392 -55
- package/core/memory-recall.js +75 -71
- package/core/memory-records.js +107 -108
- package/core/memory-review.js +891 -0
- package/core/memory-serving.js +61 -4
- package/core/memory-type-policy.js +298 -0
- package/core/operator-observability.js +249 -0
- package/core/postgres-migrations.js +22 -0
- package/core/session-checkpoint-producer.js +3 -1
- package/core/session-checkpoints.js +1 -1
- package/core/session-finalization.js +78 -3
- package/core/storage.js +124 -8
- package/docs/getting-started.md +50 -4
- package/docs/setup.md +163 -24
- package/package.json +5 -4
- package/schema/004-completion.sql +4 -4
- package/schema/010-v1-finalization-review.sql +72 -0
- package/schema/019-v1-memory-review-resolutions.sql +53 -0
- package/schema/020-v1-assistant-shaping-memory.sql +30 -0
- package/scripts/backfill-canonical-key.js +1 -1
- package/scripts/codex-checkpoint-commands.js +28 -0
- package/scripts/codex-checkpoint-runtime.js +109 -0
- package/scripts/codex-recovery.js +16 -4
- package/scripts/diagnose-fts-zh.js +1 -1
- package/scripts/extract-insights-from-recent-sessions.js +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
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-assistant-shaping-memory.test.js test/v1-assistant-shaping-review-explain.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": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
-- * consumer_profiles table — consumer schema registry with composite primary key
|
|
10
10
|
-- (tenant_id, consumer_id, version) for future multi-tenant safety
|
|
11
11
|
--
|
|
12
|
-
-- All identifiers stay parameterised on ${schema} so
|
|
13
|
-
--
|
|
12
|
+
-- All identifiers stay parameterised on ${schema} so schema renames remain a
|
|
13
|
+
-- config change rather than a DDL rewrite.
|
|
14
14
|
|
|
15
15
|
-- Ensure pg_trgm available (used by existing migrations; re-declared for independent
|
|
16
16
|
-- run safety).
|
|
@@ -141,8 +141,8 @@ CREATE TRIGGER trg_consumer_profiles_updated_at
|
|
|
141
141
|
EXECUTE FUNCTION ${schema}.set_updated_at();
|
|
142
142
|
|
|
143
143
|
-- timeline_events: append-only event log keyed by (tenant, agent, occurred_at).
|
|
144
|
-
-- category vocabulary is consumer-owned
|
|
145
|
-
--
|
|
144
|
+
-- category vocabulary is consumer-owned, event shape is strict core.
|
|
145
|
+
-- idempotency_key UNIQUE
|
|
146
146
|
-- across the table to make caller-driven dedupe safe.
|
|
147
147
|
CREATE TABLE IF NOT EXISTS ${schema}.timeline_events (
|
|
148
148
|
id BIGSERIAL PRIMARY KEY,
|
|
@@ -43,6 +43,78 @@ ALTER TABLE ${schema}.fact_assertions_v1
|
|
|
43
43
|
'tombstoned','quarantined','archived','incorrect'
|
|
44
44
|
));
|
|
45
45
|
|
|
46
|
+
ALTER TABLE ${schema}.memory_records
|
|
47
|
+
DROP CONSTRAINT IF EXISTS memory_records_lifecycle_consistency_check;
|
|
48
|
+
|
|
49
|
+
ALTER TABLE ${schema}.memory_records
|
|
50
|
+
ADD CONSTRAINT memory_records_lifecycle_consistency_check
|
|
51
|
+
CHECK (
|
|
52
|
+
(valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from)
|
|
53
|
+
AND (revoked_at IS NULL OR accepted_at IS NULL OR revoked_at >= accepted_at)
|
|
54
|
+
AND (superseded_at IS NULL OR accepted_at IS NULL OR superseded_at >= accepted_at)
|
|
55
|
+
AND NOT (revoked_at IS NOT NULL AND superseded_at IS NOT NULL)
|
|
56
|
+
AND (status <> 'active' OR (
|
|
57
|
+
revoked_at IS NULL
|
|
58
|
+
AND superseded_at IS NULL
|
|
59
|
+
AND superseded_by IS NULL
|
|
60
|
+
))
|
|
61
|
+
AND (status <> 'superseded' OR superseded_at IS NOT NULL)
|
|
62
|
+
AND (status <> 'revoked' OR revoked_at IS NOT NULL)
|
|
63
|
+
) NOT VALID;
|
|
64
|
+
|
|
65
|
+
ALTER TABLE ${schema}.fact_assertions_v1
|
|
66
|
+
DROP CONSTRAINT IF EXISTS fact_assertions_v1_lifecycle_consistency_check;
|
|
67
|
+
|
|
68
|
+
ALTER TABLE ${schema}.fact_assertions_v1
|
|
69
|
+
ADD CONSTRAINT fact_assertions_v1_lifecycle_consistency_check
|
|
70
|
+
CHECK (
|
|
71
|
+
(valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from)
|
|
72
|
+
AND (revoked_at IS NULL OR accepted_at IS NULL OR revoked_at >= accepted_at)
|
|
73
|
+
AND (superseded_at IS NULL OR accepted_at IS NULL OR superseded_at >= accepted_at)
|
|
74
|
+
AND NOT (revoked_at IS NOT NULL AND superseded_at IS NOT NULL)
|
|
75
|
+
AND (status <> 'active' OR (
|
|
76
|
+
revoked_at IS NULL
|
|
77
|
+
AND superseded_at IS NULL
|
|
78
|
+
AND superseded_by IS NULL
|
|
79
|
+
))
|
|
80
|
+
AND (status <> 'superseded' OR superseded_at IS NOT NULL)
|
|
81
|
+
AND (status <> 'revoked' OR revoked_at IS NOT NULL)
|
|
82
|
+
) NOT VALID;
|
|
83
|
+
|
|
84
|
+
DO $$
|
|
85
|
+
BEGIN
|
|
86
|
+
IF NOT EXISTS (
|
|
87
|
+
SELECT 1
|
|
88
|
+
FROM pg_constraint
|
|
89
|
+
WHERE conrelid = '${schema}.memory_records'::regclass
|
|
90
|
+
AND conname = 'memory_records_superseded_by_tenant_fk'
|
|
91
|
+
) THEN
|
|
92
|
+
ALTER TABLE ${schema}.memory_records
|
|
93
|
+
ADD CONSTRAINT memory_records_superseded_by_tenant_fk
|
|
94
|
+
FOREIGN KEY (tenant_id, superseded_by)
|
|
95
|
+
REFERENCES ${schema}.memory_records (tenant_id, id)
|
|
96
|
+
NOT VALID;
|
|
97
|
+
END IF;
|
|
98
|
+
END;
|
|
99
|
+
$$;
|
|
100
|
+
|
|
101
|
+
DO $$
|
|
102
|
+
BEGIN
|
|
103
|
+
IF NOT EXISTS (
|
|
104
|
+
SELECT 1
|
|
105
|
+
FROM pg_constraint
|
|
106
|
+
WHERE conrelid = '${schema}.fact_assertions_v1'::regclass
|
|
107
|
+
AND conname = 'fact_assertions_v1_superseded_by_tenant_fk'
|
|
108
|
+
) THEN
|
|
109
|
+
ALTER TABLE ${schema}.fact_assertions_v1
|
|
110
|
+
ADD CONSTRAINT fact_assertions_v1_superseded_by_tenant_fk
|
|
111
|
+
FOREIGN KEY (tenant_id, superseded_by)
|
|
112
|
+
REFERENCES ${schema}.fact_assertions_v1 (tenant_id, id)
|
|
113
|
+
NOT VALID;
|
|
114
|
+
END IF;
|
|
115
|
+
END;
|
|
116
|
+
$$;
|
|
117
|
+
|
|
46
118
|
-- =========================================================================
|
|
47
119
|
-- Row-level finalization lineage
|
|
48
120
|
-- =========================================================================
|
|
@@ -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.';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Aquifer v1 assistant-shaping memory layer
|
|
2
|
+
-- Requires: 007-v1-foundation.sql
|
|
3
|
+
-- Usage: replace ${schema} with actual schema name
|
|
4
|
+
--
|
|
5
|
+
-- Adds a product-level memory type for user-facing assistant shaping:
|
|
6
|
+
-- response style, retrieval policy, interaction boundaries, and tool routing.
|
|
7
|
+
-- This remains curated memory, not a host/persona-specific schema concept.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE ${schema}.memory_records
|
|
10
|
+
DROP CONSTRAINT IF EXISTS memory_records_memory_type_check;
|
|
11
|
+
|
|
12
|
+
ALTER TABLE ${schema}.memory_records
|
|
13
|
+
ADD CONSTRAINT memory_records_memory_type_check
|
|
14
|
+
CHECK (memory_type IN (
|
|
15
|
+
'assistant_shaping',
|
|
16
|
+
'fact','state','decision','preference','constraint',
|
|
17
|
+
'entity_note','open_loop','conclusion'
|
|
18
|
+
));
|
|
19
|
+
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_memory_records_assistant_shaping_bootstrap
|
|
21
|
+
ON ${schema}.memory_records (tenant_id, scope_id, status, accepted_at DESC, id)
|
|
22
|
+
WHERE status = 'active'
|
|
23
|
+
AND memory_type = 'assistant_shaping'
|
|
24
|
+
AND visible_in_bootstrap;
|
|
25
|
+
|
|
26
|
+
COMMENT ON CONSTRAINT memory_records_memory_type_check ON ${schema}.memory_records IS
|
|
27
|
+
'Allowed curated memory types. assistant_shaping stores user-facing assistant behavior and retrieval policy, not host-specific persona code.';
|
|
28
|
+
|
|
29
|
+
COMMENT ON INDEX ${schema}.idx_memory_records_assistant_shaping_bootstrap IS
|
|
30
|
+
'Fast path for high-priority assistant-shaping memories during bootstrap materialization.';
|
|
@@ -32,7 +32,7 @@ function printUsageAndExit(code = 0) {
|
|
|
32
32
|
'Usage: node scripts/backfill-canonical-key.js --schema <name> [options]',
|
|
33
33
|
'',
|
|
34
34
|
'Required:',
|
|
35
|
-
' --schema <name> Target schema (e.g.
|
|
35
|
+
' --schema <name> Target schema (e.g. aquifer, app_memory)',
|
|
36
36
|
' --agent <id> Limit to one agent (or use --all-agents)',
|
|
37
37
|
'',
|
|
38
38
|
'Optional:',
|
|
@@ -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,
|
|
@@ -27,7 +27,7 @@ const SCHEMA = process.env.AQUIFER_SCHEMA || 'public';
|
|
|
27
27
|
|
|
28
28
|
const DEFAULT_QUERIES = [
|
|
29
29
|
// latin
|
|
30
|
-
'afterburn', 'bootstrap', 'session', 'recall', 'entity', 'OpenCode', '
|
|
30
|
+
'afterburn', 'bootstrap', 'session', 'recall', 'entity', 'OpenCode', 'ExampleUser', 'Aquifer',
|
|
31
31
|
// CJK short tokens — 最容易暴露 tokenizer 問題
|
|
32
32
|
'記憶', '時區', '去重', '架構', '修復',
|
|
33
33
|
// CJK phrase
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* [--days 14] \
|
|
18
18
|
* [--max-sessions 50] \
|
|
19
19
|
* [--types preference,pattern,frustration,workflow] \
|
|
20
|
-
* [--schema
|
|
20
|
+
* [--schema aquifer] \
|
|
21
21
|
* [--tenant-id default] \
|
|
22
22
|
* [--dry-run]
|
|
23
23
|
*
|
|
@@ -71,7 +71,7 @@ function parseArgs(argv) {
|
|
|
71
71
|
days: 14,
|
|
72
72
|
maxSessions: 50,
|
|
73
73
|
types: ['preference', 'pattern', 'frustration', 'workflow'],
|
|
74
|
-
schema: process.env.AQUIFER_SCHEMA || '
|
|
74
|
+
schema: process.env.AQUIFER_SCHEMA || 'aquifer',
|
|
75
75
|
tenantId: process.env.AQUIFER_TENANT_ID || 'default',
|
|
76
76
|
dryRun: false,
|
|
77
77
|
};
|
|
@@ -109,8 +109,8 @@ themes. Returning only 2-3 on a rich window means you're under-extracting.
|
|
|
109
109
|
Returning 0 is only correct when the window is genuinely sparse.
|
|
110
110
|
|
|
111
111
|
## Insight types
|
|
112
|
-
- preference: stable user preference (e.g. "
|
|
113
|
-
- pattern: recurring behaviour or decision (e.g. "
|
|
112
|
+
- preference: stable user preference (e.g. "The user prefers terse responses with no trailing summaries")
|
|
113
|
+
- pattern: recurring behaviour or decision (e.g. "The user runs a planning workflow before non-trivial schema changes")
|
|
114
114
|
- frustration: repeated pain point (e.g. "Cron jobs.json prompt parse keeps breaking on minor LLM output drift")
|
|
115
115
|
- workflow: reusable procedure that worked (e.g. "Aquifer release: pack tarball -> bump gateway pkg -> migrate -> restart")
|
|
116
116
|
|