@shadowforge0/aquifer-memory 1.5.9 → 1.6.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 +23 -0
- package/README.md +96 -73
- package/README_CN.md +659 -0
- package/README_TW.md +680 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +374 -39
- package/consumers/codex-handoff.js +152 -0
- package/consumers/codex.js +1549 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +131 -7
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +44 -4
- package/consumers/shared/config.js +28 -0
- package/consumers/shared/factory.js +2 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +53 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +384 -18
- package/core/finalization-review.js +319 -0
- package/core/insights.js +210 -58
- package/core/mcp-manifest.js +69 -2
- package/core/memory-bootstrap.js +188 -0
- package/core/memory-consolidation.js +1236 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +581 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +350 -0
- package/core/storage.js +456 -2
- package/docs/getting-started.md +99 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +51 -2
- package/package.json +31 -9
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/backfill-canonical-key.js +250 -0
- package/scripts/codex-recovery.js +532 -0
- package/consumers/miranda/context-inject.js +0 -119
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
- package/scripts/queries.json +0 -45
- package/scripts/retro-recall-bench.js +0 -409
- package/scripts/sample-bench-queries.sql +0 -75
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const { createAquiferFromConfig } = require('../consumers/shared/factory');
|
|
9
|
+
const codex = require('../consumers/codex');
|
|
10
|
+
const DB_ENV_KEYS = new Set(['DATABASE_URL', 'AQUIFER_DB_URL', 'AQUIFER_SCHEMA', 'AQUIFER_TENANT_ID']);
|
|
11
|
+
|
|
12
|
+
const VALUE_FLAGS = new Set([
|
|
13
|
+
'agent-id',
|
|
14
|
+
'codex-home',
|
|
15
|
+
'config',
|
|
16
|
+
'except-session-id',
|
|
17
|
+
'file-path',
|
|
18
|
+
'finalizer-model',
|
|
19
|
+
'idle-ms',
|
|
20
|
+
'max-candidates',
|
|
21
|
+
'max-recovery-bytes',
|
|
22
|
+
'max-recovery-chars',
|
|
23
|
+
'max-recovery-messages',
|
|
24
|
+
'max-recovery-prompt-tokens',
|
|
25
|
+
'min-session-bytes',
|
|
26
|
+
'mode',
|
|
27
|
+
'reason',
|
|
28
|
+
'scope-kind',
|
|
29
|
+
'scope-key',
|
|
30
|
+
'session-id',
|
|
31
|
+
'session-key',
|
|
32
|
+
'sessions-dir',
|
|
33
|
+
'source',
|
|
34
|
+
'state-dir',
|
|
35
|
+
'structured-summary-json',
|
|
36
|
+
'summary-json',
|
|
37
|
+
'summary-text',
|
|
38
|
+
'verdict',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const args = { _: [], flags: {} };
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const current = argv[i];
|
|
45
|
+
if (current === '--') {
|
|
46
|
+
args._.push(...argv.slice(i + 1));
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (current.startsWith('--')) {
|
|
50
|
+
const key = current.slice(2);
|
|
51
|
+
if (VALUE_FLAGS.has(key) && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
52
|
+
args.flags[key] = argv[++i];
|
|
53
|
+
} else {
|
|
54
|
+
args.flags[key] = true;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
args._.push(current);
|
|
59
|
+
}
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseIntFlag(value, fallback) {
|
|
64
|
+
if (value === undefined || value === null || value === true || value === '') return fallback;
|
|
65
|
+
const parsed = parseInt(value, 10);
|
|
66
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function envDefault(env, ...keys) {
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const value = env[key];
|
|
72
|
+
if (value !== undefined && value !== '') return value;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadEnvFile(filePath, env = process.env, opts = {}) {
|
|
78
|
+
if (!filePath) return;
|
|
79
|
+
const overrideKeys = opts.overrideKeys || null;
|
|
80
|
+
let raw = '';
|
|
81
|
+
try {
|
|
82
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
} catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const line of raw.split('\n')) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
89
|
+
const match = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(trimmed);
|
|
90
|
+
if (!match) continue;
|
|
91
|
+
if (env[match[1]] && !(overrideKeys && overrideKeys.has(match[1]))) continue;
|
|
92
|
+
let value = match[2].trim();
|
|
93
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
94
|
+
value = value.slice(1, -1);
|
|
95
|
+
}
|
|
96
|
+
env[match[1]] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function loadCodexEnv(env = process.env, opts = {}) {
|
|
101
|
+
const codexHome = env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
102
|
+
const fileOpts = opts.overrideDb ? { overrideKeys: DB_ENV_KEYS } : {};
|
|
103
|
+
loadEnvFile(env.CODEX_ENV_PATH, env, fileOpts);
|
|
104
|
+
loadEnvFile(path.join(codexHome, '.env'), env, fileOpts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildRecoveryOptions(flags = {}, env = process.env) {
|
|
108
|
+
const opts = {
|
|
109
|
+
agentId: flags['agent-id'] || envDefault(env, 'CODEX_AQUIFER_AGENT_ID', 'AQUIFER_AGENT_ID') || 'main',
|
|
110
|
+
source: flags.source || envDefault(env, 'CODEX_AQUIFER_SOURCE', 'AQUIFER_SOURCE') || 'codex',
|
|
111
|
+
sessionKey: flags['session-key'] || envDefault(env, 'CODEX_AQUIFER_SESSION_KEY') || 'codex:cli',
|
|
112
|
+
codexHome: flags['codex-home'] || envDefault(env, 'CODEX_HOME') || undefined,
|
|
113
|
+
stateDir: flags['state-dir'] || undefined,
|
|
114
|
+
sessionsDir: flags['sessions-dir'] || undefined,
|
|
115
|
+
maxRecoveryCandidates: parseIntFlag(flags['max-candidates'], 1),
|
|
116
|
+
minSessionBytes: parseIntFlag(flags['min-session-bytes'], undefined),
|
|
117
|
+
idleMs: parseIntFlag(flags['idle-ms'], undefined),
|
|
118
|
+
maxRecoveryBytes: parseIntFlag(flags['max-recovery-bytes'], undefined),
|
|
119
|
+
maxRecoveryMessages: parseIntFlag(flags['max-recovery-messages'], undefined),
|
|
120
|
+
maxRecoveryChars: parseIntFlag(flags['max-recovery-chars'], undefined),
|
|
121
|
+
maxRecoveryPromptTokens: parseIntFlag(flags['max-recovery-prompt-tokens'], undefined),
|
|
122
|
+
includeJsonlPreviews: flags['include-jsonl-previews'] === true,
|
|
123
|
+
includeDeferredRecovery: flags['include-deferred'] === true,
|
|
124
|
+
excludeNewest: flags['include-current'] === true ? false : true,
|
|
125
|
+
};
|
|
126
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
127
|
+
if (value === undefined) delete opts[key];
|
|
128
|
+
}
|
|
129
|
+
return opts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shellQuote(value) {
|
|
133
|
+
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function scriptCommand(subcommand, candidate = {}, opts = {}, extra = []) {
|
|
137
|
+
const scriptPath = path.resolve(__filename);
|
|
138
|
+
const parts = [process.execPath, scriptPath, subcommand];
|
|
139
|
+
if (candidate.sessionId) parts.push('--session-id', candidate.sessionId);
|
|
140
|
+
if (opts.agentId) parts.push('--agent-id', opts.agentId);
|
|
141
|
+
if (opts.source) parts.push('--source', opts.source);
|
|
142
|
+
if (opts.sessionKey) parts.push('--session-key', opts.sessionKey);
|
|
143
|
+
if (opts.maxRecoveryCandidates) parts.push('--max-candidates', String(opts.maxRecoveryCandidates));
|
|
144
|
+
parts.push(...extra);
|
|
145
|
+
return parts.map(shellQuote).join(' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function oneLine(value) {
|
|
149
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderCandidate(candidate = {}) {
|
|
153
|
+
const id = candidate.sessionId || candidate.fileSessionId || '(unknown)';
|
|
154
|
+
const counts = [];
|
|
155
|
+
if (candidate.userCount !== null && candidate.userCount !== undefined) counts.push(`${candidate.userCount} user turns`);
|
|
156
|
+
if (candidate.messageCount !== null && candidate.messageCount !== undefined) counts.push(`${candidate.messageCount} messages`);
|
|
157
|
+
if (candidate.approxPromptTokens !== null && candidate.approxPromptTokens !== undefined) counts.push(`~${candidate.approxPromptTokens} prompt tokens`);
|
|
158
|
+
if (candidate.updatedAt) {
|
|
159
|
+
const updated = new Date(candidate.updatedAt);
|
|
160
|
+
if (!Number.isNaN(updated.getTime())) counts.push(`updated ${updated.toISOString()}`);
|
|
161
|
+
}
|
|
162
|
+
return counts.length > 0 ? `${id} (${counts.join(', ')})` : id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderFinalizeCommand(candidate = {}, opts = {}, extra = []) {
|
|
166
|
+
return scriptCommand('finalize', candidate, opts, [
|
|
167
|
+
...recoveryArgsForCandidate(candidate),
|
|
168
|
+
'--summary-stdin',
|
|
169
|
+
'--mode',
|
|
170
|
+
'session_start_recovery',
|
|
171
|
+
...extra,
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function recoveryArgsForCandidate(candidate = {}) {
|
|
176
|
+
return candidate.origin === 'jsonl_preview' ? ['--include-jsonl-previews'] : [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderHookContext(candidates = [], opts = {}) {
|
|
180
|
+
if (!candidates.length) return '';
|
|
181
|
+
|
|
182
|
+
const sharedArgs = candidates.some(candidate => candidate.origin === 'jsonl_preview')
|
|
183
|
+
? ['--include-jsonl-previews']
|
|
184
|
+
: [];
|
|
185
|
+
const deferUnselectedCommand = scriptCommand('decision', {}, opts, [
|
|
186
|
+
...sharedArgs,
|
|
187
|
+
'--all',
|
|
188
|
+
'--except-session-id',
|
|
189
|
+
'SELECTED_IDS_COMMA_SEPARATED',
|
|
190
|
+
'--verdict',
|
|
191
|
+
'deferred',
|
|
192
|
+
'--reason',
|
|
193
|
+
'not_selected_at_session_start',
|
|
194
|
+
]);
|
|
195
|
+
const deferAllCommand = scriptCommand('decision', {}, opts, [
|
|
196
|
+
...sharedArgs,
|
|
197
|
+
'--all',
|
|
198
|
+
'--verdict',
|
|
199
|
+
'deferred',
|
|
200
|
+
'--reason',
|
|
201
|
+
'deferred_by_user_at_session_start',
|
|
202
|
+
]);
|
|
203
|
+
const declineAllCommand = scriptCommand('decision', {}, opts, [
|
|
204
|
+
...sharedArgs,
|
|
205
|
+
'--all',
|
|
206
|
+
'--verdict',
|
|
207
|
+
'declined',
|
|
208
|
+
'--reason',
|
|
209
|
+
'declined_by_user_at_session_start',
|
|
210
|
+
]);
|
|
211
|
+
const candidateLines = [];
|
|
212
|
+
candidates.forEach((candidate, index) => {
|
|
213
|
+
const previewArgs = recoveryArgsForCandidate(candidate);
|
|
214
|
+
const promptCommand = scriptCommand('prompt', candidate, opts, previewArgs);
|
|
215
|
+
const finalizeCommand = renderFinalizeCommand(candidate, opts);
|
|
216
|
+
candidateLines.push(`${index + 1}. ${renderCandidate(candidate)}`);
|
|
217
|
+
candidateLines.push(` prompt: ${promptCommand}`);
|
|
218
|
+
candidateLines.push(` finalize: ${finalizeCommand}`);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return [
|
|
222
|
+
'[AQUIFER RECOVERY]',
|
|
223
|
+
`Aquifer found ${candidates.length} Codex JSONL session(s) eligible for DB recovery.`,
|
|
224
|
+
'This hook scanned local JSONL only to compute eligibility, counts, hashes, and prompt budget. It did not inject transcript text.',
|
|
225
|
+
'Recover all: process every candidate below one at a time with its prompt command, summarize with the current Codex agent, then write the JSON result with its finalize command.',
|
|
226
|
+
'Recover selected: process only selected candidates, then mark the rest for manual recovery later with:',
|
|
227
|
+
deferUnselectedCommand,
|
|
228
|
+
'Recover none now but keep manual recovery available:',
|
|
229
|
+
deferAllCommand,
|
|
230
|
+
'Decline all recovery candidates:',
|
|
231
|
+
declineAllCommand,
|
|
232
|
+
'Manual later: rerun preview or prompt with --include-deferred.',
|
|
233
|
+
'',
|
|
234
|
+
...candidateLines,
|
|
235
|
+
].join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function selectCandidate(candidates = [], flags = {}) {
|
|
239
|
+
const wanted = flags['session-id'] ? String(flags['session-id']) : '';
|
|
240
|
+
if (!wanted) return candidates[0] || null;
|
|
241
|
+
return candidates.find((candidate) => {
|
|
242
|
+
return candidate.sessionId === wanted
|
|
243
|
+
|| candidate.fileSessionId === wanted
|
|
244
|
+
|| candidate.transcriptHash === wanted;
|
|
245
|
+
}) || null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function compactCandidate(candidate = {}) {
|
|
249
|
+
return {
|
|
250
|
+
sessionId: candidate.sessionId || null,
|
|
251
|
+
fileSessionId: candidate.fileSessionId || null,
|
|
252
|
+
origin: candidate.origin || null,
|
|
253
|
+
source: candidate.source || null,
|
|
254
|
+
agentId: candidate.agentId || null,
|
|
255
|
+
sessionKey: candidate.sessionKey || null,
|
|
256
|
+
userCount: candidate.userCount || null,
|
|
257
|
+
messageCount: candidate.messageCount || null,
|
|
258
|
+
safeMessageCount: candidate.safeMessageCount || null,
|
|
259
|
+
charCount: candidate.charCount || null,
|
|
260
|
+
approxPromptTokens: candidate.approxPromptTokens || null,
|
|
261
|
+
transcriptHash: candidate.transcriptHash || null,
|
|
262
|
+
eligibilityStatus: candidate.eligibilityStatus || null,
|
|
263
|
+
finalizationStatus: candidate.finalizationStatus || null,
|
|
264
|
+
recoveryDecisionStatus: candidate.recoveryDecisionStatus || null,
|
|
265
|
+
updatedAt: candidate.updatedAt || null,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseIdList(value) {
|
|
270
|
+
if (!value || value === true) return new Set();
|
|
271
|
+
return new Set(String(value).split(',').map(part => part.trim()).filter(Boolean));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readSummaryJson(flags = {}) {
|
|
275
|
+
if (flags['summary-stdin']) {
|
|
276
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
277
|
+
if (!raw) throw new Error('summary JSON stdin is empty');
|
|
278
|
+
return JSON.parse(raw);
|
|
279
|
+
}
|
|
280
|
+
if (flags['summary-json']) {
|
|
281
|
+
const raw = fs.readFileSync(flags['summary-json'], 'utf8');
|
|
282
|
+
return JSON.parse(raw);
|
|
283
|
+
}
|
|
284
|
+
const summaryText = oneLine(flags['summary-text']);
|
|
285
|
+
const structuredRaw = flags['structured-summary-json'];
|
|
286
|
+
const structuredSummary = structuredRaw ? JSON.parse(structuredRaw) : {};
|
|
287
|
+
if (!summaryText && Object.keys(structuredSummary).length === 0) {
|
|
288
|
+
throw new Error('finalize requires --summary-stdin, --summary-json, or --summary-text');
|
|
289
|
+
}
|
|
290
|
+
return { summaryText, structuredSummary };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function finalizationReviewText(result = {}) {
|
|
294
|
+
return result.humanReviewText
|
|
295
|
+
|| result.human_review_text
|
|
296
|
+
|| result.finalization?.humanReviewText
|
|
297
|
+
|| result.finalization?.human_review_text
|
|
298
|
+
|| result.finalization?.finalization?.humanReviewText
|
|
299
|
+
|| result.finalization?.finalization?.human_review_text
|
|
300
|
+
|| '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function withAquifer(fn) {
|
|
304
|
+
let aquifer;
|
|
305
|
+
try {
|
|
306
|
+
loadCodexEnv(process.env, { overrideDb: true });
|
|
307
|
+
aquifer = createAquiferFromConfig({});
|
|
308
|
+
return await fn(aquifer);
|
|
309
|
+
} finally {
|
|
310
|
+
if (aquifer && typeof aquifer.close === 'function') {
|
|
311
|
+
await aquifer.close().catch(() => {});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function listCandidates(aquifer, opts) {
|
|
317
|
+
return codex.findRecoveryCandidates(aquifer, opts);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function listDbEligibleCandidates(aquifer, opts) {
|
|
321
|
+
return codex.findDbEligibleRecoveryCandidates(aquifer, opts);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function listOperationalCandidates(aquifer, opts) {
|
|
325
|
+
if (opts && opts.includeJsonlPreviews) {
|
|
326
|
+
return listDbEligibleCandidates(aquifer, opts);
|
|
327
|
+
}
|
|
328
|
+
return listCandidates(aquifer, opts);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function cmdPreview(aquifer, flags, opts) {
|
|
332
|
+
const candidates = await listCandidates(aquifer, opts);
|
|
333
|
+
if (flags.json) {
|
|
334
|
+
console.log(JSON.stringify({ status: candidates.length ? 'needs_consent' : 'none', candidates: candidates.map(compactCandidate) }, null, 2));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (candidates.length === 0) {
|
|
338
|
+
console.log('No Codex recovery candidates.');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
for (const candidate of candidates) console.log(renderCandidate(candidate));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function cmdHookContext(aquifer, flags, opts) {
|
|
345
|
+
const maxRecoveryCandidates = Number.isFinite(opts.maxRecoveryCandidates)
|
|
346
|
+
? Math.max(1, opts.maxRecoveryCandidates)
|
|
347
|
+
: 1;
|
|
348
|
+
const candidates = await listDbEligibleCandidates(aquifer, {
|
|
349
|
+
...opts,
|
|
350
|
+
idleMs: opts.idleMs ?? 0,
|
|
351
|
+
maxRecoveryCandidates,
|
|
352
|
+
includeJsonlPreviews: true,
|
|
353
|
+
});
|
|
354
|
+
const context = renderHookContext(candidates, opts);
|
|
355
|
+
if (flags.json) {
|
|
356
|
+
console.log(JSON.stringify({ status: context ? 'needs_consent' : 'none', context, candidates: candidates.map(compactCandidate) }, null, 2));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (context) console.log(context);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function cmdPrompt(aquifer, flags, opts) {
|
|
363
|
+
const candidates = await listOperationalCandidates(aquifer, opts);
|
|
364
|
+
const candidate = selectCandidate(candidates, flags);
|
|
365
|
+
if (!candidate) throw new Error(`No matching Codex recovery candidate: ${flags['session-id'] || '(first)'}`);
|
|
366
|
+
const prepared = await codex.prepareSessionStartRecovery(aquifer, {
|
|
367
|
+
...opts,
|
|
368
|
+
consent: true,
|
|
369
|
+
candidate,
|
|
370
|
+
});
|
|
371
|
+
if (flags.json) {
|
|
372
|
+
console.log(JSON.stringify({
|
|
373
|
+
status: prepared.status,
|
|
374
|
+
candidate: compactCandidate(candidate),
|
|
375
|
+
view: prepared.view ? {
|
|
376
|
+
status: prepared.view.status,
|
|
377
|
+
sessionId: prepared.view.sessionId,
|
|
378
|
+
transcriptHash: prepared.view.transcriptHash,
|
|
379
|
+
charCount: prepared.view.charCount,
|
|
380
|
+
approxPromptTokens: prepared.view.approxPromptTokens,
|
|
381
|
+
counts: prepared.view.counts,
|
|
382
|
+
} : null,
|
|
383
|
+
prompt: prepared.prompt || null,
|
|
384
|
+
}, null, 2));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (prepared.status !== 'needs_agent_summary') {
|
|
388
|
+
console.log(`Recovery prompt unavailable: ${prepared.status}`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
console.log([
|
|
392
|
+
prepared.prompt,
|
|
393
|
+
'',
|
|
394
|
+
'[AQUIFER FINALIZE]',
|
|
395
|
+
'After returning the JSON summary, pipe it into:',
|
|
396
|
+
renderFinalizeCommand(candidate, opts),
|
|
397
|
+
].join('\n'));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function cmdFinalize(aquifer, flags, opts) {
|
|
401
|
+
const candidates = await listOperationalCandidates(aquifer, opts);
|
|
402
|
+
const candidate = selectCandidate(candidates, flags);
|
|
403
|
+
if (!candidate && !flags['file-path']) {
|
|
404
|
+
throw new Error(`No matching Codex recovery candidate: ${flags['session-id'] || '(first)'}`);
|
|
405
|
+
}
|
|
406
|
+
const summary = readSummaryJson(flags);
|
|
407
|
+
const result = await codex.finalizeCodexSession(aquifer, {
|
|
408
|
+
candidate: candidate || null,
|
|
409
|
+
filePath: flags['file-path'] || undefined,
|
|
410
|
+
sessionId: flags['session-id'] || candidate?.sessionId || undefined,
|
|
411
|
+
summary,
|
|
412
|
+
mode: flags.mode || 'handoff',
|
|
413
|
+
agentId: opts.agentId,
|
|
414
|
+
source: opts.source,
|
|
415
|
+
sessionKey: opts.sessionKey,
|
|
416
|
+
finalizerModel: flags['finalizer-model'] || undefined,
|
|
417
|
+
scopeKind: flags['scope-kind'] || undefined,
|
|
418
|
+
scopeKey: flags['scope-key'] || undefined,
|
|
419
|
+
}, opts);
|
|
420
|
+
if (flags.json) {
|
|
421
|
+
console.log(JSON.stringify(result, null, 2));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
console.log(`Finalization ${result.status}: ${result.sessionId || flags['session-id'] || '(unknown)'}`);
|
|
425
|
+
const review = finalizationReviewText(result);
|
|
426
|
+
if (review) console.log(review);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function cmdDecision(aquifer, flags, opts) {
|
|
430
|
+
const verdict = flags.verdict;
|
|
431
|
+
if (!['declined', 'deferred'].includes(verdict)) {
|
|
432
|
+
throw new Error('decision requires --verdict declined|deferred');
|
|
433
|
+
}
|
|
434
|
+
const candidates = await listOperationalCandidates(aquifer, opts);
|
|
435
|
+
if (flags.all === true) {
|
|
436
|
+
const exceptIds = parseIdList(flags['except-session-id']);
|
|
437
|
+
const selected = candidates.filter(candidate => {
|
|
438
|
+
const ids = [candidate.sessionId, candidate.fileSessionId, candidate.transcriptHash].filter(Boolean);
|
|
439
|
+
return !ids.some(id => exceptIds.has(id));
|
|
440
|
+
});
|
|
441
|
+
const results = [];
|
|
442
|
+
for (const candidate of selected) {
|
|
443
|
+
results.push(await codex.recordRecoveryDecision(aquifer, candidate, verdict, {
|
|
444
|
+
...opts,
|
|
445
|
+
reason: flags.reason || null,
|
|
446
|
+
mode: 'session_start_recovery',
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
if (flags.json) {
|
|
450
|
+
console.log(JSON.stringify({ status: verdict, count: results.length, results }, null, 2));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log(`Recovery ${verdict}: ${results.length} candidate(s)`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const candidate = selectCandidate(candidates, flags);
|
|
457
|
+
if (!candidate) throw new Error(`No matching Codex recovery candidate: ${flags['session-id'] || '(first)'}`);
|
|
458
|
+
const result = await codex.recordRecoveryDecision(aquifer, candidate, verdict, {
|
|
459
|
+
...opts,
|
|
460
|
+
reason: flags.reason || null,
|
|
461
|
+
mode: 'session_start_recovery',
|
|
462
|
+
});
|
|
463
|
+
if (flags.json) {
|
|
464
|
+
console.log(JSON.stringify(result, null, 2));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
console.log(`Recovery ${verdict}: ${candidate.sessionId}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function main(argv = process.argv.slice(2)) {
|
|
471
|
+
const args = parseArgs(argv);
|
|
472
|
+
const command = args._[0] || 'help';
|
|
473
|
+
if (args.flags.config) process.env.AQUIFER_CONFIG = args.flags.config;
|
|
474
|
+
const opts = buildRecoveryOptions(args.flags);
|
|
475
|
+
|
|
476
|
+
if (command === 'help' || args.flags.help || args.flags.h) {
|
|
477
|
+
console.log(`Usage:
|
|
478
|
+
node scripts/codex-recovery.js hook-context [options]
|
|
479
|
+
node scripts/codex-recovery.js preview [options]
|
|
480
|
+
node scripts/codex-recovery.js prompt --session-id ID [options]
|
|
481
|
+
node scripts/codex-recovery.js finalize --session-id ID --summary-stdin [options]
|
|
482
|
+
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]`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await withAquifer(async (aquifer) => {
|
|
488
|
+
switch (command) {
|
|
489
|
+
case 'preview':
|
|
490
|
+
await cmdPreview(aquifer, args.flags, opts);
|
|
491
|
+
break;
|
|
492
|
+
case 'hook-context':
|
|
493
|
+
await cmdHookContext(aquifer, args.flags, opts);
|
|
494
|
+
break;
|
|
495
|
+
case 'prompt':
|
|
496
|
+
await cmdPrompt(aquifer, args.flags, opts);
|
|
497
|
+
break;
|
|
498
|
+
case 'finalize':
|
|
499
|
+
await cmdFinalize(aquifer, args.flags, opts);
|
|
500
|
+
break;
|
|
501
|
+
case 'decision':
|
|
502
|
+
await cmdDecision(aquifer, args.flags, opts);
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
throw new Error(`Unknown command: ${command}`);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = {
|
|
511
|
+
buildRecoveryOptions,
|
|
512
|
+
cmdDecision,
|
|
513
|
+
cmdFinalize,
|
|
514
|
+
cmdHookContext,
|
|
515
|
+
cmdPrompt,
|
|
516
|
+
loadCodexEnv,
|
|
517
|
+
parseArgs,
|
|
518
|
+
renderFinalizeCommand,
|
|
519
|
+
renderHookContext,
|
|
520
|
+
selectCandidate,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
if (require.main === module) {
|
|
524
|
+
main().catch((err) => {
|
|
525
|
+
if (process.argv[2] !== 'hook-context') {
|
|
526
|
+
console.error(`codex-recovery: ${err.message}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
process.exit(0);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { getDailyEntries } = require('./daily-entries');
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// buildSessionContext — pure function, testable.
|
|
7
|
-
// Emits the Miranda persona briefing that gets prepended to the system
|
|
8
|
-
// prompt. Style: 散文段落, 結論收尾, 不給 bullet/table/headers.
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
const TODO_CAP = 5;
|
|
12
|
-
|
|
13
|
-
function buildSessionContext({ today, agentId, focusText, todoItems, moodLine, handoffText, cliEntries }) {
|
|
14
|
-
const parts = [];
|
|
15
|
-
parts.push('你是 Miranda。以下是你已經知道的現況,直接用來回應,不需要讀檔或搜尋。像做 briefing——帶現況也帶判斷和建議。用散文段落,最後一句必須是結論或建議,不能是問句。若草稿有 bullet、標題、表格或問句收尾,改寫再送出。');
|
|
16
|
-
parts.push('回答任何關於過去做過什麼、討論過什麼、決策過什麼的問題時,第一步用 session_recall MCP tool 查,不要用 grep、讀 log、翻檔案。工具在手上就用。');
|
|
17
|
-
|
|
18
|
-
if (focusText) parts.push(`現在的焦點是${focusText}。`);
|
|
19
|
-
if (handoffText) parts.push(`上一段的交接:${handoffText}`);
|
|
20
|
-
|
|
21
|
-
const items = (todoItems || []).slice(0, TODO_CAP);
|
|
22
|
-
if (items.length > 0) parts.push(`手上還有${items.join('、')}。`);
|
|
23
|
-
|
|
24
|
-
if (moodLine) parts.push(`整體狀態${moodLine}。`);
|
|
25
|
-
|
|
26
|
-
const cli = (cliEntries || []).slice(-15);
|
|
27
|
-
if (cli.length > 0) parts.push(`今天已經做過的事(不要重複):${cli.join(';')}`);
|
|
28
|
-
|
|
29
|
-
if (parts.length <= 2) return '';
|
|
30
|
-
return `<session-context date="${today}" agent="${agentId}">\n${parts.join('\n')}\n</session-context>`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// extractFocusTodoMood — pull state rows (focus/todo/mood/handoff) + cli log
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
function extractFocusTodoMood(todayEntries, yesterdayEntries) {
|
|
38
|
-
const allEntries = [...(todayEntries || []), ...(yesterdayEntries || [])]
|
|
39
|
-
.sort((a, b) => new Date(b.event_at) - new Date(a.event_at));
|
|
40
|
-
|
|
41
|
-
const preferCli = (tag) => {
|
|
42
|
-
const cli = allEntries.find(e => e.tag === tag && e.source === 'cli');
|
|
43
|
-
return cli || allEntries.find(e => e.tag === tag);
|
|
44
|
-
};
|
|
45
|
-
const focusEntry = preferCli('[FOCUS]');
|
|
46
|
-
const todoEntry = preferCli('[TODO]');
|
|
47
|
-
const moodEntry = allEntries.find(e => e.tag === '[MOOD]');
|
|
48
|
-
const handoffEntry = preferCli('[HANDOFF]');
|
|
49
|
-
|
|
50
|
-
const focusText = focusEntry
|
|
51
|
-
? focusEntry.text.split('\n').map(l => l.trim().replace(/^-\s*/, '')).filter(Boolean).join(', ')
|
|
52
|
-
: '';
|
|
53
|
-
|
|
54
|
-
const todoItems = todoEntry
|
|
55
|
-
? todoEntry.text.split('\n').map(l => l.trim().replace(/^-\s*/, '')).filter(Boolean)
|
|
56
|
-
: [];
|
|
57
|
-
|
|
58
|
-
const moodLine = moodEntry ? moodEntry.text.trim() : '';
|
|
59
|
-
|
|
60
|
-
let handoffText = '';
|
|
61
|
-
if (handoffEntry) {
|
|
62
|
-
const meta = handoffEntry.metadata || {};
|
|
63
|
-
const isTrivial = (meta.status === 'completed' && meta.next === '無')
|
|
64
|
-
|| handoffEntry.text.trim().startsWith('上一段已完成 簡短交談');
|
|
65
|
-
if (!isTrivial) {
|
|
66
|
-
handoffText = handoffEntry.text.trim();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const stateTags = new Set(['[FOCUS]', '[TODO]', '[MOOD]', '[HANDOFF]', '[HIGHLIGHT]', '[NARRATIVE]']);
|
|
71
|
-
const logEntries = (todayEntries || [])
|
|
72
|
-
.filter(e => !stateTags.has(e.tag))
|
|
73
|
-
.sort((a, b) => new Date(a.event_at) - new Date(b.event_at))
|
|
74
|
-
.map(e => e.text.trim())
|
|
75
|
-
.filter(Boolean);
|
|
76
|
-
|
|
77
|
-
return { focusText, todoItems, moodLine, handoffText, cliEntries: logEntries };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// computeInjection — gateway/CC shared: queries daily_entries + aquifer.bootstrap
|
|
82
|
-
// and returns a ready-to-prepend context string. No host-specific wiring.
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
function dateTaipei(d) {
|
|
86
|
-
return new Intl.DateTimeFormat('sv-SE', {
|
|
87
|
-
timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
88
|
-
}).format(d);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function computeInjection({ aquifer, pool, agentId, now, includeBootstrap = true }) {
|
|
92
|
-
if (!now) now = new Date();
|
|
93
|
-
const today = dateTaipei(now);
|
|
94
|
-
const yesterday = dateTaipei(new Date(now.getTime() - 86400000));
|
|
95
|
-
|
|
96
|
-
const [todayEntries, yesterdayEntries] = await Promise.all([
|
|
97
|
-
getDailyEntries(pool, today, agentId),
|
|
98
|
-
getDailyEntries(pool, yesterday, agentId),
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
const state = extractFocusTodoMood(todayEntries, yesterdayEntries);
|
|
102
|
-
const context = buildSessionContext({ today, agentId, ...state });
|
|
103
|
-
|
|
104
|
-
let bootstrapText = '';
|
|
105
|
-
if (includeBootstrap && aquifer) {
|
|
106
|
-
try {
|
|
107
|
-
const bs = await aquifer.bootstrap({ agentId, limit: 5, maxChars: 2000, format: 'text' });
|
|
108
|
-
if (bs.text && bs.sessions && bs.sessions.length > 0) bootstrapText = '\n' + bs.text;
|
|
109
|
-
} catch { /* best-effort */ }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return context + bootstrapText;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
module.exports = {
|
|
116
|
-
buildSessionContext,
|
|
117
|
-
extractFocusTodoMood,
|
|
118
|
-
computeInjection,
|
|
119
|
-
};
|