sentinelayer-cli 0.17.0 → 0.17.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.
@@ -34,6 +34,7 @@ import {
34
34
  } from "./checkpoints.js";
35
35
  import { resolveSessionPaths } from "./paths.js";
36
36
  import {
37
+ DEFAULT_RECAP_ACTIVITY_THRESHOLD,
37
38
  DEFAULT_RECAP_INACTIVITY_MS,
38
39
  DEFAULT_RECAP_INTERVAL_MS,
39
40
  emitPeriodicRecap,
@@ -58,7 +59,34 @@ const RENEWAL_LEAD_MS = 60 * 60 * 1000;
58
59
  const DEFAULT_STALE_AGENT_SECONDS = 90;
59
60
  const DEFAULT_RECAP_INTERVAL_MS_OVERRIDE = DEFAULT_RECAP_INTERVAL_MS;
60
61
  const DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE = DEFAULT_RECAP_INACTIVITY_MS;
62
+ const DEFAULT_RECAP_ACTIVITY_THRESHOLD_OVERRIDE = DEFAULT_RECAP_ACTIVITY_THRESHOLD;
61
63
  const DEFAULT_CHECKPOINT_INTERVAL_MS = 60_000;
64
+ const DEFAULT_CHECKPOINT_EVENT_THRESHOLD = DEFAULT_CHECKPOINT_MIN_EVENTS;
65
+ const DEFAULT_CHECKPOINT_IDLE_MS = DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE;
66
+ const CHECKPOINT_MEANINGFUL_EVENT_NAMES = new Set([
67
+ "file_lock",
68
+ "file_unlock",
69
+ "finding",
70
+ "help_request",
71
+ "help_response",
72
+ "session_admin_kill",
73
+ "session_message",
74
+ "task_accepted",
75
+ "task_assign",
76
+ "task_blocked",
77
+ "task_completed",
78
+ ]);
79
+ const CHECKPOINT_IGNORED_EVENT_NAMES = new Set([
80
+ "agent_heartbeat",
81
+ "agent_join",
82
+ "agent_status",
83
+ "context_briefing",
84
+ "daemon_alert",
85
+ "session_checkpoint",
86
+ "session_listen_error",
87
+ "session_recap",
88
+ "session_usage",
89
+ ]);
62
90
 
63
91
  const SENTI_MODEL = "gpt-5.4-mini";
64
92
  const SENTI_IDENTITY = Object.freeze({
@@ -176,10 +204,14 @@ function createSentiState({
176
204
  tickIntervalMs,
177
205
  recapIntervalMs,
178
206
  recapInactivityMs,
207
+ recapActivityThreshold,
179
208
  checkpointGenerator,
180
209
  checkpointIntervalMs,
181
210
  checkpointMinEvents,
182
211
  checkpointMaxEvents,
212
+ checkpointEventThreshold,
213
+ checkpointIdleMs,
214
+ checkpointCloseoutOnStop,
183
215
  helpResponder,
184
216
  llmInvoker,
185
217
  telemetrySessionId,
@@ -195,12 +227,19 @@ function createSentiState({
195
227
  tickIntervalMs,
196
228
  recapIntervalMs,
197
229
  recapInactivityMs,
230
+ recapActivityThreshold,
198
231
  checkpointGenerator,
199
232
  checkpointIntervalMs,
200
233
  checkpointMinEvents,
201
234
  checkpointMaxEvents,
235
+ checkpointEventThreshold,
236
+ checkpointIdleMs,
237
+ checkpointCloseoutOnStop,
202
238
  checkpointGenerationInFlight: false,
203
239
  lastCheckpointAttemptAt: null,
240
+ lastCheckpointSourceEventAt: null,
241
+ lastCheckpointSourceSequenceId: null,
242
+ lastCheckpointSourceCursor: null,
204
243
  lastCheckpointResult: null,
205
244
  helpResponder,
206
245
  llmInvoker,
@@ -840,6 +879,169 @@ function parseEpoch(value, fallbackIso = new Date().toISOString()) {
840
879
  return Date.parse(normalizeIsoTimestamp(value, fallbackIso)) || 0;
841
880
  }
842
881
 
882
+ function eventSequenceId(event = {}) {
883
+ const parsed = Number(event.sequenceId ?? event.sequence_id);
884
+ if (!Number.isFinite(parsed) || parsed <= 0) {
885
+ return null;
886
+ }
887
+ return Math.floor(parsed);
888
+ }
889
+
890
+ function eventCursor(event = {}) {
891
+ return normalizeString(event.cursor || event.eventId || event.idempotencyToken || event.ts);
892
+ }
893
+
894
+ function isMeaningfulCheckpointSourceEvent(event = {}) {
895
+ const eventName = normalizeString(event.event).toLowerCase();
896
+ if (!eventName || CHECKPOINT_IGNORED_EVENT_NAMES.has(eventName)) {
897
+ return false;
898
+ }
899
+ if (CHECKPOINT_MEANINGFUL_EVENT_NAMES.has(eventName) || eventName.startsWith("task_")) {
900
+ return true;
901
+ }
902
+ return false;
903
+ }
904
+
905
+ function isEventAfterCheckpointAnchor(event = {}, daemonState = {}) {
906
+ const anchorSequence = Number(daemonState.lastCheckpointSourceSequenceId);
907
+ const sequence = eventSequenceId(event);
908
+ if (Number.isFinite(anchorSequence) && anchorSequence > 0 && sequence !== null) {
909
+ return sequence > anchorSequence;
910
+ }
911
+
912
+ const anchorAt = normalizeString(daemonState.lastCheckpointSourceEventAt);
913
+ if (!anchorAt) {
914
+ return true;
915
+ }
916
+ const anchorEpoch = parseEpoch(anchorAt, anchorAt);
917
+ const eventEpoch = parseEpoch(event.ts, anchorAt);
918
+ if (eventEpoch > anchorEpoch) {
919
+ return true;
920
+ }
921
+ if (eventEpoch < anchorEpoch) {
922
+ return false;
923
+ }
924
+
925
+ const anchorCursor = normalizeString(daemonState.lastCheckpointSourceCursor);
926
+ if (!anchorCursor) {
927
+ return true;
928
+ }
929
+ return eventCursor(event) !== anchorCursor;
930
+ }
931
+
932
+ function rememberCheckpointSource(daemonState, event = {}, nowIso = new Date().toISOString()) {
933
+ daemonState.lastCheckpointSourceEventAt = normalizeIsoTimestamp(event.ts, nowIso);
934
+ daemonState.lastCheckpointSourceSequenceId = eventSequenceId(event);
935
+ daemonState.lastCheckpointSourceCursor = eventCursor(event);
936
+ }
937
+
938
+ async function resolveCheckpointTrigger(
939
+ daemonState,
940
+ nowIso = new Date().toISOString(),
941
+ { force = false, forceReason = "" } = {}
942
+ ) {
943
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
944
+ const nowEpoch = parseEpoch(normalizedNow, normalizedNow);
945
+ const eventThreshold = Math.min(
946
+ 200,
947
+ normalizePositiveInteger(
948
+ daemonState.checkpointEventThreshold,
949
+ DEFAULT_CHECKPOINT_EVENT_THRESHOLD
950
+ )
951
+ );
952
+ const idleMs = normalizePositiveInteger(
953
+ daemonState.checkpointIdleMs,
954
+ DEFAULT_CHECKPOINT_IDLE_MS
955
+ );
956
+ const tail = Math.min(
957
+ 200,
958
+ Math.max(
959
+ eventThreshold,
960
+ normalizePositiveInteger(daemonState.checkpointMaxEvents, DEFAULT_CHECKPOINT_MAX_EVENTS)
961
+ )
962
+ );
963
+ const events = await readStream(daemonState.sessionId, {
964
+ targetPath: daemonState.targetPath,
965
+ tail,
966
+ });
967
+ const sourceEvents = events.filter(isMeaningfulCheckpointSourceEvent);
968
+ const uncheckpointedSourceEvents = sourceEvents.filter((event) =>
969
+ isEventAfterCheckpointAnchor(event, daemonState)
970
+ );
971
+ const latestSourceEvent = sourceEvents.length > 0 ? sourceEvents[sourceEvents.length - 1] : null;
972
+ const latestUncheckpointedSourceEvent =
973
+ uncheckpointedSourceEvents.length > 0
974
+ ? uncheckpointedSourceEvents[uncheckpointedSourceEvents.length - 1]
975
+ : null;
976
+ const latestSourceEventAt = latestSourceEvent
977
+ ? normalizeIsoTimestamp(latestSourceEvent.ts, normalizedNow)
978
+ : null;
979
+ const latestUncheckpointedSourceEventAt = latestUncheckpointedSourceEvent
980
+ ? normalizeIsoTimestamp(latestUncheckpointedSourceEvent.ts, normalizedNow)
981
+ : null;
982
+ const sourceIdleMs = latestUncheckpointedSourceEvent
983
+ ? Math.max(0, nowEpoch - parseEpoch(latestUncheckpointedSourceEvent.ts, normalizedNow))
984
+ : null;
985
+ const policy = {
986
+ eventThreshold,
987
+ idleMs,
988
+ sourceEventCount: uncheckpointedSourceEvents.length,
989
+ totalSourceEventCount: sourceEvents.length,
990
+ latestSourceEventAt,
991
+ latestUncheckpointedSourceEventAt,
992
+ lastCheckpointSourceEventAt: daemonState.lastCheckpointSourceEventAt || null,
993
+ sourceIdleMs,
994
+ };
995
+
996
+ if (!latestUncheckpointedSourceEvent) {
997
+ return {
998
+ shouldAttempt: false,
999
+ trigger: "",
1000
+ reason: force ? "checkpoint_closeout_no_new_source_events" : "checkpoint_no_new_source_events",
1001
+ sourceEvent: null,
1002
+ policy,
1003
+ };
1004
+ }
1005
+
1006
+ if (force) {
1007
+ return {
1008
+ shouldAttempt: true,
1009
+ trigger: normalizeString(forceReason) || "closeout",
1010
+ reason: "",
1011
+ sourceEvent: latestUncheckpointedSourceEvent,
1012
+ policy,
1013
+ };
1014
+ }
1015
+
1016
+ if (uncheckpointedSourceEvents.length >= eventThreshold) {
1017
+ return {
1018
+ shouldAttempt: true,
1019
+ trigger: "event_threshold",
1020
+ reason: "",
1021
+ sourceEvent: latestUncheckpointedSourceEvent,
1022
+ policy,
1023
+ };
1024
+ }
1025
+
1026
+ if (sourceIdleMs !== null && sourceIdleMs >= idleMs) {
1027
+ return {
1028
+ shouldAttempt: true,
1029
+ trigger: "inactivity",
1030
+ reason: "",
1031
+ sourceEvent: latestUncheckpointedSourceEvent,
1032
+ policy,
1033
+ };
1034
+ }
1035
+
1036
+ return {
1037
+ shouldAttempt: false,
1038
+ trigger: "",
1039
+ reason: "checkpoint_event_count_wait",
1040
+ sourceEvent: latestUncheckpointedSourceEvent,
1041
+ policy,
1042
+ };
1043
+ }
1044
+
843
1045
  function createHealthSummaryBase(nowIso, session, agents) {
844
1046
  return {
845
1047
  sessionId: session.sessionId,
@@ -855,6 +1057,13 @@ function createHealthSummaryBase(nowIso, session, agents) {
855
1057
  cursor: null,
856
1058
  reason: "",
857
1059
  },
1060
+ recap: {
1061
+ emitted: false,
1062
+ mode: "",
1063
+ reason: "",
1064
+ eventId: null,
1065
+ sourceEventCount: 0,
1066
+ },
858
1067
  checkpoint: {
859
1068
  attempted: false,
860
1069
  ok: false,
@@ -1090,7 +1299,8 @@ async function pollAndRelayHumanMessages(
1090
1299
  async function maybeGenerateSessionCheckpoint(
1091
1300
  daemonState,
1092
1301
  summary,
1093
- nowIso = new Date().toISOString()
1302
+ nowIso = new Date().toISOString(),
1303
+ { force = false, forceReason = "" } = {}
1094
1304
  ) {
1095
1305
  const generator = daemonState.checkpointGenerator;
1096
1306
  if (typeof generator !== "function") {
@@ -1103,6 +1313,16 @@ async function maybeGenerateSessionCheckpoint(
1103
1313
  }
1104
1314
 
1105
1315
  const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
1316
+ const trigger = await resolveCheckpointTrigger(daemonState, normalizedNow, {
1317
+ force,
1318
+ forceReason,
1319
+ });
1320
+ summary.checkpoint.policy = trigger.policy;
1321
+ if (!trigger.shouldAttempt) {
1322
+ summary.checkpoint.reason = trigger.reason;
1323
+ return;
1324
+ }
1325
+
1106
1326
  const nowEpoch = parseEpoch(normalizedNow, normalizedNow);
1107
1327
  const lastAttemptAt = normalizeString(daemonState.lastCheckpointAttemptAt);
1108
1328
  const lastAttemptEpoch = lastAttemptAt ? parseEpoch(lastAttemptAt, normalizedNow) : 0;
@@ -1110,7 +1330,7 @@ async function maybeGenerateSessionCheckpoint(
1110
1330
  daemonState.checkpointIntervalMs,
1111
1331
  DEFAULT_CHECKPOINT_INTERVAL_MS
1112
1332
  );
1113
- if (lastAttemptEpoch > 0 && nowEpoch - lastAttemptEpoch < intervalMs) {
1333
+ if (!force && lastAttemptEpoch > 0 && nowEpoch - lastAttemptEpoch < intervalMs) {
1114
1334
  summary.checkpoint.reason = "checkpoint_cadence_wait";
1115
1335
  return;
1116
1336
  }
@@ -1135,9 +1355,16 @@ async function maybeGenerateSessionCheckpoint(
1135
1355
  reason: normalizeString(result?.reason),
1136
1356
  checkpointId: normalizeString(result?.checkpointId || checkpoint?.checkpointId || checkpoint?.checkpoint_id) || null,
1137
1357
  eventCount: Number.isFinite(Number(result?.eventCount)) ? Math.max(0, Math.floor(Number(result.eventCount))) : null,
1358
+ trigger: trigger.trigger,
1359
+ sourceEventCount: trigger.policy.sourceEventCount,
1360
+ latestSourceEventAt: trigger.policy.latestUncheckpointedSourceEventAt,
1361
+ policy: trigger.policy,
1138
1362
  };
1139
1363
  summary.checkpoint = normalizedResult;
1140
1364
  daemonState.lastCheckpointResult = normalizedResult;
1365
+ if (result?.ok !== false || result?.created || result?.duplicate) {
1366
+ rememberCheckpointSource(daemonState, trigger.sourceEvent, normalizedNow);
1367
+ }
1141
1368
  } catch (error) {
1142
1369
  const failure = {
1143
1370
  attempted: true,
@@ -1147,6 +1374,10 @@ async function maybeGenerateSessionCheckpoint(
1147
1374
  reason: normalizeString(error?.message) || "checkpoint_generation_failed",
1148
1375
  checkpointId: null,
1149
1376
  eventCount: null,
1377
+ trigger: trigger.trigger,
1378
+ sourceEventCount: trigger.policy.sourceEventCount,
1379
+ latestSourceEventAt: trigger.policy.latestUncheckpointedSourceEventAt,
1380
+ policy: trigger.policy,
1150
1381
  };
1151
1382
  summary.checkpoint = failure;
1152
1383
  daemonState.lastCheckpointResult = failure;
@@ -1155,6 +1386,42 @@ async function maybeGenerateSessionCheckpoint(
1155
1386
  }
1156
1387
  }
1157
1388
 
1389
+ async function maybeEmitPeriodicRecap(
1390
+ daemonState,
1391
+ summary,
1392
+ nowIso = new Date().toISOString()
1393
+ ) {
1394
+ const emitter = daemonState.recapEmitter;
1395
+ if (!emitter || typeof emitter.tickNow !== "function") {
1396
+ summary.recap.reason = "disabled";
1397
+ return;
1398
+ }
1399
+ try {
1400
+ const emitted = await emitter.tickNow(nowIso);
1401
+ const emitterState =
1402
+ typeof emitter.getState === "function" ? emitter.getState() : {};
1403
+ const decision =
1404
+ emitterState.lastDecision && typeof emitterState.lastDecision === "object"
1405
+ ? emitterState.lastDecision
1406
+ : null;
1407
+ if (!emitted) {
1408
+ summary.recap.mode = normalizeString(decision?.mode);
1409
+ summary.recap.reason = normalizeString(decision?.reason);
1410
+ summary.recap.sourceEventCount = Number(decision?.policy?.sourceEventCount || 0);
1411
+ return;
1412
+ }
1413
+ summary.recap.emitted = true;
1414
+ summary.recap.mode = normalizeString(emitted.payload?.mode || decision?.mode);
1415
+ summary.recap.reason = "";
1416
+ summary.recap.eventId = normalizeString(emitted.eventId) || null;
1417
+ summary.recap.sourceEventCount = Number(
1418
+ emitted.payload?.sourceEventCount || decision?.sourceEventCount || 0
1419
+ );
1420
+ } catch (error) {
1421
+ summary.recap.reason = normalizeString(error?.message) || "recap_emit_failed";
1422
+ }
1423
+ }
1424
+
1158
1425
  export async function runSentiHealthTick(
1159
1426
  sessionId,
1160
1427
  {
@@ -1189,10 +1456,14 @@ export async function runSentiHealthTick(
1189
1456
  tickIntervalMs: DAEMON_TICK_INTERVAL_MS,
1190
1457
  recapIntervalMs: DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
1191
1458
  recapInactivityMs: DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
1459
+ recapActivityThreshold: DEFAULT_RECAP_ACTIVITY_THRESHOLD_OVERRIDE,
1192
1460
  checkpointGenerator: null,
1193
1461
  checkpointIntervalMs: DEFAULT_CHECKPOINT_INTERVAL_MS,
1194
1462
  checkpointMinEvents: DEFAULT_CHECKPOINT_MIN_EVENTS,
1195
1463
  checkpointMaxEvents: DEFAULT_CHECKPOINT_MAX_EVENTS,
1464
+ checkpointEventThreshold: DEFAULT_CHECKPOINT_EVENT_THRESHOLD,
1465
+ checkpointIdleMs: DEFAULT_CHECKPOINT_IDLE_MS,
1466
+ checkpointCloseoutOnStop: true,
1196
1467
  helpResponder: null,
1197
1468
  llmInvoker: invokeViaProxy,
1198
1469
  telemetrySessionId: null,
@@ -1216,6 +1487,7 @@ export async function runSentiHealthTick(
1216
1487
  await emitConflictAlerts(resolvedDaemonState, summary, filteredAgents, normalizedNow);
1217
1488
  await maybeRenewActiveSession(resolvedDaemonState, summary, session, normalizedNow);
1218
1489
  await pollAndRelayHumanMessages(resolvedDaemonState, summary, normalizedNow);
1490
+ await maybeEmitPeriodicRecap(resolvedDaemonState, summary, normalizedNow);
1219
1491
  await maybeGenerateSessionCheckpoint(resolvedDaemonState, summary, normalizedNow);
1220
1492
  return summary;
1221
1493
  }
@@ -1231,10 +1503,14 @@ export async function startSenti(
1231
1503
  helpRequestTimeoutMs = HELP_REQUEST_TIMEOUT_MS,
1232
1504
  recapIntervalMs = DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
1233
1505
  recapInactivityMs = DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
1506
+ recapActivityThreshold = DEFAULT_RECAP_ACTIVITY_THRESHOLD_OVERRIDE,
1234
1507
  checkpointGenerator = generateSessionCheckpointBestEffort,
1235
1508
  checkpointIntervalMs = DEFAULT_CHECKPOINT_INTERVAL_MS,
1236
1509
  checkpointMinEvents = DEFAULT_CHECKPOINT_MIN_EVENTS,
1237
1510
  checkpointMaxEvents = DEFAULT_CHECKPOINT_MAX_EVENTS,
1511
+ checkpointEventThreshold = DEFAULT_CHECKPOINT_EVENT_THRESHOLD,
1512
+ checkpointIdleMs = DEFAULT_CHECKPOINT_IDLE_MS,
1513
+ checkpointCloseoutOnStop = true,
1238
1514
  helpResponder = null,
1239
1515
  llmInvoker = invokeViaProxy,
1240
1516
  } = {}
@@ -1274,6 +1550,10 @@ export async function startSenti(
1274
1550
  recapInactivityMs,
1275
1551
  DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE
1276
1552
  );
1553
+ const normalizedRecapActivityThreshold = Math.min(
1554
+ 200,
1555
+ normalizePositiveInteger(recapActivityThreshold, DEFAULT_RECAP_ACTIVITY_THRESHOLD_OVERRIDE)
1556
+ );
1277
1557
  const normalizedCheckpointIntervalMs = normalizePositiveInteger(
1278
1558
  checkpointIntervalMs,
1279
1559
  DEFAULT_CHECKPOINT_INTERVAL_MS
@@ -1286,6 +1566,14 @@ export async function startSenti(
1286
1566
  normalizedCheckpointMinEvents,
1287
1567
  Math.min(200, normalizePositiveInteger(checkpointMaxEvents, DEFAULT_CHECKPOINT_MAX_EVENTS))
1288
1568
  );
1569
+ const normalizedCheckpointEventThreshold = Math.min(
1570
+ 200,
1571
+ normalizePositiveInteger(checkpointEventThreshold, DEFAULT_CHECKPOINT_EVENT_THRESHOLD)
1572
+ );
1573
+ const normalizedCheckpointIdleMs = normalizePositiveInteger(
1574
+ checkpointIdleMs,
1575
+ DEFAULT_CHECKPOINT_IDLE_MS
1576
+ );
1289
1577
  const nowIso = new Date().toISOString();
1290
1578
  const telemetrySession = startTelemetrySession(`session daemon ${normalizedSessionId}`);
1291
1579
  const daemonState = createSentiState({
@@ -1299,10 +1587,14 @@ export async function startSenti(
1299
1587
  tickIntervalMs: normalizedTickIntervalMs,
1300
1588
  recapIntervalMs: normalizedRecapIntervalMs,
1301
1589
  recapInactivityMs: normalizedRecapInactivityMs,
1590
+ recapActivityThreshold: normalizedRecapActivityThreshold,
1302
1591
  checkpointGenerator: typeof checkpointGenerator === "function" ? checkpointGenerator : null,
1303
1592
  checkpointIntervalMs: normalizedCheckpointIntervalMs,
1304
1593
  checkpointMinEvents: normalizedCheckpointMinEvents,
1305
1594
  checkpointMaxEvents: normalizedCheckpointMaxEvents,
1595
+ checkpointEventThreshold: normalizedCheckpointEventThreshold,
1596
+ checkpointIdleMs: normalizedCheckpointIdleMs,
1597
+ checkpointCloseoutOnStop: checkpointCloseoutOnStop !== false,
1306
1598
  helpResponder,
1307
1599
  llmInvoker: typeof llmInvoker === "function" ? llmInvoker : invokeViaProxy,
1308
1600
  telemetrySessionId: telemetrySession?.id || null,
@@ -1377,6 +1669,45 @@ export async function startSenti(
1377
1669
  daemonState.recapEmitter = null;
1378
1670
  }
1379
1671
 
1672
+ let checkpointCloseout = null;
1673
+ if (daemonState.checkpointCloseoutOnStop) {
1674
+ const checkpointNowIso = new Date().toISOString();
1675
+ try {
1676
+ const closeoutSession = await getSession(normalizedSessionId, {
1677
+ targetPath: normalizedTargetPath,
1678
+ });
1679
+ const closeoutAgents = await listAgents(normalizedSessionId, {
1680
+ targetPath: normalizedTargetPath,
1681
+ includeInactive: false,
1682
+ });
1683
+ const closeoutSummary = createHealthSummaryBase(
1684
+ checkpointNowIso,
1685
+ closeoutSession || session,
1686
+ closeoutAgents
1687
+ );
1688
+ await maybeGenerateSessionCheckpoint(
1689
+ daemonState,
1690
+ closeoutSummary,
1691
+ checkpointNowIso,
1692
+ {
1693
+ force: true,
1694
+ forceReason: "closeout",
1695
+ }
1696
+ );
1697
+ checkpointCloseout = closeoutSummary.checkpoint;
1698
+ } catch (error) {
1699
+ checkpointCloseout = {
1700
+ attempted: true,
1701
+ ok: false,
1702
+ created: false,
1703
+ duplicate: false,
1704
+ reason: normalizeString(error?.message) || "checkpoint_closeout_failed",
1705
+ checkpointId: null,
1706
+ eventCount: null,
1707
+ };
1708
+ }
1709
+ }
1710
+
1380
1711
  let runtimeStopSummary = null;
1381
1712
  try {
1382
1713
  runtimeStopSummary = await stopRuntimeRunsForSession(normalizedSessionId, {
@@ -1408,6 +1739,7 @@ export async function startSenti(
1408
1739
  target: SENTI_IDENTITY.id,
1409
1740
  reason: normalizeString(reason) || "manual_stop",
1410
1741
  runtimeStops: runtimeStopSummary?.stoppedCount || 0,
1742
+ checkpointCloseout,
1411
1743
  },
1412
1744
  {
1413
1745
  targetPath: normalizedTargetPath,
@@ -1424,6 +1756,7 @@ export async function startSenti(
1424
1756
  sessionId: normalizedSessionId,
1425
1757
  targetPath: normalizedTargetPath,
1426
1758
  reason: normalizeString(reason) || "manual_stop",
1759
+ checkpointCloseout,
1427
1760
  runtimeStopSummary,
1428
1761
  event: killedEvent,
1429
1762
  };
@@ -1448,10 +1781,15 @@ export async function startSenti(
1448
1781
  staleAlertedAgents: [...daemonState.staleAlertedAgents],
1449
1782
  pendingHelpRequests: daemonState.pendingHelpTimers.size,
1450
1783
  recapRunning: Boolean(daemonState.recapEmitter?.isRunning?.()),
1784
+ recapActivityThreshold: daemonState.recapActivityThreshold,
1451
1785
  checkpointIntervalMs: daemonState.checkpointIntervalMs,
1452
1786
  checkpointMinEvents: daemonState.checkpointMinEvents,
1453
1787
  checkpointMaxEvents: daemonState.checkpointMaxEvents,
1788
+ checkpointEventThreshold: daemonState.checkpointEventThreshold,
1789
+ checkpointIdleMs: daemonState.checkpointIdleMs,
1454
1790
  lastCheckpointAttemptAt: daemonState.lastCheckpointAttemptAt,
1791
+ lastCheckpointSourceEventAt: daemonState.lastCheckpointSourceEventAt,
1792
+ lastCheckpointSourceSequenceId: daemonState.lastCheckpointSourceSequenceId,
1455
1793
  lastCheckpointResult: daemonState.lastCheckpointResult,
1456
1794
  humanMessageCursor: daemonState.humanMessageCursor,
1457
1795
  }),
@@ -1466,6 +1804,7 @@ export async function startSenti(
1466
1804
  targetPath: normalizedTargetPath,
1467
1805
  intervalMs: daemonState.recapIntervalMs,
1468
1806
  inactivityMs: daemonState.recapInactivityMs,
1807
+ newEventThreshold: daemonState.recapActivityThreshold,
1469
1808
  });
1470
1809
 
1471
1810
  if (autoStart) {