@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.
Files changed (44) hide show
  1. package/.env.example +8 -0
  2. package/README.md +72 -0
  3. package/README_CN.md +17 -0
  4. package/README_TW.md +4 -0
  5. package/aquifer.config.example.json +19 -0
  6. package/consumers/cli.js +259 -12
  7. package/consumers/codex-active-checkpoint.js +186 -0
  8. package/consumers/codex-current-memory.js +106 -0
  9. package/consumers/codex-handoff.js +551 -6
  10. package/consumers/codex.js +209 -25
  11. package/consumers/mcp.js +144 -6
  12. package/consumers/shared/config.js +60 -1
  13. package/consumers/shared/factory.js +10 -3
  14. package/core/aquifer.js +357 -838
  15. package/core/backends/capabilities.js +89 -0
  16. package/core/backends/local.js +430 -0
  17. package/core/legacy-bootstrap.js +140 -0
  18. package/core/mcp-manifest.js +66 -2
  19. package/core/memory-bootstrap.js +20 -8
  20. package/core/memory-consolidation.js +365 -11
  21. package/core/memory-promotion.js +157 -26
  22. package/core/memory-recall.js +341 -22
  23. package/core/memory-records.js +347 -11
  24. package/core/memory-serving.js +132 -0
  25. package/core/postgres-migrations.js +533 -0
  26. package/core/public-session-filter.js +40 -0
  27. package/core/recall-runtime.js +115 -0
  28. package/core/scope-attribution.js +279 -0
  29. package/core/session-checkpoint-producer.js +412 -0
  30. package/core/session-checkpoints.js +432 -0
  31. package/core/session-finalization.js +98 -2
  32. package/core/storage-checkpoints.js +546 -0
  33. package/core/storage.js +121 -8
  34. package/docs/getting-started.md +6 -0
  35. package/docs/setup.md +66 -3
  36. package/package.json +8 -4
  37. package/schema/014-v1-checkpoint-runs.sql +349 -0
  38. package/schema/015-v1-evidence-items.sql +92 -0
  39. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  40. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  41. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  42. package/scripts/codex-checkpoint-commands.js +464 -0
  43. package/scripts/codex-checkpoint-runtime.js +520 -0
  44. package/scripts/codex-recovery.js +246 -1
@@ -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
+ };