@shadowforge0/aquifer-memory 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +72 -0
- package/README_CN.md +17 -0
- package/README_TW.md +4 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +259 -12
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +551 -6
- package/consumers/codex.js +209 -25
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +357 -838
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-bootstrap.js +20 -8
- package/core/memory-consolidation.js +365 -11
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +347 -11
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +98 -2
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/getting-started.md +6 -0
- package/docs/setup.md +66 -3
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- package/scripts/codex-recovery.js +246 -1
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const codex = require('../consumers/codex');
|
|
7
|
+
const {
|
|
8
|
+
acquireHeartbeatClaim,
|
|
9
|
+
checkpointCheckIntervalMs,
|
|
10
|
+
checkpointClaimDir,
|
|
11
|
+
checkpointClaimTtlMs,
|
|
12
|
+
checkpointDueFromMarker,
|
|
13
|
+
checkpointEveryMessages,
|
|
14
|
+
checkpointEveryUserMessages,
|
|
15
|
+
checkpointHeartbeatCommand,
|
|
16
|
+
checkpointHeartbeatInput,
|
|
17
|
+
checkpointMarkerDir,
|
|
18
|
+
checkpointProposalWindow,
|
|
19
|
+
checkpointSchedulerDir,
|
|
20
|
+
checkpointSpoolDir,
|
|
21
|
+
defaultHooksPath,
|
|
22
|
+
findNewestJsonlFile,
|
|
23
|
+
isoAt,
|
|
24
|
+
loadRuntimeConfig,
|
|
25
|
+
mergeCheckpointHeartbeatHook,
|
|
26
|
+
readCheckpointMarker,
|
|
27
|
+
readHooksConfig,
|
|
28
|
+
readSchedulerMarker,
|
|
29
|
+
releaseHeartbeatClaim,
|
|
30
|
+
spoolCheckpointProposal,
|
|
31
|
+
validateCheckpointTranscriptPath,
|
|
32
|
+
writeCheckpointMarker,
|
|
33
|
+
writeSchedulerMarker,
|
|
34
|
+
} = require('./codex-checkpoint-runtime');
|
|
35
|
+
|
|
36
|
+
function parseIntFlag(value, fallback) {
|
|
37
|
+
if (value === undefined || value === null || value === true || value === '') return fallback;
|
|
38
|
+
const parsed = parseInt(value, 10);
|
|
39
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseScopePath(value) {
|
|
43
|
+
if (!value || value === true) return undefined;
|
|
44
|
+
const parts = String(value).split(',').map(part => part.trim()).filter(Boolean);
|
|
45
|
+
return parts.length ? parts : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readHookInputFromStdin() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
51
|
+
if (!raw.trim()) return {};
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function cmdCheckpointPrompt(aquifer, flags, opts) {
|
|
60
|
+
const filePath = flags['file-path'];
|
|
61
|
+
if (!filePath) throw new Error('checkpoint-prompt requires --file-path');
|
|
62
|
+
const prepared = await codex.prepareActiveSessionCheckpoint(aquifer, {
|
|
63
|
+
...opts,
|
|
64
|
+
filePath,
|
|
65
|
+
sessionId: flags['session-id'] || undefined,
|
|
66
|
+
scopeKind: flags['scope-kind'] || undefined,
|
|
67
|
+
scopeKey: flags['scope-key'] || flags['active-scope-key'] || undefined,
|
|
68
|
+
activeScopeKey: flags['active-scope-key'] || flags['scope-key'] || undefined,
|
|
69
|
+
activeScopePath: parseScopePath(flags['active-scope-path']),
|
|
70
|
+
checkpointEveryMessages: parseIntFlag(flags['checkpoint-every-messages'], undefined),
|
|
71
|
+
checkpointEveryUserMessages: parseIntFlag(flags['checkpoint-every-user-messages'], undefined),
|
|
72
|
+
maxCheckpointBytes: parseIntFlag(flags['max-checkpoint-bytes'], undefined),
|
|
73
|
+
maxCheckpointMessages: parseIntFlag(flags['max-checkpoint-messages'], undefined),
|
|
74
|
+
maxCheckpointChars: parseIntFlag(flags['max-checkpoint-chars'], undefined),
|
|
75
|
+
maxCheckpointPromptTokens: parseIntFlag(flags['max-checkpoint-prompt-tokens'], undefined),
|
|
76
|
+
force: flags.force === true,
|
|
77
|
+
});
|
|
78
|
+
if (flags.json) {
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
status: prepared.status,
|
|
81
|
+
due: prepared.due === true,
|
|
82
|
+
threshold: prepared.checkpointInput?.threshold || null,
|
|
83
|
+
coverage: prepared.checkpointInput?.coverage || null,
|
|
84
|
+
prompt: prepared.prompt || null,
|
|
85
|
+
}, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (prepared.status !== 'needs_agent_checkpoint') {
|
|
89
|
+
const threshold = prepared.checkpointInput?.threshold;
|
|
90
|
+
if (threshold) {
|
|
91
|
+
console.log(`Checkpoint prompt unavailable: ${prepared.status} (${threshold.messageCount}/${threshold.everyMessages} messages)`);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`Checkpoint prompt unavailable: ${prepared.status}`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log([
|
|
98
|
+
prepared.prompt,
|
|
99
|
+
'',
|
|
100
|
+
'[AQUIFER CHECKPOINT]',
|
|
101
|
+
'Use the returned JSON as checkpoint process material for a later handoff or operator-reviewed checkpoint write.',
|
|
102
|
+
].join('\n'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cmdCheckpointTick(aquifer, flags, opts) {
|
|
106
|
+
const paths = codex.defaultPaths(opts);
|
|
107
|
+
const filePath = flags['file-path'] || findNewestJsonlFile(opts.sessionsDir || paths.sessionsDir);
|
|
108
|
+
if (!filePath) throw new Error('checkpoint-tick requires --file-path or a readable --sessions-dir');
|
|
109
|
+
const view = codex.materializeRecoveryTranscriptView({
|
|
110
|
+
filePath,
|
|
111
|
+
sessionId: flags['session-id'] || undefined,
|
|
112
|
+
}, {
|
|
113
|
+
...opts,
|
|
114
|
+
tailOnMaxBudget: true,
|
|
115
|
+
maxRecoveryBytes: parseIntFlag(flags['max-checkpoint-bytes'], opts.maxRecoveryBytes),
|
|
116
|
+
maxRecoveryMessages: parseIntFlag(flags['max-checkpoint-messages'], opts.maxRecoveryMessages),
|
|
117
|
+
maxRecoveryChars: parseIntFlag(flags['max-checkpoint-chars'], opts.maxRecoveryChars),
|
|
118
|
+
maxRecoveryPromptTokens: parseIntFlag(flags['max-checkpoint-prompt-tokens'], opts.maxRecoveryPromptTokens),
|
|
119
|
+
});
|
|
120
|
+
if (!view || view.status !== 'ok') {
|
|
121
|
+
const result = {
|
|
122
|
+
status: view?.status || 'missing_view',
|
|
123
|
+
due: false,
|
|
124
|
+
filePath,
|
|
125
|
+
reason: view?.reason || null,
|
|
126
|
+
view,
|
|
127
|
+
};
|
|
128
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
129
|
+
else console.log(`Checkpoint tick unavailable: ${result.status}${result.reason ? ` (${result.reason})` : ''}`);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const markerDir = checkpointMarkerDir(flags, opts);
|
|
134
|
+
const marker = readCheckpointMarker(markerDir, view.sessionId);
|
|
135
|
+
const threshold = checkpointDueFromMarker(view, marker, flags);
|
|
136
|
+
if (!threshold.due) {
|
|
137
|
+
const result = {
|
|
138
|
+
status: 'not_ready',
|
|
139
|
+
due: false,
|
|
140
|
+
filePath,
|
|
141
|
+
sessionId: view.sessionId,
|
|
142
|
+
marker: marker ? {
|
|
143
|
+
markerPath: marker.markerPath,
|
|
144
|
+
messageCount: marker.messageCount || 0,
|
|
145
|
+
userCount: marker.userCount || 0,
|
|
146
|
+
writtenAt: marker.writtenAt || null,
|
|
147
|
+
} : null,
|
|
148
|
+
threshold,
|
|
149
|
+
};
|
|
150
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
151
|
+
else {
|
|
152
|
+
console.log(`Checkpoint tick not ready: ${threshold.deltaMessages}/${threshold.everyMessages} new messages`);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const prepared = await codex.prepareActiveSessionCheckpoint(aquifer, {
|
|
158
|
+
...opts,
|
|
159
|
+
filePath,
|
|
160
|
+
view,
|
|
161
|
+
sessionId: flags['session-id'] || undefined,
|
|
162
|
+
scopeKind: flags['scope-kind'] || undefined,
|
|
163
|
+
scopeKey: flags['scope-key'] || flags['active-scope-key'] || undefined,
|
|
164
|
+
activeScopeKey: flags['active-scope-key'] || flags['scope-key'] || undefined,
|
|
165
|
+
activeScopePath: parseScopePath(flags['active-scope-path']),
|
|
166
|
+
checkpointEveryMessages: parseIntFlag(flags['checkpoint-every-messages'], undefined),
|
|
167
|
+
checkpointEveryUserMessages: parseIntFlag(flags['checkpoint-every-user-messages'], undefined),
|
|
168
|
+
maxCheckpointBytes: parseIntFlag(flags['max-checkpoint-bytes'], undefined),
|
|
169
|
+
maxCheckpointMessages: parseIntFlag(flags['max-checkpoint-messages'], undefined),
|
|
170
|
+
maxCheckpointChars: parseIntFlag(flags['max-checkpoint-chars'], undefined),
|
|
171
|
+
maxCheckpointPromptTokens: parseIntFlag(flags['max-checkpoint-prompt-tokens'], undefined),
|
|
172
|
+
force: true,
|
|
173
|
+
triggerKind: marker ? 'message_count_delta' : 'message_count',
|
|
174
|
+
});
|
|
175
|
+
const writtenMarker = prepared.status === 'needs_agent_checkpoint' && flags['dry-run'] !== true
|
|
176
|
+
? writeCheckpointMarker(markerDir, prepared)
|
|
177
|
+
: null;
|
|
178
|
+
const result = {
|
|
179
|
+
status: prepared.status,
|
|
180
|
+
due: prepared.due === true,
|
|
181
|
+
filePath,
|
|
182
|
+
sessionId: view.sessionId,
|
|
183
|
+
marker: writtenMarker,
|
|
184
|
+
previousMarker: marker ? {
|
|
185
|
+
markerPath: marker.markerPath,
|
|
186
|
+
messageCount: marker.messageCount || 0,
|
|
187
|
+
userCount: marker.userCount || 0,
|
|
188
|
+
writtenAt: marker.writtenAt || null,
|
|
189
|
+
} : null,
|
|
190
|
+
threshold,
|
|
191
|
+
coverage: prepared.checkpointInput?.coverage || null,
|
|
192
|
+
prompt: prepared.prompt || null,
|
|
193
|
+
};
|
|
194
|
+
if (flags.json) {
|
|
195
|
+
console.log(JSON.stringify(result, null, 2));
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
if (prepared.status !== 'needs_agent_checkpoint') {
|
|
199
|
+
console.log(`Checkpoint tick unavailable: ${prepared.status}`);
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
console.log([
|
|
203
|
+
prepared.prompt,
|
|
204
|
+
'',
|
|
205
|
+
'[AQUIFER CHECKPOINT TICK]',
|
|
206
|
+
writtenMarker
|
|
207
|
+
? `Marker written: ${writtenMarker.markerPath}`
|
|
208
|
+
: 'Dry run: marker not written.',
|
|
209
|
+
'Use the returned JSON as checkpoint process material for a later handoff or operator-reviewed checkpoint write.',
|
|
210
|
+
].join('\n'));
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function emitCheckpointHeartbeatResult(result, flags = {}) {
|
|
215
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function cmdCheckpointHeartbeat(aquifer, flags, opts, hookInputArg) {
|
|
219
|
+
const hookInput = hookInputArg || (flags['hook-stdin'] === true ? readHookInputFromStdin() : {});
|
|
220
|
+
const input = checkpointHeartbeatInput(flags, hookInput);
|
|
221
|
+
const config = loadRuntimeConfig(flags, opts);
|
|
222
|
+
const nowMs = Date.now();
|
|
223
|
+
const intervalMs = checkpointCheckIntervalMs(flags, config);
|
|
224
|
+
const schedulerDir = checkpointSchedulerDir(flags, opts);
|
|
225
|
+
|
|
226
|
+
let safeSessionId;
|
|
227
|
+
try {
|
|
228
|
+
safeSessionId = codex.assertSafeSessionId(input.sessionId, 'sessionId');
|
|
229
|
+
} catch {
|
|
230
|
+
const result = { status: 'missing_or_invalid_session_id', due: false };
|
|
231
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const marker = readSchedulerMarker(schedulerDir, safeSessionId);
|
|
236
|
+
const proposalWindow = checkpointProposalWindow(marker, intervalMs, nowMs);
|
|
237
|
+
const nextCheckAt = proposalWindow.nextProposalAt || null;
|
|
238
|
+
if (flags.force !== true && !proposalWindow.due) {
|
|
239
|
+
const result = {
|
|
240
|
+
status: 'not_due_time',
|
|
241
|
+
due: false,
|
|
242
|
+
sessionId: safeSessionId,
|
|
243
|
+
lastProposalAt: proposalWindow.lastProposalAt,
|
|
244
|
+
nextCheckAt: proposalWindow.nextProposalAt,
|
|
245
|
+
markerPath: marker.markerPath,
|
|
246
|
+
};
|
|
247
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const claim = acquireHeartbeatClaim(
|
|
252
|
+
checkpointClaimDir(flags, opts),
|
|
253
|
+
safeSessionId,
|
|
254
|
+
nowMs,
|
|
255
|
+
checkpointClaimTtlMs(flags, config),
|
|
256
|
+
);
|
|
257
|
+
if (!claim.acquired) {
|
|
258
|
+
const result = {
|
|
259
|
+
status: 'checkpoint_heartbeat_claimed',
|
|
260
|
+
due: false,
|
|
261
|
+
sessionId: safeSessionId,
|
|
262
|
+
reason: claim.reason || 'claim_active',
|
|
263
|
+
};
|
|
264
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const pathCheck = validateCheckpointTranscriptPath(input.filePath, opts);
|
|
270
|
+
if (!pathCheck.ok) {
|
|
271
|
+
const written = writeSchedulerMarker(schedulerDir, safeSessionId, {
|
|
272
|
+
lastCheckAt: isoAt(nowMs),
|
|
273
|
+
nextCheckAt,
|
|
274
|
+
lastStatus: pathCheck.status,
|
|
275
|
+
lastReason: pathCheck.reason || null,
|
|
276
|
+
hookEventName: input.hookEventName || null,
|
|
277
|
+
});
|
|
278
|
+
const result = {
|
|
279
|
+
status: pathCheck.status,
|
|
280
|
+
due: false,
|
|
281
|
+
sessionId: safeSessionId,
|
|
282
|
+
reason: pathCheck.reason || null,
|
|
283
|
+
nextCheckAt,
|
|
284
|
+
markerPath: written?.markerPath || null,
|
|
285
|
+
};
|
|
286
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const view = codex.materializeRecoveryTranscriptView({
|
|
291
|
+
filePath: pathCheck.filePath,
|
|
292
|
+
sessionId: safeSessionId,
|
|
293
|
+
}, {
|
|
294
|
+
...opts,
|
|
295
|
+
tailOnMaxBudget: true,
|
|
296
|
+
maxRecoveryBytes: parseIntFlag(flags['max-checkpoint-bytes'], opts.maxRecoveryBytes),
|
|
297
|
+
maxRecoveryMessages: parseIntFlag(flags['max-checkpoint-messages'], opts.maxRecoveryMessages),
|
|
298
|
+
maxRecoveryChars: parseIntFlag(flags['max-checkpoint-chars'], opts.maxRecoveryChars),
|
|
299
|
+
maxRecoveryPromptTokens: parseIntFlag(flags['max-checkpoint-prompt-tokens'], opts.maxRecoveryPromptTokens),
|
|
300
|
+
});
|
|
301
|
+
if (!view || view.status !== 'ok') {
|
|
302
|
+
const written = writeSchedulerMarker(schedulerDir, safeSessionId, {
|
|
303
|
+
lastCheckAt: isoAt(nowMs),
|
|
304
|
+
nextCheckAt,
|
|
305
|
+
lastStatus: view?.status || 'missing_view',
|
|
306
|
+
lastReason: view?.reason || null,
|
|
307
|
+
hookEventName: input.hookEventName || null,
|
|
308
|
+
});
|
|
309
|
+
const result = {
|
|
310
|
+
status: view?.status || 'missing_view',
|
|
311
|
+
due: false,
|
|
312
|
+
sessionId: safeSessionId,
|
|
313
|
+
reason: view?.reason || null,
|
|
314
|
+
nextCheckAt,
|
|
315
|
+
markerPath: written?.markerPath || null,
|
|
316
|
+
};
|
|
317
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const coveredMessages = Number(marker?.lastCoveredMessageCount || 0);
|
|
322
|
+
const coveredUsers = Number(marker?.lastCoveredUserCount || 0);
|
|
323
|
+
const threshold = checkpointDueFromMarker(view, coveredMessages > 0 || coveredUsers > 0
|
|
324
|
+
? { messageCount: coveredMessages, userCount: coveredUsers }
|
|
325
|
+
: null, flags, config);
|
|
326
|
+
if (!threshold.due) {
|
|
327
|
+
const written = writeSchedulerMarker(schedulerDir, safeSessionId, {
|
|
328
|
+
lastCheckAt: isoAt(nowMs),
|
|
329
|
+
nextCheckAt,
|
|
330
|
+
lastStatus: 'not_enough_messages',
|
|
331
|
+
lastReason: null,
|
|
332
|
+
hookEventName: input.hookEventName || null,
|
|
333
|
+
lastObservedMessageCount: threshold.messageCount,
|
|
334
|
+
lastObservedUserCount: threshold.userCount,
|
|
335
|
+
});
|
|
336
|
+
const result = {
|
|
337
|
+
status: 'not_enough_messages',
|
|
338
|
+
due: false,
|
|
339
|
+
sessionId: safeSessionId,
|
|
340
|
+
nextCheckAt,
|
|
341
|
+
markerPath: written?.markerPath || null,
|
|
342
|
+
threshold,
|
|
343
|
+
};
|
|
344
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const prepared = await codex.prepareActiveSessionCheckpoint(aquifer, {
|
|
349
|
+
...opts,
|
|
350
|
+
filePath: pathCheck.filePath,
|
|
351
|
+
view,
|
|
352
|
+
sessionId: safeSessionId,
|
|
353
|
+
scopeKind: flags['scope-kind'] || undefined,
|
|
354
|
+
scopeKey: flags['scope-key'] || flags['active-scope-key'] || undefined,
|
|
355
|
+
activeScopeKey: flags['active-scope-key'] || flags['scope-key'] || undefined,
|
|
356
|
+
activeScopePath: parseScopePath(flags['active-scope-path']),
|
|
357
|
+
checkpointEveryMessages: checkpointEveryMessages(flags, config),
|
|
358
|
+
checkpointEveryUserMessages: checkpointEveryUserMessages(flags, config),
|
|
359
|
+
force: true,
|
|
360
|
+
includeCurrentMemory: false,
|
|
361
|
+
triggerKind: 'heartbeat_time_window',
|
|
362
|
+
});
|
|
363
|
+
if (prepared.status !== 'needs_agent_checkpoint') {
|
|
364
|
+
const written = writeSchedulerMarker(schedulerDir, safeSessionId, {
|
|
365
|
+
lastCheckAt: isoAt(nowMs),
|
|
366
|
+
nextCheckAt,
|
|
367
|
+
lastStatus: prepared.status,
|
|
368
|
+
hookEventName: input.hookEventName || null,
|
|
369
|
+
});
|
|
370
|
+
const result = {
|
|
371
|
+
status: prepared.status,
|
|
372
|
+
due: false,
|
|
373
|
+
sessionId: safeSessionId,
|
|
374
|
+
nextCheckAt,
|
|
375
|
+
markerPath: written?.markerPath || null,
|
|
376
|
+
};
|
|
377
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const spool = flags['dry-run'] === true
|
|
382
|
+
? null
|
|
383
|
+
: spoolCheckpointProposal(checkpointSpoolDir(flags, opts), prepared, {
|
|
384
|
+
sessionId: safeSessionId,
|
|
385
|
+
source: opts.source || 'codex',
|
|
386
|
+
hookEventName: input.hookEventName || null,
|
|
387
|
+
});
|
|
388
|
+
const proposalAt = isoAt(nowMs);
|
|
389
|
+
const nextProposalAt = isoAt(nowMs + intervalMs);
|
|
390
|
+
const markerPatch = {
|
|
391
|
+
lastCheckAt: proposalAt,
|
|
392
|
+
lastProposalAt: flags['dry-run'] === true ? marker?.lastProposalAt || null : proposalAt,
|
|
393
|
+
nextCheckAt: flags['dry-run'] === true ? nextCheckAt : nextProposalAt,
|
|
394
|
+
lastStatus: flags['dry-run'] === true ? 'checkpoint_due_dry_run' : 'checkpoint_spooled',
|
|
395
|
+
lastReason: null,
|
|
396
|
+
hookEventName: input.hookEventName || null,
|
|
397
|
+
lastSpoolPath: spool?.filePath || marker?.lastSpoolPath || null,
|
|
398
|
+
};
|
|
399
|
+
if (flags['dry-run'] !== true) {
|
|
400
|
+
markerPatch.lastCoveredMessageCount = threshold.messageCount;
|
|
401
|
+
markerPatch.lastCoveredUserCount = threshold.userCount;
|
|
402
|
+
}
|
|
403
|
+
const written = writeSchedulerMarker(schedulerDir, safeSessionId, markerPatch);
|
|
404
|
+
const result = {
|
|
405
|
+
status: flags['dry-run'] === true ? 'checkpoint_due_dry_run' : 'checkpoint_spooled',
|
|
406
|
+
due: true,
|
|
407
|
+
sessionId: safeSessionId,
|
|
408
|
+
nextCheckAt: flags['dry-run'] === true ? nextCheckAt : nextProposalAt,
|
|
409
|
+
markerPath: written?.markerPath || null,
|
|
410
|
+
spool,
|
|
411
|
+
threshold,
|
|
412
|
+
coverage: prepared.checkpointInput?.coverage || null,
|
|
413
|
+
};
|
|
414
|
+
emitCheckpointHeartbeatResult(result, flags);
|
|
415
|
+
return result;
|
|
416
|
+
} finally {
|
|
417
|
+
releaseHeartbeatClaim(claim);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function cmdCheckpointHeartbeatHook(flags, opts) {
|
|
422
|
+
if (!flags['scope-key'] && !flags['active-scope-key']) {
|
|
423
|
+
throw new Error('checkpoint-heartbeat-hook requires --scope-key or --active-scope-key');
|
|
424
|
+
}
|
|
425
|
+
const hooksPath = flags['hooks-path'] || defaultHooksPath(opts);
|
|
426
|
+
const before = readHooksConfig(hooksPath);
|
|
427
|
+
const after = mergeCheckpointHeartbeatHook(before, flags, opts);
|
|
428
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
429
|
+
const apply = flags.apply === true;
|
|
430
|
+
if (apply) {
|
|
431
|
+
fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
|
|
432
|
+
fs.writeFileSync(hooksPath, JSON.stringify(after, null, 2) + '\n', 'utf8');
|
|
433
|
+
}
|
|
434
|
+
const result = {
|
|
435
|
+
status: apply ? 'applied' : 'dry_run',
|
|
436
|
+
hooksPath,
|
|
437
|
+
changed,
|
|
438
|
+
event: 'UserPromptSubmit',
|
|
439
|
+
command: checkpointHeartbeatCommand(flags, opts),
|
|
440
|
+
hooks: after,
|
|
441
|
+
};
|
|
442
|
+
if (flags.json) {
|
|
443
|
+
console.log(JSON.stringify(result, null, 2));
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
console.log([
|
|
447
|
+
`Codex heartbeat hook ${apply ? 'applied' : 'dry run'}: ${hooksPath}`,
|
|
448
|
+
`Changed: ${changed ? 'yes' : 'no'}`,
|
|
449
|
+
'Command:',
|
|
450
|
+
result.command,
|
|
451
|
+
apply ? '' : 'Pass --apply to write the merged hooks.json.',
|
|
452
|
+
].filter(Boolean).join('\n'));
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
module.exports = {
|
|
457
|
+
cmdCheckpointHeartbeat,
|
|
458
|
+
cmdCheckpointHeartbeatHook,
|
|
459
|
+
cmdCheckpointPrompt,
|
|
460
|
+
cmdCheckpointTick,
|
|
461
|
+
emitCheckpointHeartbeatResult,
|
|
462
|
+
parseScopePath,
|
|
463
|
+
readHookInputFromStdin,
|
|
464
|
+
};
|