@shadowforge0/aquifer-memory 1.7.0 → 1.8.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.
Files changed (39) hide show
  1. package/.env.example +8 -0
  2. package/README.md +66 -0
  3. package/aquifer.config.example.json +19 -0
  4. package/consumers/cli.js +217 -14
  5. package/consumers/codex-active-checkpoint.js +186 -0
  6. package/consumers/codex-current-memory.js +106 -0
  7. package/consumers/codex-handoff.js +442 -3
  8. package/consumers/codex.js +164 -107
  9. package/consumers/mcp.js +144 -6
  10. package/consumers/shared/config.js +60 -1
  11. package/consumers/shared/factory.js +10 -3
  12. package/core/aquifer.js +351 -840
  13. package/core/backends/capabilities.js +89 -0
  14. package/core/backends/local.js +430 -0
  15. package/core/legacy-bootstrap.js +140 -0
  16. package/core/mcp-manifest.js +66 -2
  17. package/core/memory-promotion.js +157 -26
  18. package/core/memory-recall.js +341 -22
  19. package/core/memory-records.js +128 -8
  20. package/core/memory-serving.js +132 -0
  21. package/core/postgres-migrations.js +533 -0
  22. package/core/public-session-filter.js +40 -0
  23. package/core/recall-runtime.js +115 -0
  24. package/core/scope-attribution.js +279 -0
  25. package/core/session-checkpoint-producer.js +412 -0
  26. package/core/session-checkpoints.js +432 -0
  27. package/core/session-finalization.js +82 -1
  28. package/core/storage-checkpoints.js +546 -0
  29. package/core/storage.js +121 -8
  30. package/docs/setup.md +22 -0
  31. package/package.json +8 -4
  32. package/schema/014-v1-checkpoint-runs.sql +349 -0
  33. package/schema/015-v1-evidence-items.sql +92 -0
  34. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  35. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  36. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  37. package/scripts/codex-checkpoint-commands.js +464 -0
  38. package/scripts/codex-checkpoint-runtime.js +520 -0
  39. package/scripts/codex-recovery.js +105 -0
@@ -0,0 +1,520 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const { loadConfig } = require('../consumers/shared/config');
8
+ const codex = require('../consumers/codex');
9
+
10
+ function parseIntFlag(value, fallback) {
11
+ if (value === undefined || value === null || value === true || value === '') return fallback;
12
+ const parsed = parseInt(value, 10);
13
+ return Number.isFinite(parsed) ? parsed : fallback;
14
+ }
15
+
16
+ function checkpointMarkerDir(flags = {}, opts = {}) {
17
+ if (flags['checkpoint-marker-dir']) return flags['checkpoint-marker-dir'];
18
+ const paths = codex.defaultPaths(opts);
19
+ return path.join(path.dirname(paths.importedDir), 'codex-active-checkpoints');
20
+ }
21
+
22
+ function checkpointSchedulerDir(flags = {}, opts = {}) {
23
+ if (flags['checkpoint-scheduler-dir']) return flags['checkpoint-scheduler-dir'];
24
+ const paths = codex.defaultPaths(opts);
25
+ return path.join(path.dirname(paths.importedDir), 'codex-active-checkpoint-scheduler');
26
+ }
27
+
28
+ function checkpointClaimDir(flags = {}, opts = {}) {
29
+ if (flags['checkpoint-claim-dir']) return flags['checkpoint-claim-dir'];
30
+ const paths = codex.defaultPaths(opts);
31
+ return path.join(path.dirname(paths.importedDir), 'codex-active-checkpoint-claims');
32
+ }
33
+
34
+ function checkpointSpoolDir(flags = {}, opts = {}) {
35
+ if (flags['checkpoint-spool-dir']) return flags['checkpoint-spool-dir'];
36
+ const paths = codex.defaultPaths(opts);
37
+ return path.join(path.dirname(paths.importedDir), 'codex-active-checkpoint-spool');
38
+ }
39
+
40
+ function viewMessageCount(view = {}) {
41
+ return Number.isFinite(Number(view.counts?.safeMessageCount))
42
+ ? Number(view.counts.safeMessageCount)
43
+ : (Array.isArray(view.messages) ? view.messages.length : 0);
44
+ }
45
+
46
+ function viewUserCount(view = {}) {
47
+ return Number.isFinite(Number(view.counts?.userCount))
48
+ ? Number(view.counts.userCount)
49
+ : (Array.isArray(view.messages) ? view.messages.filter(message => message.role === 'user').length : 0);
50
+ }
51
+
52
+ function positiveThreshold(value, fallback) {
53
+ const parsed = parseIntFlag(value, fallback);
54
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
55
+ }
56
+
57
+ function loadRuntimeConfig(flags = {}, opts = {}) {
58
+ return loadConfig({
59
+ env: opts.env || process.env,
60
+ configPath: flags.config || (opts.env || process.env).AQUIFER_CONFIG || null,
61
+ cwd: opts.cwd || process.cwd(),
62
+ });
63
+ }
64
+
65
+ function checkpointPolicy(config = {}) {
66
+ return config.codex?.checkpoint || {};
67
+ }
68
+
69
+ function configuredNumber(value, fallback) {
70
+ return Number.isFinite(Number(value)) ? Number(value) : fallback;
71
+ }
72
+
73
+ function checkpointCheckIntervalMs(flags = {}, config = {}) {
74
+ const policy = checkpointPolicy(config);
75
+ if (flags['checkpoint-check-interval-ms'] !== undefined) {
76
+ return Math.max(0, parseIntFlag(flags['checkpoint-check-interval-ms'], 600000));
77
+ }
78
+ if (flags['checkpoint-check-interval-minutes'] !== undefined) {
79
+ return Math.max(0, parseIntFlag(flags['checkpoint-check-interval-minutes'], 10) * 60 * 1000);
80
+ }
81
+ if (policy.checkIntervalMs !== undefined && policy.checkIntervalMs !== null) {
82
+ return Math.max(0, configuredNumber(policy.checkIntervalMs, 600000));
83
+ }
84
+ return Math.max(0, configuredNumber(policy.checkIntervalMinutes, 10) * 60 * 1000);
85
+ }
86
+
87
+ function checkpointQuietMs(flags = {}, config = {}) {
88
+ return Math.max(0, parseIntFlag(
89
+ flags['checkpoint-quiet-ms'],
90
+ configuredNumber(checkpointPolicy(config).quietMs, 3000),
91
+ ));
92
+ }
93
+
94
+ function checkpointClaimTtlMs(flags = {}, config = {}) {
95
+ return Math.max(1000, parseIntFlag(
96
+ flags['checkpoint-claim-ttl-ms'],
97
+ configuredNumber(checkpointPolicy(config).claimTtlMs, 60000),
98
+ ));
99
+ }
100
+
101
+ function checkpointEveryMessages(flags = {}, config = {}) {
102
+ return positiveThreshold(
103
+ flags['checkpoint-every-messages'],
104
+ configuredNumber(checkpointPolicy(config).everyMessages, 20),
105
+ );
106
+ }
107
+
108
+ function checkpointEveryUserMessages(flags = {}, config = {}) {
109
+ if (flags['checkpoint-every-user-messages']) {
110
+ return positiveThreshold(flags['checkpoint-every-user-messages'], 10);
111
+ }
112
+ const configured = checkpointPolicy(config).everyUserMessages;
113
+ return configured === undefined || configured === null
114
+ ? null
115
+ : positiveThreshold(configured, 10);
116
+ }
117
+
118
+ function checkpointProposalWindow(marker = null, intervalMs = 0, nowMs = Date.now()) {
119
+ const lastProposalMs = marker?.lastProposalAt ? Date.parse(marker.lastProposalAt) : NaN;
120
+ if (!Number.isFinite(lastProposalMs)) return { due: true, lastProposalAt: null, nextProposalAt: null };
121
+ const nextMs = lastProposalMs + Math.max(0, intervalMs);
122
+ return {
123
+ due: nowMs >= nextMs,
124
+ lastProposalAt: marker.lastProposalAt,
125
+ nextProposalAt: isoAt(nextMs),
126
+ };
127
+ }
128
+
129
+ function isoAt(ms) {
130
+ return new Date(ms).toISOString();
131
+ }
132
+
133
+ function readJsonFile(filePath) {
134
+ try {
135
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
136
+ return parsed && typeof parsed === 'object' ? parsed : null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function readSchedulerMarker(dir, sessionId) {
143
+ if (!dir || !sessionId) return null;
144
+ const filePath = codex.markerPath(dir, sessionId);
145
+ const parsed = readJsonFile(filePath);
146
+ return parsed ? { ...parsed, markerPath: filePath } : null;
147
+ }
148
+
149
+ function writeSchedulerMarker(dir, sessionId, patch = {}) {
150
+ if (!dir || !sessionId) return null;
151
+ fs.mkdirSync(dir, { recursive: true });
152
+ const filePath = codex.markerPath(dir, sessionId);
153
+ const existing = readSchedulerMarker(dir, sessionId) || {};
154
+ const marker = {
155
+ kind: 'codex_active_checkpoint_scheduler_v1',
156
+ ...existing,
157
+ ...patch,
158
+ sessionId,
159
+ updatedAt: new Date().toISOString(),
160
+ };
161
+ delete marker.markerPath;
162
+ fs.writeFileSync(filePath, `${JSON.stringify(marker)}\n`, 'utf8');
163
+ return { ...marker, markerPath: filePath };
164
+ }
165
+
166
+ function readCheckpointMarker(dir, sessionId) {
167
+ if (!dir || !sessionId) return null;
168
+ const filePath = codex.markerPath(dir, sessionId);
169
+ const parsed = readJsonFile(filePath);
170
+ return parsed ? { ...parsed, markerPath: filePath } : null;
171
+ }
172
+
173
+ function writeCheckpointMarker(dir, prepared = {}) {
174
+ const sessionId = prepared.view?.sessionId || prepared.checkpointInput?.transcript?.sessionId;
175
+ if (!dir || !sessionId) return null;
176
+ fs.mkdirSync(dir, { recursive: true });
177
+ const filePath = codex.markerPath(dir, sessionId);
178
+ const marker = {
179
+ kind: 'codex_active_checkpoint_marker_v1',
180
+ sessionId,
181
+ filePath: prepared.view?.filePath || null,
182
+ writtenAt: new Date().toISOString(),
183
+ transcriptHash: prepared.view?.transcriptHash || null,
184
+ inputHash: prepared.checkpointInput?.inputHash || null,
185
+ messageCount: viewMessageCount(prepared.view),
186
+ userCount: viewUserCount(prepared.view),
187
+ coverage: prepared.checkpointInput?.coverage || null,
188
+ };
189
+ fs.writeFileSync(filePath, `${JSON.stringify(marker)}\n`, 'utf8');
190
+ return { ...marker, markerPath: filePath };
191
+ }
192
+
193
+ function checkpointDueFromMarker(view = {}, marker = null, flags = {}, config = {}) {
194
+ const everyMessages = checkpointEveryMessages(flags, config);
195
+ const everyUserMessages = checkpointEveryUserMessages(flags, config);
196
+ const messageCount = viewMessageCount(view);
197
+ const userCount = viewUserCount(view);
198
+ const markerMessageCount = Number(marker?.messageCount || 0);
199
+ const markerUserCount = Number(marker?.userCount || 0);
200
+ const deltaMessages = Math.max(0, messageCount - markerMessageCount);
201
+ const deltaUserMessages = Math.max(0, userCount - markerUserCount);
202
+ const due = Boolean(flags.force === true
203
+ || (!marker && (messageCount >= everyMessages || (everyUserMessages !== null && userCount >= everyUserMessages)))
204
+ || (marker && (deltaMessages >= everyMessages || (everyUserMessages !== null && deltaUserMessages >= everyUserMessages))));
205
+ return {
206
+ due,
207
+ everyMessages,
208
+ everyUserMessages,
209
+ messageCount,
210
+ userCount,
211
+ markerMessageCount,
212
+ markerUserCount,
213
+ deltaMessages,
214
+ deltaUserMessages,
215
+ };
216
+ }
217
+
218
+ function findNewestJsonlFile(dir) {
219
+ if (!dir) return null;
220
+ const files = [];
221
+ const stack = [dir];
222
+ while (stack.length) {
223
+ const current = stack.pop();
224
+ let entries = [];
225
+ try {
226
+ entries = fs.readdirSync(current, { withFileTypes: true });
227
+ } catch {
228
+ continue;
229
+ }
230
+ for (const entry of entries) {
231
+ const filePath = path.join(current, entry.name);
232
+ if (entry.isDirectory()) {
233
+ stack.push(filePath);
234
+ continue;
235
+ }
236
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
237
+ try {
238
+ files.push({ filePath, mtimeMs: fs.statSync(filePath).mtimeMs });
239
+ } catch {}
240
+ }
241
+ }
242
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
243
+ return files[0]?.filePath || null;
244
+ }
245
+
246
+ function isPathInside(childPath, parentPath) {
247
+ const relative = path.relative(parentPath, childPath);
248
+ return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
249
+ }
250
+
251
+ function validateCheckpointTranscriptPath(filePath, opts = {}) {
252
+ if (!filePath) return { ok: false, status: 'missing_file_path' };
253
+ if (path.extname(filePath) !== '.jsonl') {
254
+ return { ok: false, status: 'invalid_transcript_path', reason: 'not_jsonl', filePath };
255
+ }
256
+ const sessionsDir = opts.sessionsDir || codex.defaultPaths(opts).sessionsDir;
257
+ let realFile;
258
+ let realSessionsDir;
259
+ let stat;
260
+ try {
261
+ realFile = fs.realpathSync(filePath);
262
+ realSessionsDir = fs.realpathSync(sessionsDir);
263
+ stat = fs.statSync(realFile);
264
+ } catch {
265
+ return { ok: false, status: 'not_found', filePath, sessionsDir };
266
+ }
267
+ if (!isPathInside(realFile, realSessionsDir)) {
268
+ return {
269
+ ok: false,
270
+ status: 'invalid_transcript_path',
271
+ reason: 'outside_sessions_dir',
272
+ filePath: realFile,
273
+ sessionsDir: realSessionsDir,
274
+ };
275
+ }
276
+ if (!stat.isFile()) {
277
+ return { ok: false, status: 'invalid_transcript_path', reason: 'not_regular_file', filePath: realFile };
278
+ }
279
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
280
+ return { ok: false, status: 'invalid_transcript_path', reason: 'owner_mismatch', filePath: realFile };
281
+ }
282
+ return { ok: true, filePath: realFile, sessionsDir: realSessionsDir, stat };
283
+ }
284
+
285
+ function checkpointHeartbeatInput(flags = {}, hookInput = {}) {
286
+ return {
287
+ sessionId: flags['session-id'] || hookInput.session_id || undefined,
288
+ filePath: flags['file-path'] || hookInput.transcript_path || undefined,
289
+ hookEventName: hookInput.hook_event_name || flags['hook-event-name'] || undefined,
290
+ };
291
+ }
292
+
293
+ function claimPayload(nowMs, ttlMs) {
294
+ return {
295
+ pid: process.pid,
296
+ createdAt: isoAt(nowMs),
297
+ expiresAt: isoAt(nowMs + ttlMs),
298
+ };
299
+ }
300
+
301
+ function acquireHeartbeatClaim(dir, sessionId, nowMs = Date.now(), ttlMs = 60000) {
302
+ fs.mkdirSync(dir, { recursive: true });
303
+ const filePath = codex.markerPath(dir, sessionId);
304
+ const payload = claimPayload(nowMs, ttlMs);
305
+ const content = `${JSON.stringify(payload)}\n`;
306
+ try {
307
+ const fd = fs.openSync(filePath, 'wx');
308
+ try {
309
+ fs.writeFileSync(fd, content, 'utf8');
310
+ } finally {
311
+ fs.closeSync(fd);
312
+ }
313
+ return { acquired: true, filePath, payload };
314
+ } catch (err) {
315
+ if (err && err.code !== 'EEXIST') {
316
+ return { acquired: false, filePath, reason: err.message || 'claim_failed' };
317
+ }
318
+ }
319
+
320
+ const existing = readJsonFile(filePath);
321
+ const expiresMs = existing?.expiresAt ? Date.parse(existing.expiresAt) : NaN;
322
+ if (Number.isFinite(expiresMs) && expiresMs > nowMs) {
323
+ return { acquired: false, filePath, reason: 'claim_active', existing };
324
+ }
325
+
326
+ try {
327
+ fs.unlinkSync(filePath);
328
+ } catch {}
329
+ try {
330
+ const fd = fs.openSync(filePath, 'wx');
331
+ try {
332
+ fs.writeFileSync(fd, content, 'utf8');
333
+ } finally {
334
+ fs.closeSync(fd);
335
+ }
336
+ return { acquired: true, filePath, payload, staleReplaced: true };
337
+ } catch (err) {
338
+ return { acquired: false, filePath, reason: err?.message || 'claim_race' };
339
+ }
340
+ }
341
+
342
+ function releaseHeartbeatClaim(claim) {
343
+ if (!claim?.acquired || !claim.filePath) return;
344
+ const existing = readJsonFile(claim.filePath);
345
+ if (existing?.pid !== claim.payload?.pid || existing?.createdAt !== claim.payload?.createdAt) return;
346
+ try {
347
+ fs.unlinkSync(claim.filePath);
348
+ } catch {}
349
+ }
350
+
351
+ function spoolCheckpointProposal(dir, prepared = {}, meta = {}) {
352
+ const sessionId = prepared.view?.sessionId || meta.sessionId;
353
+ if (!dir || !sessionId || !prepared.prompt) return null;
354
+ fs.mkdirSync(dir, { recursive: true });
355
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
356
+ const filePath = path.join(dir, `${codex.safeMarkerKey(sessionId)}-${stamp}-${process.pid}.json`);
357
+ const payload = {
358
+ kind: 'codex_active_checkpoint_pending_v1',
359
+ createdAt: new Date().toISOString(),
360
+ sessionId,
361
+ source: meta.source || 'codex-heartbeat',
362
+ hookEventName: meta.hookEventName || null,
363
+ triggerKind: meta.triggerKind || 'time_window_message_delta',
364
+ guards: {
365
+ checkpointIsProcessMaterial: true,
366
+ stdoutPromptExcluded: true,
367
+ additionalContextExcluded: true,
368
+ dbWriteExcluded: true,
369
+ activeMemoryCommitExcluded: true,
370
+ rawHookPromptExcluded: true,
371
+ },
372
+ threshold: prepared.checkpointInput?.threshold || null,
373
+ coverage: prepared.checkpointInput?.coverage || null,
374
+ prompt: prepared.prompt,
375
+ };
376
+ fs.writeFileSync(filePath, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', flag: 'wx' });
377
+ return { filePath, createdAt: payload.createdAt };
378
+ }
379
+
380
+ function shellQuote(value) {
381
+ return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
382
+ }
383
+
384
+ function defaultHooksPath(opts = {}) {
385
+ const codexHome = opts.codexHome || path.join(os.homedir(), '.codex');
386
+ return path.join(codexHome, 'hooks.json');
387
+ }
388
+
389
+ function checkpointHeartbeatCommand(flags = {}, opts = {}) {
390
+ const parts = [
391
+ process.execPath,
392
+ opts.scriptPath || path.resolve(__dirname, 'codex-recovery.js'),
393
+ 'checkpoint-heartbeat',
394
+ '--hook-stdin',
395
+ ];
396
+ const pushValue = (flag, value) => {
397
+ if (value !== undefined && value !== null && value !== '' && value !== true) {
398
+ parts.push(flag, String(value));
399
+ }
400
+ };
401
+ pushValue('--scope-key', flags['scope-key'] || flags['active-scope-key']);
402
+ pushValue('--active-scope-key', flags['active-scope-key']);
403
+ pushValue('--active-scope-path', flags['active-scope-path']);
404
+ pushValue('--config', flags.config);
405
+ pushValue('--checkpoint-check-interval-ms', flags['checkpoint-check-interval-ms']);
406
+ pushValue('--checkpoint-check-interval-minutes', flags['checkpoint-check-interval-minutes']);
407
+ pushValue('--checkpoint-every-messages', flags['checkpoint-every-messages']);
408
+ pushValue('--checkpoint-every-user-messages', flags['checkpoint-every-user-messages']);
409
+ pushValue('--checkpoint-quiet-ms', flags['checkpoint-quiet-ms']);
410
+ pushValue('--checkpoint-scheduler-dir', flags['checkpoint-scheduler-dir']);
411
+ pushValue('--checkpoint-claim-dir', flags['checkpoint-claim-dir']);
412
+ pushValue('--checkpoint-spool-dir', flags['checkpoint-spool-dir']);
413
+ pushValue('--checkpoint-claim-ttl-ms', flags['checkpoint-claim-ttl-ms']);
414
+ pushValue('--agent-id', flags['agent-id']);
415
+ pushValue('--source', flags.source);
416
+ pushValue('--session-key', flags['session-key']);
417
+ pushValue('--workspace', flags.workspace || flags['workspace-path']);
418
+ pushValue('--project', flags.project || flags['project-key']);
419
+ pushValue('--repo-path', flags['repo-path']);
420
+ pushValue('--codex-home', flags['codex-home']);
421
+ return parts.map(shellQuote).join(' ');
422
+ }
423
+
424
+ function readHooksConfig(filePath) {
425
+ try {
426
+ const raw = fs.readFileSync(filePath, 'utf8');
427
+ if (!raw.trim()) return { hooks: {} };
428
+ const parsed = JSON.parse(raw);
429
+ if (!parsed.hooks || typeof parsed.hooks !== 'object') parsed.hooks = {};
430
+ return parsed;
431
+ } catch (err) {
432
+ if (err && err.code === 'ENOENT') return { hooks: {} };
433
+ throw err;
434
+ }
435
+ }
436
+
437
+ function mergeCheckpointHeartbeatHook(existing = { hooks: {} }, flags = {}, opts = {}) {
438
+ const out = JSON.parse(JSON.stringify(existing || { hooks: {} }));
439
+ if (!out.hooks || typeof out.hooks !== 'object') out.hooks = {};
440
+ const event = 'UserPromptSubmit';
441
+ if (!Array.isArray(out.hooks[event])) out.hooks[event] = [];
442
+ const group = {
443
+ hooks: [{
444
+ type: 'command',
445
+ command: checkpointHeartbeatCommand(flags, opts),
446
+ }],
447
+ };
448
+ const existingGroup = out.hooks[event].find((candidate) => {
449
+ return Array.isArray(candidate?.hooks)
450
+ && candidate.hooks.some((hook) => String(hook?.command || '').includes('checkpoint-heartbeat'));
451
+ });
452
+ if (existingGroup) {
453
+ delete existingGroup.matcher;
454
+ existingGroup.hooks = group.hooks;
455
+ } else {
456
+ out.hooks[event].push(group);
457
+ }
458
+ return out;
459
+ }
460
+
461
+ function inspectCheckpointHeartbeatHook(opts = {}) {
462
+ const hooksPath = opts.hooksPath || defaultHooksPath(opts);
463
+ let parsed;
464
+ try {
465
+ parsed = readHooksConfig(hooksPath);
466
+ } catch (err) {
467
+ return {
468
+ status: 'fail',
469
+ hooksPath,
470
+ installed: false,
471
+ detail: err && err.message ? err.message : String(err),
472
+ };
473
+ }
474
+ const groups = Array.isArray(parsed.hooks?.UserPromptSubmit) ? parsed.hooks.UserPromptSubmit : [];
475
+ const installed = groups.some((group) => {
476
+ return Array.isArray(group?.hooks)
477
+ && group.hooks.some((hook) => String(hook?.command || '').includes('checkpoint-heartbeat'));
478
+ });
479
+ return {
480
+ status: installed ? 'ok' : 'warn',
481
+ hooksPath,
482
+ installed,
483
+ detail: installed
484
+ ? 'UserPromptSubmit checkpoint heartbeat hook is installed.'
485
+ : 'UserPromptSubmit checkpoint heartbeat hook is not installed.',
486
+ };
487
+ }
488
+
489
+ module.exports = {
490
+ acquireHeartbeatClaim,
491
+ checkpointCheckIntervalMs,
492
+ checkpointClaimDir,
493
+ checkpointClaimTtlMs,
494
+ checkpointDueFromMarker,
495
+ checkpointEveryMessages,
496
+ checkpointEveryUserMessages,
497
+ checkpointHeartbeatCommand,
498
+ checkpointHeartbeatInput,
499
+ checkpointMarkerDir,
500
+ checkpointProposalWindow,
501
+ checkpointQuietMs,
502
+ checkpointSchedulerDir,
503
+ checkpointSpoolDir,
504
+ defaultHooksPath,
505
+ findNewestJsonlFile,
506
+ inspectCheckpointHeartbeatHook,
507
+ isoAt,
508
+ loadRuntimeConfig,
509
+ mergeCheckpointHeartbeatHook,
510
+ readCheckpointMarker,
511
+ readHooksConfig,
512
+ readSchedulerMarker,
513
+ releaseHeartbeatClaim,
514
+ spoolCheckpointProposal,
515
+ validateCheckpointTranscriptPath,
516
+ viewMessageCount,
517
+ viewUserCount,
518
+ writeCheckpointMarker,
519
+ writeSchedulerMarker,
520
+ };
@@ -7,6 +7,36 @@ const path = require('path');
7
7
 
8
8
  const { createAquiferFromConfig } = require('../consumers/shared/factory');
9
9
  const codex = require('../consumers/codex');
10
+ const {
11
+ cmdCheckpointHeartbeat,
12
+ cmdCheckpointHeartbeatHook,
13
+ cmdCheckpointPrompt,
14
+ cmdCheckpointTick,
15
+ } = require('./codex-checkpoint-commands');
16
+ const {
17
+ acquireHeartbeatClaim,
18
+ checkpointCheckIntervalMs,
19
+ checkpointClaimDir,
20
+ checkpointClaimTtlMs,
21
+ checkpointDueFromMarker,
22
+ checkpointEveryMessages,
23
+ checkpointEveryUserMessages,
24
+ checkpointHeartbeatCommand,
25
+ checkpointMarkerDir,
26
+ checkpointQuietMs,
27
+ checkpointSchedulerDir,
28
+ checkpointSpoolDir,
29
+ defaultHooksPath,
30
+ findNewestJsonlFile,
31
+ inspectCheckpointHeartbeatHook,
32
+ loadRuntimeConfig,
33
+ mergeCheckpointHeartbeatHook,
34
+ readCheckpointMarker,
35
+ readSchedulerMarker,
36
+ releaseHeartbeatClaim,
37
+ writeCheckpointMarker,
38
+ writeSchedulerMarker,
39
+ } = require('./codex-checkpoint-runtime');
10
40
  const DB_ENV_KEYS = new Set(['DATABASE_URL', 'AQUIFER_DB_URL', 'AQUIFER_SCHEMA', 'AQUIFER_TENANT_ID']);
11
41
 
12
42
  const VALUE_FLAGS = new Set([
@@ -16,7 +46,23 @@ const VALUE_FLAGS = new Set([
16
46
  'except-session-id',
17
47
  'file-path',
18
48
  'finalizer-model',
49
+ 'checkpoint-every-messages',
50
+ 'checkpoint-every-user-messages',
51
+ 'checkpoint-check-interval-ms',
52
+ 'checkpoint-check-interval-minutes',
53
+ 'checkpoint-claim-ttl-ms',
54
+ 'checkpoint-claim-dir',
55
+ 'checkpoint-marker-dir',
56
+ 'checkpoint-scheduler-dir',
57
+ 'checkpoint-spool-dir',
58
+ 'checkpoint-quiet-ms',
59
+ 'hook-event-name',
60
+ 'hooks-path',
19
61
  'idle-ms',
62
+ 'max-checkpoint-bytes',
63
+ 'max-checkpoint-chars',
64
+ 'max-checkpoint-messages',
65
+ 'max-checkpoint-prompt-tokens',
20
66
  'max-candidates',
21
67
  'max-recovery-bytes',
22
68
  'max-recovery-chars',
@@ -27,6 +73,8 @@ const VALUE_FLAGS = new Set([
27
73
  'reason',
28
74
  'scope-kind',
29
75
  'scope-key',
76
+ 'active-scope-key',
77
+ 'active-scope-path',
30
78
  'session-id',
31
79
  'session-key',
32
80
  'sessions-dir',
@@ -118,6 +166,7 @@ function buildRecoveryOptions(flags = {}, env = process.env) {
118
166
  project: flags.project || flags['project-key'] || envDefault(env, 'CODEX_AQUIFER_PROJECT', 'CODEX_PROJECT') || undefined,
119
167
  repoPath: flags['repo-path'] || envDefault(env, 'CODEX_AQUIFER_REPO_PATH', 'CODEX_REPO_PATH') || undefined,
120
168
  codexHome: flags['codex-home'] || envDefault(env, 'CODEX_HOME') || undefined,
169
+ hooksPath: flags['hooks-path'] || undefined,
121
170
  stateDir: flags['state-dir'] || undefined,
122
171
  sessionsDir: flags['sessions-dir'] || undefined,
123
172
  maxRecoveryCandidates: parseIntFlag(flags['max-candidates'], 1),
@@ -288,6 +337,7 @@ function compactDoctorOptions(opts = {}) {
288
337
  project: opts.project || null,
289
338
  repoPath: opts.repoPath || null,
290
339
  codexHome: opts.codexHome || null,
340
+ hooksPath: opts.hooksPath || null,
291
341
  sessionsDir: opts.sessionsDir || null,
292
342
  stateDir: opts.stateDir || null,
293
343
  excludeNewest: opts.excludeNewest !== false,
@@ -319,6 +369,15 @@ async function buildDoctorReport(aquifer, opts = {}, env = process.env) {
319
369
  addDoctorCheck(checks, 'current_transcript_guard', 'ok', 'Newest transcript exclusion is enabled.');
320
370
  }
321
371
 
372
+ const heartbeatHook = inspectCheckpointHeartbeatHook(opts);
373
+ addDoctorCheck(
374
+ checks,
375
+ 'checkpoint_heartbeat_hook',
376
+ heartbeatHook.status,
377
+ heartbeatHook.detail,
378
+ { hooksPath: heartbeatHook.hooksPath, installed: heartbeatHook.installed },
379
+ );
380
+
322
381
  let candidates = [];
323
382
  try {
324
383
  candidates = await listDbEligibleCandidates(aquifer, {
@@ -602,6 +661,10 @@ async function main(argv = process.argv.slice(2)) {
602
661
  node scripts/codex-recovery.js hook-context [options]
603
662
  node scripts/codex-recovery.js preview [options]
604
663
  node scripts/codex-recovery.js prompt --session-id ID [options]
664
+ node scripts/codex-recovery.js checkpoint-prompt --file-path FILE --scope-key KEY [options]
665
+ node scripts/codex-recovery.js checkpoint-tick --scope-key KEY [--file-path FILE|--sessions-dir DIR] [options]
666
+ node scripts/codex-recovery.js checkpoint-heartbeat --hook-stdin --scope-key KEY [options]
667
+ node scripts/codex-recovery.js checkpoint-heartbeat-hook --scope-key KEY [--hooks-path FILE] [--apply]
605
668
  node scripts/codex-recovery.js finalize --session-id ID --summary-stdin [options]
606
669
  node scripts/codex-recovery.js decision --session-id ID --verdict declined|deferred [options]
607
670
  node scripts/codex-recovery.js decision --all --verdict declined|deferred [options]
@@ -620,6 +683,16 @@ async function main(argv = process.argv.slice(2)) {
620
683
  return;
621
684
  }
622
685
 
686
+ if (command === 'checkpoint-heartbeat') {
687
+ await cmdCheckpointHeartbeat(null, args.flags, opts);
688
+ return;
689
+ }
690
+
691
+ if (command === 'checkpoint-heartbeat-hook') {
692
+ await cmdCheckpointHeartbeatHook(args.flags, opts);
693
+ return;
694
+ }
695
+
623
696
  await withAquifer(async (aquifer) => {
624
697
  switch (command) {
625
698
  case 'preview':
@@ -631,6 +704,12 @@ async function main(argv = process.argv.slice(2)) {
631
704
  case 'prompt':
632
705
  await cmdPrompt(aquifer, args.flags, opts);
633
706
  break;
707
+ case 'checkpoint-prompt':
708
+ await cmdCheckpointPrompt(aquifer, args.flags, opts);
709
+ break;
710
+ case 'checkpoint-tick':
711
+ await cmdCheckpointTick(aquifer, args.flags, opts);
712
+ break;
634
713
  case 'finalize':
635
714
  await cmdFinalize(aquifer, args.flags, opts);
636
715
  break;
@@ -651,13 +730,39 @@ module.exports = {
651
730
  cmdDoctorInitFailure,
652
731
  cmdFinalize,
653
732
  cmdHookContext,
733
+ cmdCheckpointHeartbeat,
734
+ cmdCheckpointHeartbeatHook,
735
+ cmdCheckpointPrompt,
736
+ cmdCheckpointTick,
654
737
  cmdPrompt,
738
+ acquireHeartbeatClaim,
739
+ checkpointDueFromMarker,
740
+ checkpointHeartbeatCommand,
741
+ checkpointCheckIntervalMs,
742
+ checkpointEveryMessages,
743
+ checkpointEveryUserMessages,
744
+ checkpointQuietMs,
745
+ checkpointClaimDir,
746
+ checkpointClaimTtlMs,
747
+ checkpointMarkerDir,
748
+ checkpointSchedulerDir,
749
+ checkpointSpoolDir,
750
+ defaultHooksPath,
751
+ findNewestJsonlFile,
752
+ inspectCheckpointHeartbeatHook,
753
+ loadRuntimeConfig,
655
754
  loadCodexEnv,
656
755
  main,
756
+ mergeCheckpointHeartbeatHook,
657
757
  parseArgs,
758
+ readCheckpointMarker,
759
+ readSchedulerMarker,
760
+ releaseHeartbeatClaim,
658
761
  renderFinalizeCommand,
659
762
  renderHookContext,
660
763
  selectCandidate,
764
+ writeCheckpointMarker,
765
+ writeSchedulerMarker,
661
766
  };
662
767
 
663
768
  if (require.main === module) {