sentinelayer-cli 0.13.0 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import process from "node:process";
3
3
 
4
- import { requestJson, requestJsonMutation } from "../auth/http.js";
4
+ import { SentinelayerApiError, requestJson, requestJsonMutation } from "../auth/http.js";
5
5
  import { resolveActiveAuthSession } from "../auth/service.js";
6
6
 
7
7
  const DEFAULT_API_BASE_URL = "https://api.sentinelayer.com";
@@ -9,6 +9,7 @@ const DEFAULT_CHECKPOINT_LIMIT = 100;
9
9
  const MAX_CHECKPOINT_LIMIT = 200;
10
10
  const DEFAULT_MIN_EVENTS = 20;
11
11
  const DEFAULT_MAX_EVENTS = 80;
12
+ const DEFAULT_CREATED_BY_AGENT_ID = "senti";
12
13
 
13
14
  function normalizeString(value) {
14
15
  return String(value || "").trim();
@@ -96,6 +97,28 @@ function buildInvocationIdempotencyKey(operation) {
96
97
  return `sl_cli_session_checkpoint_${normalizeString(operation) || "mutation"}_${suffix}`;
97
98
  }
98
99
 
100
+ function normalizeReason(value, fallbackValue = "checkpoint_generate_failed") {
101
+ return (
102
+ normalizeString(value)
103
+ .toLowerCase()
104
+ .replace(/[^a-z0-9_:-]+/g, "_")
105
+ .replace(/^_+|_+$/g, "") || fallbackValue
106
+ );
107
+ }
108
+
109
+ function buildCheckpointNoop(reason, extra = {}) {
110
+ return {
111
+ ok: false,
112
+ created: false,
113
+ duplicate: false,
114
+ reason: normalizeReason(reason),
115
+ checkpoint: null,
116
+ checkpointId: null,
117
+ eventCount: null,
118
+ ...extra,
119
+ };
120
+ }
121
+
99
122
  function normalizeTokenRange({ tokenStart, tokenEnd } = {}) {
100
123
  const hasStart = tokenStart !== undefined && normalizeString(tokenStart) !== "";
101
124
  const hasEnd = tokenEnd !== undefined && normalizeString(tokenEnd) !== "";
@@ -195,6 +218,35 @@ export function buildGenerateCheckpointPayload(sessionId, {
195
218
  };
196
219
  }
197
220
 
221
+ export function normalizeCheckpointGenerationResult(payload = {}) {
222
+ const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
223
+ const checkpoint =
224
+ source.checkpoint && typeof source.checkpoint === "object" && !Array.isArray(source.checkpoint)
225
+ ? source.checkpoint
226
+ : null;
227
+ const checkpointId = normalizeString(
228
+ source.checkpointId ||
229
+ source.checkpoint_id ||
230
+ checkpoint?.checkpointId ||
231
+ checkpoint?.checkpoint_id,
232
+ ) || null;
233
+ const eventCount = Number(source.eventCount ?? source.event_count);
234
+ const minEvents = Number(source.minEvents ?? source.min_events ?? DEFAULT_MIN_EVENTS);
235
+ const maxEvents = Number(source.maxEvents ?? source.max_events ?? DEFAULT_MAX_EVENTS);
236
+ return {
237
+ ...source,
238
+ ok: source.ok !== false,
239
+ created: Boolean(source.created || checkpoint),
240
+ duplicate: Boolean(source.duplicate),
241
+ reason: normalizeReason(source.reason, ""),
242
+ checkpoint,
243
+ checkpointId,
244
+ eventCount: Number.isFinite(eventCount) ? Math.max(0, Math.floor(eventCount)) : null,
245
+ minEvents: Number.isFinite(minEvents) ? Math.max(1, Math.floor(minEvents)) : DEFAULT_MIN_EVENTS,
246
+ maxEvents: Number.isFinite(maxEvents) ? Math.max(1, Math.floor(maxEvents)) : DEFAULT_MAX_EVENTS,
247
+ };
248
+ }
249
+
198
250
  async function resolveCheckpointApi({
199
251
  targetPath = process.cwd(),
200
252
  resolveAuthSession = resolveActiveAuthSession,
@@ -292,3 +344,37 @@ export async function generateSessionCheckpoint(sessionId, options = {}) {
292
344
  idempotencyKey,
293
345
  };
294
346
  }
347
+
348
+ export async function generateSessionCheckpointBestEffort(sessionId, options = {}) {
349
+ const normalizedSessionId = normalizeString(sessionId);
350
+ if (!normalizedSessionId) {
351
+ return buildCheckpointNoop("invalid_session_id");
352
+ }
353
+ if (normalizeString(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "") === "1") {
354
+ return buildCheckpointNoop("remote_sync_disabled_env");
355
+ }
356
+ try {
357
+ const result = await generateSessionCheckpoint(normalizedSessionId, {
358
+ ...options,
359
+ createdByAgentId: normalizeString(options.createdByAgentId) || DEFAULT_CREATED_BY_AGENT_ID,
360
+ });
361
+ return normalizeCheckpointGenerationResult(result);
362
+ } catch (error) {
363
+ if (error instanceof SentinelayerApiError) {
364
+ return buildCheckpointNoop(`api_${error.status || error.code || "error"}`, {
365
+ status: error.status || null,
366
+ code: error.code || null,
367
+ requestId: error.requestId || null,
368
+ });
369
+ }
370
+ const message = normalizeString(error?.message);
371
+ const reason = /auth|login|token/i.test(message) ? "not_authenticated" : message;
372
+ return buildCheckpointNoop(reason || "checkpoint_generate_failed");
373
+ }
374
+ }
375
+
376
+ export {
377
+ DEFAULT_CREATED_BY_AGENT_ID,
378
+ DEFAULT_MAX_EVENTS,
379
+ DEFAULT_MIN_EVENTS,
380
+ };
@@ -27,6 +27,11 @@ import {
27
27
  lockFile,
28
28
  unlockFile,
29
29
  } from "./file-locks.js";
30
+ import {
31
+ DEFAULT_MAX_EVENTS as DEFAULT_CHECKPOINT_MAX_EVENTS,
32
+ DEFAULT_MIN_EVENTS as DEFAULT_CHECKPOINT_MIN_EVENTS,
33
+ generateSessionCheckpointBestEffort,
34
+ } from "./checkpoints.js";
30
35
  import { resolveSessionPaths } from "./paths.js";
31
36
  import {
32
37
  DEFAULT_RECAP_INACTIVITY_MS,
@@ -53,6 +58,7 @@ const RENEWAL_LEAD_MS = 60 * 60 * 1000;
53
58
  const DEFAULT_STALE_AGENT_SECONDS = 90;
54
59
  const DEFAULT_RECAP_INTERVAL_MS_OVERRIDE = DEFAULT_RECAP_INTERVAL_MS;
55
60
  const DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE = DEFAULT_RECAP_INACTIVITY_MS;
61
+ const DEFAULT_CHECKPOINT_INTERVAL_MS = 60_000;
56
62
 
57
63
  const SENTI_MODEL = "gpt-5.4-mini";
58
64
  const SENTI_IDENTITY = Object.freeze({
@@ -170,6 +176,10 @@ function createSentiState({
170
176
  tickIntervalMs,
171
177
  recapIntervalMs,
172
178
  recapInactivityMs,
179
+ checkpointGenerator,
180
+ checkpointIntervalMs,
181
+ checkpointMinEvents,
182
+ checkpointMaxEvents,
173
183
  helpResponder,
174
184
  llmInvoker,
175
185
  telemetrySessionId,
@@ -185,6 +195,13 @@ function createSentiState({
185
195
  tickIntervalMs,
186
196
  recapIntervalMs,
187
197
  recapInactivityMs,
198
+ checkpointGenerator,
199
+ checkpointIntervalMs,
200
+ checkpointMinEvents,
201
+ checkpointMaxEvents,
202
+ checkpointGenerationInFlight: false,
203
+ lastCheckpointAttemptAt: null,
204
+ lastCheckpointResult: null,
188
205
  helpResponder,
189
206
  llmInvoker,
190
207
  telemetrySessionId,
@@ -838,6 +855,15 @@ function createHealthSummaryBase(nowIso, session, agents) {
838
855
  cursor: null,
839
856
  reason: "",
840
857
  },
858
+ checkpoint: {
859
+ attempted: false,
860
+ ok: false,
861
+ created: false,
862
+ duplicate: false,
863
+ reason: "",
864
+ checkpointId: null,
865
+ eventCount: null,
866
+ },
841
867
  };
842
868
  }
843
869
 
@@ -1061,6 +1087,74 @@ async function pollAndRelayHumanMessages(
1061
1087
  }
1062
1088
  }
1063
1089
 
1090
+ async function maybeGenerateSessionCheckpoint(
1091
+ daemonState,
1092
+ summary,
1093
+ nowIso = new Date().toISOString()
1094
+ ) {
1095
+ const generator = daemonState.checkpointGenerator;
1096
+ if (typeof generator !== "function") {
1097
+ summary.checkpoint.reason = "disabled";
1098
+ return;
1099
+ }
1100
+ if (daemonState.checkpointGenerationInFlight) {
1101
+ summary.checkpoint.reason = "checkpoint_generation_in_progress";
1102
+ return;
1103
+ }
1104
+
1105
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
1106
+ const nowEpoch = parseEpoch(normalizedNow, normalizedNow);
1107
+ const lastAttemptAt = normalizeString(daemonState.lastCheckpointAttemptAt);
1108
+ const lastAttemptEpoch = lastAttemptAt ? parseEpoch(lastAttemptAt, normalizedNow) : 0;
1109
+ const intervalMs = normalizePositiveInteger(
1110
+ daemonState.checkpointIntervalMs,
1111
+ DEFAULT_CHECKPOINT_INTERVAL_MS
1112
+ );
1113
+ if (lastAttemptEpoch > 0 && nowEpoch - lastAttemptEpoch < intervalMs) {
1114
+ summary.checkpoint.reason = "checkpoint_cadence_wait";
1115
+ return;
1116
+ }
1117
+
1118
+ daemonState.checkpointGenerationInFlight = true;
1119
+ daemonState.lastCheckpointAttemptAt = normalizedNow;
1120
+ summary.checkpoint.attempted = true;
1121
+ try {
1122
+ const result = await generator(daemonState.sessionId, {
1123
+ targetPath: daemonState.targetPath,
1124
+ minEvents: daemonState.checkpointMinEvents,
1125
+ maxEvents: daemonState.checkpointMaxEvents,
1126
+ createdByAgentId: SENTI_IDENTITY.id,
1127
+ nowIso: normalizedNow,
1128
+ });
1129
+ const checkpoint = result?.checkpoint && typeof result.checkpoint === "object" ? result.checkpoint : null;
1130
+ const normalizedResult = {
1131
+ attempted: true,
1132
+ ok: result?.ok !== false,
1133
+ created: Boolean(result?.created),
1134
+ duplicate: Boolean(result?.duplicate),
1135
+ reason: normalizeString(result?.reason),
1136
+ checkpointId: normalizeString(result?.checkpointId || checkpoint?.checkpointId || checkpoint?.checkpoint_id) || null,
1137
+ eventCount: Number.isFinite(Number(result?.eventCount)) ? Math.max(0, Math.floor(Number(result.eventCount))) : null,
1138
+ };
1139
+ summary.checkpoint = normalizedResult;
1140
+ daemonState.lastCheckpointResult = normalizedResult;
1141
+ } catch (error) {
1142
+ const failure = {
1143
+ attempted: true,
1144
+ ok: false,
1145
+ created: false,
1146
+ duplicate: false,
1147
+ reason: normalizeString(error?.message) || "checkpoint_generation_failed",
1148
+ checkpointId: null,
1149
+ eventCount: null,
1150
+ };
1151
+ summary.checkpoint = failure;
1152
+ daemonState.lastCheckpointResult = failure;
1153
+ } finally {
1154
+ daemonState.checkpointGenerationInFlight = false;
1155
+ }
1156
+ }
1157
+
1064
1158
  export async function runSentiHealthTick(
1065
1159
  sessionId,
1066
1160
  {
@@ -1095,6 +1189,10 @@ export async function runSentiHealthTick(
1095
1189
  tickIntervalMs: DAEMON_TICK_INTERVAL_MS,
1096
1190
  recapIntervalMs: DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
1097
1191
  recapInactivityMs: DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
1192
+ checkpointGenerator: null,
1193
+ checkpointIntervalMs: DEFAULT_CHECKPOINT_INTERVAL_MS,
1194
+ checkpointMinEvents: DEFAULT_CHECKPOINT_MIN_EVENTS,
1195
+ checkpointMaxEvents: DEFAULT_CHECKPOINT_MAX_EVENTS,
1098
1196
  helpResponder: null,
1099
1197
  llmInvoker: invokeViaProxy,
1100
1198
  telemetrySessionId: null,
@@ -1118,6 +1216,7 @@ export async function runSentiHealthTick(
1118
1216
  await emitConflictAlerts(resolvedDaemonState, summary, filteredAgents, normalizedNow);
1119
1217
  await maybeRenewActiveSession(resolvedDaemonState, summary, session, normalizedNow);
1120
1218
  await pollAndRelayHumanMessages(resolvedDaemonState, summary, normalizedNow);
1219
+ await maybeGenerateSessionCheckpoint(resolvedDaemonState, summary, normalizedNow);
1121
1220
  return summary;
1122
1221
  }
1123
1222
 
@@ -1132,6 +1231,10 @@ export async function startSenti(
1132
1231
  helpRequestTimeoutMs = HELP_REQUEST_TIMEOUT_MS,
1133
1232
  recapIntervalMs = DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
1134
1233
  recapInactivityMs = DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
1234
+ checkpointGenerator = generateSessionCheckpointBestEffort,
1235
+ checkpointIntervalMs = DEFAULT_CHECKPOINT_INTERVAL_MS,
1236
+ checkpointMinEvents = DEFAULT_CHECKPOINT_MIN_EVENTS,
1237
+ checkpointMaxEvents = DEFAULT_CHECKPOINT_MAX_EVENTS,
1135
1238
  helpResponder = null,
1136
1239
  llmInvoker = invokeViaProxy,
1137
1240
  } = {}
@@ -1171,6 +1274,18 @@ export async function startSenti(
1171
1274
  recapInactivityMs,
1172
1275
  DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE
1173
1276
  );
1277
+ const normalizedCheckpointIntervalMs = normalizePositiveInteger(
1278
+ checkpointIntervalMs,
1279
+ DEFAULT_CHECKPOINT_INTERVAL_MS
1280
+ );
1281
+ const normalizedCheckpointMinEvents = Math.min(
1282
+ 200,
1283
+ normalizePositiveInteger(checkpointMinEvents, DEFAULT_CHECKPOINT_MIN_EVENTS)
1284
+ );
1285
+ const normalizedCheckpointMaxEvents = Math.max(
1286
+ normalizedCheckpointMinEvents,
1287
+ Math.min(200, normalizePositiveInteger(checkpointMaxEvents, DEFAULT_CHECKPOINT_MAX_EVENTS))
1288
+ );
1174
1289
  const nowIso = new Date().toISOString();
1175
1290
  const telemetrySession = startTelemetrySession(`session daemon ${normalizedSessionId}`);
1176
1291
  const daemonState = createSentiState({
@@ -1184,6 +1299,10 @@ export async function startSenti(
1184
1299
  tickIntervalMs: normalizedTickIntervalMs,
1185
1300
  recapIntervalMs: normalizedRecapIntervalMs,
1186
1301
  recapInactivityMs: normalizedRecapInactivityMs,
1302
+ checkpointGenerator: typeof checkpointGenerator === "function" ? checkpointGenerator : null,
1303
+ checkpointIntervalMs: normalizedCheckpointIntervalMs,
1304
+ checkpointMinEvents: normalizedCheckpointMinEvents,
1305
+ checkpointMaxEvents: normalizedCheckpointMaxEvents,
1187
1306
  helpResponder,
1188
1307
  llmInvoker: typeof llmInvoker === "function" ? llmInvoker : invokeViaProxy,
1189
1308
  telemetrySessionId: telemetrySession?.id || null,
@@ -1329,6 +1448,11 @@ export async function startSenti(
1329
1448
  staleAlertedAgents: [...daemonState.staleAlertedAgents],
1330
1449
  pendingHelpRequests: daemonState.pendingHelpTimers.size,
1331
1450
  recapRunning: Boolean(daemonState.recapEmitter?.isRunning?.()),
1451
+ checkpointIntervalMs: daemonState.checkpointIntervalMs,
1452
+ checkpointMinEvents: daemonState.checkpointMinEvents,
1453
+ checkpointMaxEvents: daemonState.checkpointMaxEvents,
1454
+ lastCheckpointAttemptAt: daemonState.lastCheckpointAttemptAt,
1455
+ lastCheckpointResult: daemonState.lastCheckpointResult,
1332
1456
  humanMessageCursor: daemonState.humanMessageCursor,
1333
1457
  }),
1334
1458
  };
@@ -55,6 +55,14 @@ export function sessionEventIdentityKeys(event = {}) {
55
55
  if (messageId) {
56
56
  keys.push(`message:${messageId}`);
57
57
  }
58
+ const actionId = typeof payload.actionId === "string"
59
+ ? payload.actionId.trim()
60
+ : typeof payload.action_id === "string"
61
+ ? payload.action_id.trim()
62
+ : "";
63
+ if (actionId) {
64
+ keys.push(`action:${actionId}`);
65
+ }
58
66
  const timestamp = timestampKey(event.ts, event.timestamp, event.at);
59
67
  const hasPayloadSignal = Object.keys(payload).length > 0;
60
68
  const hasFingerprintSignal =