sentinelayer-cli 0.13.1 → 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 +1 -1
- package/src/session/checkpoints.js +87 -1
- package/src/session/daemon.js +124 -0
package/package.json
CHANGED
|
@@ -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
|
+
};
|
package/src/session/daemon.js
CHANGED
|
@@ -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
|
};
|