@shadowforge0/aquifer-memory 1.6.0 → 1.7.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/README.md +6 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/consumers/cli.js +69 -2
- package/consumers/codex-handoff.js +112 -6
- package/consumers/codex.js +136 -9
- package/core/aquifer.js +8 -0
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-records.js +220 -4
- package/core/session-finalization.js +16 -1
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +44 -3
- package/package.json +2 -2
- package/scripts/codex-recovery.js +141 -1
package/core/memory-records.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { resolveApplicableRecords } = require('./memory-bootstrap');
|
|
4
5
|
|
|
5
6
|
function requireField(obj, field) {
|
|
6
7
|
if (!obj || obj[field] === undefined || obj[field] === null || obj[field] === '') {
|
|
@@ -24,10 +25,10 @@ function advisoryLockKeys(namespace, value) {
|
|
|
24
25
|
const BOOTSTRAP_ORDER_SQL = `
|
|
25
26
|
CASE m.memory_type
|
|
26
27
|
WHEN 'constraint' THEN 0
|
|
27
|
-
WHEN '
|
|
28
|
-
WHEN '
|
|
29
|
-
WHEN '
|
|
30
|
-
WHEN '
|
|
28
|
+
WHEN 'state' THEN 1
|
|
29
|
+
WHEN 'open_loop' THEN 2
|
|
30
|
+
WHEN 'decision' THEN 3
|
|
31
|
+
WHEN 'preference' THEN 4
|
|
31
32
|
WHEN 'fact' THEN 5
|
|
32
33
|
WHEN 'conclusion' THEN 6
|
|
33
34
|
WHEN 'entity_note' THEN 7
|
|
@@ -46,6 +47,123 @@ const BOOTSTRAP_ORDER_SQL = `
|
|
|
46
47
|
m.accepted_at DESC NULLS LAST,
|
|
47
48
|
m.id ASC`;
|
|
48
49
|
|
|
50
|
+
const CURRENT_TYPE_PRIORITY = {
|
|
51
|
+
constraint: 0,
|
|
52
|
+
state: 1,
|
|
53
|
+
open_loop: 2,
|
|
54
|
+
decision: 3,
|
|
55
|
+
preference: 4,
|
|
56
|
+
fact: 5,
|
|
57
|
+
conclusion: 6,
|
|
58
|
+
entity_note: 7,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const CURRENT_AUTHORITY_PRIORITY = {
|
|
62
|
+
user_explicit: 0,
|
|
63
|
+
executable_evidence: 1,
|
|
64
|
+
manual: 2,
|
|
65
|
+
system: 3,
|
|
66
|
+
verified_summary: 4,
|
|
67
|
+
llm_inference: 5,
|
|
68
|
+
raw_transcript: 6,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function parseTime(value) {
|
|
72
|
+
const parsed = Date.parse(value || '');
|
|
73
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeScopePath(activeScopePath, activeScopeKey) {
|
|
77
|
+
const source = Array.isArray(activeScopePath)
|
|
78
|
+
? activeScopePath
|
|
79
|
+
: (typeof activeScopePath === 'string' ? activeScopePath.split(',') : null);
|
|
80
|
+
if (source && source.length > 0) {
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const path = [];
|
|
83
|
+
for (const value of source) {
|
|
84
|
+
const key = String(value || '').trim();
|
|
85
|
+
if (!key || seen.has(key)) continue;
|
|
86
|
+
seen.add(key);
|
|
87
|
+
path.push(key);
|
|
88
|
+
}
|
|
89
|
+
if (path.length > 0) return path;
|
|
90
|
+
}
|
|
91
|
+
if (activeScopeKey) return [String(activeScopeKey).trim()].filter(Boolean);
|
|
92
|
+
return ['global'];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compareRecordIdAsc(a, b) {
|
|
96
|
+
const left = a.memoryId ?? a.memory_id ?? a.id ?? null;
|
|
97
|
+
const right = b.memoryId ?? b.memory_id ?? b.id ?? null;
|
|
98
|
+
const leftNum = Number(left);
|
|
99
|
+
const rightNum = Number(right);
|
|
100
|
+
if (Number.isFinite(leftNum) && Number.isFinite(rightNum) && leftNum !== rightNum) {
|
|
101
|
+
return leftNum - rightNum;
|
|
102
|
+
}
|
|
103
|
+
return String(left ?? '').localeCompare(String(right ?? ''));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeCurrentMemoryRow(row = {}) {
|
|
107
|
+
const memoryId = row.memoryId ?? row.memory_id ?? row.id ?? null;
|
|
108
|
+
const evidenceRefsValue = row.evidenceRefs ?? row.evidence_refs ?? [];
|
|
109
|
+
const evidenceRefs = Array.isArray(evidenceRefsValue) ? evidenceRefsValue : [];
|
|
110
|
+
return {
|
|
111
|
+
...row,
|
|
112
|
+
memoryId: memoryId === null ? null : String(memoryId),
|
|
113
|
+
canonicalKey: row.canonicalKey ?? row.canonical_key ?? null,
|
|
114
|
+
memoryType: row.memoryType ?? row.memory_type ?? null,
|
|
115
|
+
scopeKey: row.scopeKey ?? row.scope_key ?? null,
|
|
116
|
+
scopeKind: row.scopeKind ?? row.scope_kind ?? null,
|
|
117
|
+
inheritanceMode: row.inheritanceMode ?? row.inheritance_mode ?? row.scope_inheritance_mode ?? null,
|
|
118
|
+
visibleInBootstrap: row.visibleInBootstrap ?? row.visible_in_bootstrap ?? false,
|
|
119
|
+
visibleInRecall: row.visibleInRecall ?? row.visible_in_recall ?? false,
|
|
120
|
+
acceptedAt: row.acceptedAt ?? row.accepted_at ?? null,
|
|
121
|
+
validFrom: row.validFrom ?? row.valid_from ?? null,
|
|
122
|
+
validTo: row.validTo ?? row.valid_to ?? null,
|
|
123
|
+
staleAfter: row.staleAfter ?? row.stale_after ?? null,
|
|
124
|
+
evidenceRefs,
|
|
125
|
+
evidence_refs: evidenceRefs,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function currentScopePriority(record, positions) {
|
|
130
|
+
return positions.get(record.scopeKey ?? record.scope_key) ?? -1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sortCurrentMemoryRecords(a, b, positions) {
|
|
134
|
+
const leftScope = currentScopePriority(a, positions);
|
|
135
|
+
const rightScope = currentScopePriority(b, positions);
|
|
136
|
+
if (rightScope !== leftScope) return rightScope - leftScope;
|
|
137
|
+
|
|
138
|
+
const leftType = CURRENT_TYPE_PRIORITY[a.memoryType ?? a.memory_type] ?? 99;
|
|
139
|
+
const rightType = CURRENT_TYPE_PRIORITY[b.memoryType ?? b.memory_type] ?? 99;
|
|
140
|
+
if (leftType !== rightType) return leftType - rightType;
|
|
141
|
+
|
|
142
|
+
const leftAuthority = CURRENT_AUTHORITY_PRIORITY[a.authority] ?? 99;
|
|
143
|
+
const rightAuthority = CURRENT_AUTHORITY_PRIORITY[b.authority] ?? 99;
|
|
144
|
+
if (leftAuthority !== rightAuthority) return leftAuthority - rightAuthority;
|
|
145
|
+
|
|
146
|
+
const leftAccepted = parseTime(a.acceptedAt ?? a.accepted_at);
|
|
147
|
+
const rightAccepted = parseTime(b.acceptedAt ?? b.accepted_at);
|
|
148
|
+
if (leftAccepted !== rightAccepted) return (rightAccepted ?? 0) - (leftAccepted ?? 0);
|
|
149
|
+
|
|
150
|
+
return compareRecordIdAsc(a, b);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isCurrentProjectionRow(row, asOf) {
|
|
154
|
+
if ((row.status || 'candidate') !== 'active') return false;
|
|
155
|
+
if (row.visibleInBootstrap !== true && row.visibleInRecall !== true) return false;
|
|
156
|
+
const at = parseTime(asOf);
|
|
157
|
+
if (at === null) return true;
|
|
158
|
+
const validFrom = parseTime(row.validFrom ?? row.valid_from);
|
|
159
|
+
const validTo = parseTime(row.validTo ?? row.valid_to);
|
|
160
|
+
const staleAfter = parseTime(row.staleAfter ?? row.stale_after);
|
|
161
|
+
if (validFrom !== null && validFrom > at) return false;
|
|
162
|
+
if (validTo !== null && validTo <= at) return false;
|
|
163
|
+
if (staleAfter !== null && staleAfter <= at) return false;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
49
167
|
function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = false }) {
|
|
50
168
|
const scopes = `${schema}.scopes`;
|
|
51
169
|
const versions = `${schema}.versions`;
|
|
@@ -534,6 +652,103 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
534
652
|
return result.rows;
|
|
535
653
|
}
|
|
536
654
|
|
|
655
|
+
async function currentProjection(input = {}) {
|
|
656
|
+
const tenantId = input.tenantId || defaultTenantId;
|
|
657
|
+
let activeScopePath = normalizeScopePath(input.activeScopePath, input.activeScopeKey);
|
|
658
|
+
let activeScopeKey = input.activeScopeKey || activeScopePath[activeScopePath.length - 1] || null;
|
|
659
|
+
if (input.scopeId && !input.activeScopeKey && !input.activeScopePath) {
|
|
660
|
+
const scopeResult = await pool.query(
|
|
661
|
+
`SELECT scope_key FROM ${scopes} WHERE tenant_id = $1 AND id = $2 LIMIT 1`,
|
|
662
|
+
[tenantId, input.scopeId],
|
|
663
|
+
);
|
|
664
|
+
const scopedKey = scopeResult.rows[0]?.scope_key || null;
|
|
665
|
+
if (scopedKey) {
|
|
666
|
+
activeScopePath = [scopedKey];
|
|
667
|
+
activeScopeKey = scopedKey;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const limit = Math.max(1, Math.min(100, input.limit || 50));
|
|
671
|
+
const fetchLimit = Math.max(limit + 1, Math.min(200, Math.max(limit * 4, 40)));
|
|
672
|
+
const asOf = input.asOf || new Date().toISOString();
|
|
673
|
+
const params = [tenantId, activeScopePath, asOf];
|
|
674
|
+
const where = [
|
|
675
|
+
`m.tenant_id = $1`,
|
|
676
|
+
`m.status = 'active'`,
|
|
677
|
+
`s.scope_key = ANY($2::text[])`,
|
|
678
|
+
`(m.visible_in_bootstrap = true OR m.visible_in_recall = true)`,
|
|
679
|
+
`(m.valid_from IS NULL OR m.valid_from <= $3::timestamptz)`,
|
|
680
|
+
`(m.valid_to IS NULL OR m.valid_to > $3::timestamptz)`,
|
|
681
|
+
`(m.stale_after IS NULL OR m.stale_after > $3::timestamptz)`,
|
|
682
|
+
];
|
|
683
|
+
|
|
684
|
+
if (input.scopeId) {
|
|
685
|
+
params.push(input.scopeId);
|
|
686
|
+
where.push(`m.scope_id = $${params.length}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
params.push(fetchLimit);
|
|
690
|
+
const limitParam = `$${params.length}`;
|
|
691
|
+
const evidenceRefsSelect = input.includeEvidenceRefs === true
|
|
692
|
+
? `COALESCE((
|
|
693
|
+
SELECT jsonb_agg(
|
|
694
|
+
jsonb_build_object(
|
|
695
|
+
'id', e.id,
|
|
696
|
+
'sourceKind', e.source_kind,
|
|
697
|
+
'sourceRef', e.source_ref,
|
|
698
|
+
'relationKind', e.relation_kind,
|
|
699
|
+
'weight', e.weight,
|
|
700
|
+
'metadata', e.metadata
|
|
701
|
+
)
|
|
702
|
+
ORDER BY e.id ASC
|
|
703
|
+
)
|
|
704
|
+
FROM ${evidenceRefs} e
|
|
705
|
+
WHERE e.tenant_id = m.tenant_id
|
|
706
|
+
AND e.owner_kind = 'memory_record'
|
|
707
|
+
AND e.owner_id = m.id
|
|
708
|
+
), '[]'::jsonb)`
|
|
709
|
+
: `'[]'::jsonb`;
|
|
710
|
+
|
|
711
|
+
const result = await pool.query(
|
|
712
|
+
`SELECT
|
|
713
|
+
m.*,
|
|
714
|
+
s.scope_kind,
|
|
715
|
+
s.scope_key,
|
|
716
|
+
s.inheritance_mode AS scope_inheritance_mode,
|
|
717
|
+
${evidenceRefsSelect} AS evidence_refs
|
|
718
|
+
FROM ${memories} m
|
|
719
|
+
JOIN ${scopes} s ON s.id = m.scope_id
|
|
720
|
+
WHERE ${where.join(' AND ')}
|
|
721
|
+
ORDER BY array_position($2::text[], s.scope_key) DESC NULLS LAST,
|
|
722
|
+
${BOOTSTRAP_ORDER_SQL}
|
|
723
|
+
LIMIT ${limitParam}`,
|
|
724
|
+
params,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const positions = new Map(activeScopePath.map((key, index) => [key, index]));
|
|
728
|
+
const applicable = resolveApplicableRecords(
|
|
729
|
+
result.rows
|
|
730
|
+
.map(normalizeCurrentMemoryRow)
|
|
731
|
+
.filter(row => isCurrentProjectionRow(row, asOf)),
|
|
732
|
+
{ activeScopeKey, activeScopePath },
|
|
733
|
+
).sort((left, right) => sortCurrentMemoryRecords(left, right, positions));
|
|
734
|
+
|
|
735
|
+
const selected = applicable.slice(0, limit);
|
|
736
|
+
const truncated = applicable.length > limit;
|
|
737
|
+
return {
|
|
738
|
+
memories: selected,
|
|
739
|
+
meta: {
|
|
740
|
+
source: 'memory_records',
|
|
741
|
+
servingContract: 'current_memory_v1',
|
|
742
|
+
count: selected.length,
|
|
743
|
+
activeScopeKey,
|
|
744
|
+
activeScopePath,
|
|
745
|
+
asOf,
|
|
746
|
+
truncated,
|
|
747
|
+
degraded: truncated,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
537
752
|
async function withTransaction(fn) {
|
|
538
753
|
if (inTransaction) {
|
|
539
754
|
return fn(api, { transactional: true });
|
|
@@ -572,6 +787,7 @@ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = fa
|
|
|
572
787
|
updateMemoryStatusIfCurrent,
|
|
573
788
|
updateFactAssertionStatus,
|
|
574
789
|
listActive,
|
|
790
|
+
currentProjection,
|
|
575
791
|
withTransaction,
|
|
576
792
|
};
|
|
577
793
|
|
|
@@ -40,6 +40,20 @@ function summarizeMemoryResults(results = [], extra = {}) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function decorateCandidates(candidates = [], input = {}) {
|
|
44
|
+
const candidatePayload = input.candidatePayload && typeof input.candidatePayload === 'object'
|
|
45
|
+
? input.candidatePayload
|
|
46
|
+
: null;
|
|
47
|
+
if (!candidatePayload) return candidates;
|
|
48
|
+
return candidates.map(candidate => ({
|
|
49
|
+
...candidate,
|
|
50
|
+
payload: {
|
|
51
|
+
...(candidate.payload || {}),
|
|
52
|
+
...candidatePayload,
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
function normalizeFinalizationInput(input = {}, defaults = {}) {
|
|
44
58
|
const tenantId = input.tenantId || defaults.defaultTenantId || 'default';
|
|
45
59
|
return {
|
|
@@ -227,7 +241,7 @@ function createSessionFinalization({
|
|
|
227
241
|
phase: base.phase,
|
|
228
242
|
},
|
|
229
243
|
}];
|
|
230
|
-
const
|
|
244
|
+
const rawCandidates = Array.isArray(input.candidates)
|
|
231
245
|
? input.candidates
|
|
232
246
|
: promotion.extractCandidates({
|
|
233
247
|
sessionId: base.sessionId,
|
|
@@ -239,6 +253,7 @@ function createSessionFinalization({
|
|
|
239
253
|
authority: input.authority || 'verified_summary',
|
|
240
254
|
evidenceRefs,
|
|
241
255
|
});
|
|
256
|
+
const candidates = decorateCandidates(rawCandidates, input);
|
|
242
257
|
|
|
243
258
|
const memoryResults = candidates.length > 0
|
|
244
259
|
? await promotion.promote(candidates, {
|
package/docs/getting-started.md
CHANGED
|
@@ -83,9 +83,15 @@ For first rollout, keep `AQUIFER_MEMORY_SERVING_MODE=legacy`. Switch to `curated
|
|
|
83
83
|
| Start MCP server | `npx aquifer mcp` |
|
|
84
84
|
| Search memory | `npx aquifer recall "auth middleware"` |
|
|
85
85
|
| Plan curated compaction | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
|
|
86
|
+
| Generate a timer synthesis prompt | `npx aquifer operator compaction daily --include-synthesis-prompt --json` |
|
|
87
|
+
| Apply reviewed timer synthesis candidates | `npx aquifer operator compaction daily --synthesis-summary-file /tmp/timer-summary.json --apply --promote-candidates --json` |
|
|
86
88
|
| Show stats | `npx aquifer stats` |
|
|
87
89
|
| Enrich pending sessions | `npx aquifer backfill` |
|
|
88
90
|
|
|
91
|
+
Timer synthesis is an operator-reviewed candidate workflow. The prompt output
|
|
92
|
+
and summary JSON do not become active curated memory unless the apply step is
|
|
93
|
+
run with `--promote-candidates`.
|
|
94
|
+
|
|
89
95
|
The default public serving mode is `legacy`. To test scoped curated memory serving, set `AQUIFER_MEMORY_SERVING_MODE=curated` plus `AQUIFER_MEMORY_ACTIVE_SCOPE_KEY` or `AQUIFER_MEMORY_ACTIVE_SCOPE_PATH`. Rollback is config-only: set the serving mode back to `legacy` and restart the MCP/CLI process.
|
|
90
96
|
|
|
91
97
|
## If something fails
|
package/docs/setup.md
CHANGED
|
@@ -213,6 +213,45 @@ Do **not** use the OpenClaw plugin (`consumers/openclaw-plugin.js`) for tool del
|
|
|
213
213
|
|
|
214
214
|
Curated serving rollback is config-only: set `AQUIFER_MEMORY_SERVING_MODE=legacy` and restart the MCP/CLI process. No destructive database rollback is required.
|
|
215
215
|
|
|
216
|
+
## Operator compaction and timer synthesis
|
|
217
|
+
|
|
218
|
+
Compaction jobs are operator-safe by default. A dry-run plans lifecycle updates
|
|
219
|
+
and candidate output without writing active memory:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npx aquifer operator compaction daily --include-synthesis-prompt --json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
If an operator or external model reviews that prompt and returns timer synthesis
|
|
226
|
+
JSON, attach it back to the plan with:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx aquifer operator compaction daily \
|
|
230
|
+
--synthesis-summary-file /tmp/timer-summary.json \
|
|
231
|
+
--apply \
|
|
232
|
+
--promote-candidates \
|
|
233
|
+
--json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The summary file must match the normal structured summary shape, for example:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"summaryText": "Reviewed timer synthesis.",
|
|
241
|
+
"structuredSummary": {
|
|
242
|
+
"states": [
|
|
243
|
+
{ "state": "The reviewed state that should continue into current memory." }
|
|
244
|
+
],
|
|
245
|
+
"decisions": [],
|
|
246
|
+
"open_loops": []
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Without `--promote-candidates`, synthesis output is recorded as candidate
|
|
252
|
+
ledger material only. The prompt and summary file are producer material; active
|
|
253
|
+
curated memory still requires the explicit promotion gate.
|
|
254
|
+
|
|
216
255
|
## Release verification gates
|
|
217
256
|
|
|
218
257
|
For the publish-surface checks:
|
|
@@ -222,13 +261,15 @@ node --test test/package-surface.test.js test/mcp-manifest.test.js
|
|
|
222
261
|
npm pack --dry-run --json
|
|
223
262
|
```
|
|
224
263
|
|
|
225
|
-
For the real DB-backed
|
|
264
|
+
For the real DB-backed release gate:
|
|
226
265
|
|
|
227
266
|
```bash
|
|
228
|
-
AQUIFER_TEST_DB_URL="postgresql://..."
|
|
267
|
+
AQUIFER_TEST_DB_URL="postgresql://..." npm run test:release:db
|
|
229
268
|
```
|
|
230
269
|
|
|
231
|
-
That DB-backed test is the release proof that the stdio MCP server,
|
|
270
|
+
That DB-backed test is the release proof that the stdio MCP server, CLI
|
|
271
|
+
consumer, Codex finalization serving path, current MCP manifest, and PostgreSQL
|
|
272
|
+
path still line up on a live database.
|
|
232
273
|
|
|
233
274
|
## Troubleshooting
|
|
234
275
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowforge0/aquifer-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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": [
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"scripts": {
|
|
68
68
|
"test": "node --test test/*.test.js",
|
|
69
69
|
"test:integration": "node --test test/integration.test.js",
|
|
70
|
-
"test:release:package": "node --test test/package-surface.test.js test/mcp-manifest.test.js",
|
|
70
|
+
"test:release:package": "node --test test/package-surface.test.js test/mcp-manifest.test.js test/v1-serving-cutover.test.js test/v1-current-memory-contract.test.js test/consumer-codex.test.js test/codex-handoff.test.js",
|
|
71
71
|
"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/consumer-mcp.integration.test.js test/consumer-cli.integration.test.js test/codex-finalization-serving.integration.test.js",
|
|
72
72
|
"lint": "eslint index.js core/*.js consumers/cli.js consumers/mcp.js consumers/claude-code.js consumers/codex.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",
|
|
73
73
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
@@ -36,6 +36,11 @@ const VALUE_FLAGS = new Set([
|
|
|
36
36
|
'summary-json',
|
|
37
37
|
'summary-text',
|
|
38
38
|
'verdict',
|
|
39
|
+
'workspace',
|
|
40
|
+
'workspace-path',
|
|
41
|
+
'project',
|
|
42
|
+
'project-key',
|
|
43
|
+
'repo-path',
|
|
39
44
|
]);
|
|
40
45
|
|
|
41
46
|
function parseArgs(argv) {
|
|
@@ -109,6 +114,9 @@ function buildRecoveryOptions(flags = {}, env = process.env) {
|
|
|
109
114
|
agentId: flags['agent-id'] || envDefault(env, 'CODEX_AQUIFER_AGENT_ID', 'AQUIFER_AGENT_ID') || 'main',
|
|
110
115
|
source: flags.source || envDefault(env, 'CODEX_AQUIFER_SOURCE', 'AQUIFER_SOURCE') || 'codex',
|
|
111
116
|
sessionKey: flags['session-key'] || envDefault(env, 'CODEX_AQUIFER_SESSION_KEY') || 'codex:cli',
|
|
117
|
+
workspace: flags.workspace || flags['workspace-path'] || envDefault(env, 'CODEX_AQUIFER_WORKSPACE', 'CODEX_WORKSPACE') || undefined,
|
|
118
|
+
project: flags.project || flags['project-key'] || envDefault(env, 'CODEX_AQUIFER_PROJECT', 'CODEX_PROJECT') || undefined,
|
|
119
|
+
repoPath: flags['repo-path'] || envDefault(env, 'CODEX_AQUIFER_REPO_PATH', 'CODEX_REPO_PATH') || undefined,
|
|
112
120
|
codexHome: flags['codex-home'] || envDefault(env, 'CODEX_HOME') || undefined,
|
|
113
121
|
stateDir: flags['state-dir'] || undefined,
|
|
114
122
|
sessionsDir: flags['sessions-dir'] || undefined,
|
|
@@ -122,6 +130,7 @@ function buildRecoveryOptions(flags = {}, env = process.env) {
|
|
|
122
130
|
includeJsonlPreviews: flags['include-jsonl-previews'] === true,
|
|
123
131
|
includeDeferredRecovery: flags['include-deferred'] === true,
|
|
124
132
|
excludeNewest: flags['include-current'] === true ? false : true,
|
|
133
|
+
strictWrapperEnv: flags['strict-wrapper-env'] === true,
|
|
125
134
|
};
|
|
126
135
|
for (const [key, value] of Object.entries(opts)) {
|
|
127
136
|
if (value === undefined) delete opts[key];
|
|
@@ -129,6 +138,10 @@ function buildRecoveryOptions(flags = {}, env = process.env) {
|
|
|
129
138
|
return opts;
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
function addDoctorCheck(checks, name, status, detail, extra = {}) {
|
|
142
|
+
checks.push({ name, status, detail, ...extra });
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
function shellQuote(value) {
|
|
133
146
|
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
|
134
147
|
}
|
|
@@ -266,6 +279,81 @@ function compactCandidate(candidate = {}) {
|
|
|
266
279
|
};
|
|
267
280
|
}
|
|
268
281
|
|
|
282
|
+
function compactDoctorOptions(opts = {}) {
|
|
283
|
+
return {
|
|
284
|
+
agentId: opts.agentId || 'main',
|
|
285
|
+
source: opts.source || 'codex',
|
|
286
|
+
sessionKey: opts.sessionKey || 'codex:cli',
|
|
287
|
+
workspace: opts.workspace || null,
|
|
288
|
+
project: opts.project || null,
|
|
289
|
+
repoPath: opts.repoPath || null,
|
|
290
|
+
codexHome: opts.codexHome || null,
|
|
291
|
+
sessionsDir: opts.sessionsDir || null,
|
|
292
|
+
stateDir: opts.stateDir || null,
|
|
293
|
+
excludeNewest: opts.excludeNewest !== false,
|
|
294
|
+
includeDeferredRecovery: opts.includeDeferredRecovery === true,
|
|
295
|
+
maxRecoveryCandidates: opts.maxRecoveryCandidates || null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function buildDoctorReport(aquifer, opts = {}, env = process.env) {
|
|
300
|
+
const checks = [];
|
|
301
|
+
const hasWrapperEnv = Boolean(
|
|
302
|
+
env.CODEX_AQUIFER_AGENT_ID
|
|
303
|
+
|| env.CODEX_AQUIFER_SOURCE
|
|
304
|
+
|| env.CODEX_AQUIFER_SESSION_KEY
|
|
305
|
+
|| env.CODEX_HOME
|
|
306
|
+
|| env.CODEX_ENV_PATH,
|
|
307
|
+
);
|
|
308
|
+
if (hasWrapperEnv) {
|
|
309
|
+
addDoctorCheck(checks, 'wrapper_env', 'ok', 'Codex wrapper env is present.');
|
|
310
|
+
} else if (opts.strictWrapperEnv) {
|
|
311
|
+
addDoctorCheck(checks, 'wrapper_env', 'fail', 'Strict wrapper env requested, but no CODEX_AQUIFER_* or CODEX_HOME env was found.');
|
|
312
|
+
} else {
|
|
313
|
+
addDoctorCheck(checks, 'wrapper_env', 'warn', 'Using CLI defaults; pass --strict-wrapper-env for live wrapper deployment checks.');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (opts.excludeNewest === false) {
|
|
317
|
+
addDoctorCheck(checks, 'current_transcript_guard', 'fail', 'Current/newest transcript exclusion is disabled.');
|
|
318
|
+
} else {
|
|
319
|
+
addDoctorCheck(checks, 'current_transcript_guard', 'ok', 'Newest transcript exclusion is enabled.');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let candidates = [];
|
|
323
|
+
try {
|
|
324
|
+
candidates = await listDbEligibleCandidates(aquifer, {
|
|
325
|
+
...opts,
|
|
326
|
+
idleMs: opts.idleMs ?? 0,
|
|
327
|
+
includeJsonlPreviews: true,
|
|
328
|
+
maxRecoveryCandidates: opts.maxRecoveryCandidates || 1,
|
|
329
|
+
});
|
|
330
|
+
addDoctorCheck(
|
|
331
|
+
checks,
|
|
332
|
+
'sessionstart_preflight',
|
|
333
|
+
'ok',
|
|
334
|
+
`Metadata-only recovery scan completed; eligibleCandidates=${candidates.length}.`,
|
|
335
|
+
{ eligibleCandidates: candidates.length },
|
|
336
|
+
);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
addDoctorCheck(
|
|
339
|
+
checks,
|
|
340
|
+
'sessionstart_preflight',
|
|
341
|
+
'fail',
|
|
342
|
+
err && err.message ? err.message : String(err),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const status = checks.some(check => check.status === 'fail')
|
|
347
|
+
? 'fail'
|
|
348
|
+
: checks.some(check => check.status === 'warn') ? 'warn' : 'ok';
|
|
349
|
+
return {
|
|
350
|
+
status,
|
|
351
|
+
checks,
|
|
352
|
+
options: compactDoctorOptions(opts),
|
|
353
|
+
candidates: candidates.map(compactCandidate),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
269
357
|
function parseIdList(value) {
|
|
270
358
|
if (!value || value === true) return new Set();
|
|
271
359
|
return new Set(String(value).split(',').map(part => part.trim()).filter(Boolean));
|
|
@@ -467,6 +555,42 @@ async function cmdDecision(aquifer, flags, opts) {
|
|
|
467
555
|
console.log(`Recovery ${verdict}: ${candidate.sessionId}`);
|
|
468
556
|
}
|
|
469
557
|
|
|
558
|
+
async function cmdDoctor(aquifer, flags, opts, env = process.env) {
|
|
559
|
+
const report = await buildDoctorReport(aquifer, opts, env);
|
|
560
|
+
printDoctorReport(report, flags);
|
|
561
|
+
return report;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function printDoctorReport(report = {}, flags = {}) {
|
|
565
|
+
if (flags.json) {
|
|
566
|
+
console.log(JSON.stringify(report, null, 2));
|
|
567
|
+
} else {
|
|
568
|
+
console.log(`Codex recovery doctor: ${report.status}`);
|
|
569
|
+
for (const check of report.checks || []) {
|
|
570
|
+
console.log(`- ${check.status} ${check.name}: ${check.detail}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (report.status === 'fail') process.exitCode = 1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function cmdDoctorInitFailure(flags, opts, err, env = process.env) {
|
|
577
|
+
let report = await buildDoctorReport(null, opts, env);
|
|
578
|
+
report = {
|
|
579
|
+
...report,
|
|
580
|
+
status: 'fail',
|
|
581
|
+
checks: [
|
|
582
|
+
{
|
|
583
|
+
name: 'aquifer_init',
|
|
584
|
+
status: 'fail',
|
|
585
|
+
detail: err && err.message ? err.message : String(err),
|
|
586
|
+
},
|
|
587
|
+
...(report.checks || []),
|
|
588
|
+
],
|
|
589
|
+
};
|
|
590
|
+
printDoctorReport(report, flags);
|
|
591
|
+
return report;
|
|
592
|
+
}
|
|
593
|
+
|
|
470
594
|
async function main(argv = process.argv.slice(2)) {
|
|
471
595
|
const args = parseArgs(argv);
|
|
472
596
|
const command = args._[0] || 'help';
|
|
@@ -480,7 +604,19 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
480
604
|
node scripts/codex-recovery.js prompt --session-id ID [options]
|
|
481
605
|
node scripts/codex-recovery.js finalize --session-id ID --summary-stdin [options]
|
|
482
606
|
node scripts/codex-recovery.js decision --session-id ID --verdict declined|deferred [options]
|
|
483
|
-
node scripts/codex-recovery.js decision --all --verdict declined|deferred [options]
|
|
607
|
+
node scripts/codex-recovery.js decision --all --verdict declined|deferred [options]
|
|
608
|
+
node scripts/codex-recovery.js doctor [--strict-wrapper-env] [--json]`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (command === 'doctor') {
|
|
613
|
+
try {
|
|
614
|
+
await withAquifer(async (aquifer) => {
|
|
615
|
+
await cmdDoctor(aquifer, args.flags, opts);
|
|
616
|
+
});
|
|
617
|
+
} catch (err) {
|
|
618
|
+
await cmdDoctorInitFailure(args.flags, opts, err);
|
|
619
|
+
}
|
|
484
620
|
return;
|
|
485
621
|
}
|
|
486
622
|
|
|
@@ -508,12 +644,16 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
508
644
|
}
|
|
509
645
|
|
|
510
646
|
module.exports = {
|
|
647
|
+
buildDoctorReport,
|
|
511
648
|
buildRecoveryOptions,
|
|
512
649
|
cmdDecision,
|
|
650
|
+
cmdDoctor,
|
|
651
|
+
cmdDoctorInitFailure,
|
|
513
652
|
cmdFinalize,
|
|
514
653
|
cmdHookContext,
|
|
515
654
|
cmdPrompt,
|
|
516
655
|
loadCodexEnv,
|
|
656
|
+
main,
|
|
517
657
|
parseArgs,
|
|
518
658
|
renderFinalizeCommand,
|
|
519
659
|
renderHookContext,
|