sentinelayer-cli 0.39.2 → 0.40.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.
@@ -3,6 +3,7 @@ import process from "node:process";
3
3
 
4
4
  import { SentinelayerApiError, requestJson, requestJsonMutation } from "../auth/http.js";
5
5
  import { resolveActiveAuthSession } from "../auth/service.js";
6
+ import { pollSessionEventsBefore } from "./sync.js";
6
7
 
7
8
  const DEFAULT_API_BASE_URL = "https://api.sentinelayer.com";
8
9
  const DEFAULT_CHECKPOINT_LIMIT = 100;
@@ -11,6 +12,10 @@ const DEFAULT_MIN_EVENTS = 20;
11
12
  const DEFAULT_MAX_EVENTS = 80;
12
13
  const DEFAULT_BATCH_MAX_CHECKPOINTS = 5;
13
14
  const MAX_BATCH_MAX_CHECKPOINTS = 50;
15
+ const DEFAULT_RESTORE_CONTEXT_EVENTS = 3;
16
+ const DEFAULT_RESTORE_MAX_EVENTS = 120;
17
+ const MAX_RESTORE_CONTEXT_EVENTS = 50;
18
+ const MAX_RESTORE_EVENTS = 200;
14
19
  const DEFAULT_CREATED_BY_AGENT_ID = "senti";
15
20
 
16
21
  function normalizeString(value) {
@@ -33,6 +38,17 @@ function parsePositiveInteger(value, field, fallbackValue = null) {
33
38
  return Math.floor(parsed);
34
39
  }
35
40
 
41
+ function parseNonNegativeInteger(value, field, fallbackValue = 0) {
42
+ if (value === undefined || value === null || normalizeString(value) === "") {
43
+ return Math.max(0, Math.floor(Number(fallbackValue) || 0));
44
+ }
45
+ const parsed = Number(value);
46
+ if (!Number.isFinite(parsed) || parsed < 0) {
47
+ throw new Error(`${field} must be a non-negative integer.`);
48
+ }
49
+ return Math.floor(parsed);
50
+ }
51
+
36
52
  function normalizeLimit(value) {
37
53
  const parsed = parsePositiveInteger(value, "limit", DEFAULT_CHECKPOINT_LIMIT);
38
54
  return Math.max(1, Math.min(MAX_CHECKPOINT_LIMIT, parsed));
@@ -47,6 +63,16 @@ function normalizeBatchMaxCheckpoints(value) {
47
63
  return Math.max(1, Math.min(MAX_BATCH_MAX_CHECKPOINTS, parsed));
48
64
  }
49
65
 
66
+ function normalizeRestoreMaxEvents(value) {
67
+ const parsed = parsePositiveInteger(value, "max-events", DEFAULT_RESTORE_MAX_EVENTS);
68
+ return Math.max(1, Math.min(MAX_RESTORE_EVENTS, parsed));
69
+ }
70
+
71
+ function normalizeRestoreContextEvents(value) {
72
+ const parsed = parseNonNegativeInteger(value, "context-events", DEFAULT_RESTORE_CONTEXT_EVENTS);
73
+ return Math.max(0, Math.min(MAX_RESTORE_CONTEXT_EVENTS, parsed));
74
+ }
75
+
50
76
  function stableHash(value) {
51
77
  return crypto
52
78
  .createHash("sha256")
@@ -148,6 +174,20 @@ function normalizeTokenRange({ tokenStart, tokenEnd } = {}) {
148
174
  return { start, end };
149
175
  }
150
176
 
177
+ function checkpointIdOf(checkpoint = {}) {
178
+ return normalizeString(checkpoint.checkpointId || checkpoint.checkpoint_id);
179
+ }
180
+
181
+ function checkpointSequenceValue(checkpoint = {}, camelKey, snakeKey) {
182
+ const parsed = Number(checkpoint[camelKey] ?? checkpoint[snakeKey]);
183
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
184
+ }
185
+
186
+ function eventSequenceValue(event = {}) {
187
+ const parsed = Number(event.sequenceId ?? event.sequence_id ?? event.sequence);
188
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
189
+ }
190
+
151
191
  export function buildManualCheckpointPayload(sessionId, {
152
192
  checkpointId = "",
153
193
  startSequence,
@@ -235,6 +275,99 @@ export function buildGenerateCheckpointPayload(sessionId, {
235
275
  };
236
276
  }
237
277
 
278
+ export function findCheckpointById(checkpoints = [], checkpointId = "") {
279
+ const normalizedCheckpointId = normalizeString(checkpointId);
280
+ if (!normalizedCheckpointId) return null;
281
+ return Array.isArray(checkpoints)
282
+ ? checkpoints.find((checkpoint) => checkpointIdOf(checkpoint) === normalizedCheckpointId) || null
283
+ : null;
284
+ }
285
+
286
+ export function buildCheckpointRestoreWindow(checkpoint = {}, {
287
+ contextEvents = DEFAULT_RESTORE_CONTEXT_EVENTS,
288
+ maxEvents = DEFAULT_RESTORE_MAX_EVENTS,
289
+ } = {}) {
290
+ const startSequence = checkpointSequenceValue(checkpoint, "startSequence", "start_sequence");
291
+ const endSequence = checkpointSequenceValue(checkpoint, "endSequence", "end_sequence");
292
+ if (startSequence <= 0) {
293
+ throw new Error("checkpoint startSequence is required for restore.");
294
+ }
295
+ if (endSequence <= 0) {
296
+ throw new Error("checkpoint endSequence is required for restore.");
297
+ }
298
+ if (startSequence > endSequence) {
299
+ throw new Error("checkpoint startSequence must be less than or equal to endSequence.");
300
+ }
301
+ const normalizedContext = normalizeRestoreContextEvents(contextEvents);
302
+ const normalizedMaxEvents = normalizeRestoreMaxEvents(maxEvents);
303
+ const sourceEventCountExpected = endSequence - startSequence + 1;
304
+ const requestedEventCount = sourceEventCountExpected + normalizedContext * 2;
305
+ const limit = Math.min(normalizedMaxEvents, requestedEventCount, MAX_RESTORE_EVENTS);
306
+ return {
307
+ checkpointId: checkpointIdOf(checkpoint) || null,
308
+ startSequence,
309
+ endSequence,
310
+ contextEvents: normalizedContext,
311
+ maxEvents: normalizedMaxEvents,
312
+ limit,
313
+ beforeSequence: endSequence + normalizedContext + 1,
314
+ lowerBoundSequence: Math.max(1, startSequence - normalizedContext),
315
+ sourceEventCountExpected,
316
+ truncatedByLimit: limit < requestedEventCount,
317
+ };
318
+ }
319
+
320
+ export function classifyCheckpointRestoreEvents(events = [], window = {}) {
321
+ const normalizedEvents = Array.isArray(events) ? events : [];
322
+ const startSequence = Number(window.startSequence || 0);
323
+ const endSequence = Number(window.endSequence || 0);
324
+ const sourceEventCountExpected = Number(window.sourceEventCountExpected || 0);
325
+ const beforeContextEvents = [];
326
+ const sourceEvents = [];
327
+ const afterContextEvents = [];
328
+ const sourceSequenceIds = new Set();
329
+
330
+ for (const event of normalizedEvents) {
331
+ const sequence = eventSequenceValue(event);
332
+ if (sequence <= 0) continue;
333
+ if (sequence < startSequence) {
334
+ beforeContextEvents.push(event);
335
+ } else if (sequence > endSequence) {
336
+ afterContextEvents.push(event);
337
+ } else {
338
+ sourceEvents.push(event);
339
+ sourceSequenceIds.add(sequence);
340
+ }
341
+ }
342
+
343
+ const missingStart = startSequence > 0 && !sourceSequenceIds.has(startSequence);
344
+ const missingEnd = endSequence > 0 && !sourceSequenceIds.has(endSequence);
345
+ const missingSourceEvents = Math.max(0, sourceEventCountExpected - sourceSequenceIds.size);
346
+ return {
347
+ events: normalizedEvents,
348
+ beforeContextEvents,
349
+ sourceEvents,
350
+ afterContextEvents,
351
+ eventCount: normalizedEvents.length,
352
+ beforeContextCount: beforeContextEvents.length,
353
+ sourceEventCount: sourceEvents.length,
354
+ afterContextCount: afterContextEvents.length,
355
+ missingStart,
356
+ missingEnd,
357
+ missingSourceEvents,
358
+ completeSourceRange:
359
+ sourceEventCountExpected > 0 &&
360
+ !missingStart &&
361
+ !missingEnd &&
362
+ missingSourceEvents === 0,
363
+ partial:
364
+ Boolean(window.truncatedByLimit) ||
365
+ missingStart ||
366
+ missingEnd ||
367
+ missingSourceEvents > 0,
368
+ };
369
+ }
370
+
238
371
  export function normalizeCheckpointGenerationResult(payload = {}) {
239
372
  const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
240
373
  const checkpoint =
@@ -308,6 +441,63 @@ export async function listSessionCheckpoints(sessionId, {
308
441
  };
309
442
  }
310
443
 
444
+ export async function showSessionCheckpoint(sessionId, checkpointId, {
445
+ targetPath = process.cwd(),
446
+ contextEvents = DEFAULT_RESTORE_CONTEXT_EVENTS,
447
+ maxEvents = DEFAULT_RESTORE_MAX_EVENTS,
448
+ resolveAuthSession = resolveActiveAuthSession,
449
+ request = requestJson,
450
+ fetchEventsBefore = pollSessionEventsBefore,
451
+ } = {}) {
452
+ const normalizedSessionId = normalizeString(sessionId);
453
+ if (!normalizedSessionId) {
454
+ throw new Error("session id is required.");
455
+ }
456
+ const normalizedCheckpointId = normalizeString(checkpointId);
457
+ if (!normalizedCheckpointId) {
458
+ throw new Error("checkpoint id is required.");
459
+ }
460
+
461
+ const listed = await listSessionCheckpoints(normalizedSessionId, {
462
+ targetPath,
463
+ limit: MAX_CHECKPOINT_LIMIT,
464
+ resolveAuthSession,
465
+ request,
466
+ });
467
+ const checkpoint = findCheckpointById(listed.checkpoints, normalizedCheckpointId);
468
+ if (!checkpoint) {
469
+ throw new Error(`Checkpoint '${normalizedCheckpointId}' was not found in session ${normalizedSessionId}.`);
470
+ }
471
+ const restoreWindow = buildCheckpointRestoreWindow(checkpoint, {
472
+ contextEvents,
473
+ maxEvents,
474
+ });
475
+ const remote = await fetchEventsBefore(normalizedSessionId, {
476
+ targetPath,
477
+ beforeSequence: restoreWindow.beforeSequence,
478
+ limit: restoreWindow.limit,
479
+ forceCircuitProbe: true,
480
+ resolveAuthSession,
481
+ });
482
+ const classified = classifyCheckpointRestoreEvents(remote?.events || [], restoreWindow);
483
+ return {
484
+ ok: Boolean(remote?.ok),
485
+ reason: normalizeString(remote?.reason),
486
+ sessionId: normalizedSessionId,
487
+ checkpointId: normalizedCheckpointId,
488
+ apiUrl: listed.apiUrl,
489
+ checkpoint,
490
+ window: {
491
+ ...restoreWindow,
492
+ ...classified,
493
+ remoteOk: Boolean(remote?.ok),
494
+ remoteReason: normalizeString(remote?.reason),
495
+ remoteBeforeSequence: remote?.beforeSequence || null,
496
+ remoteCursor: remote?.cursor || null,
497
+ },
498
+ };
499
+ }
500
+
311
501
  export async function createSessionCheckpoint(sessionId, options = {}) {
312
502
  const normalizedSessionId = normalizeString(sessionId);
313
503
  const {
@@ -446,5 +636,9 @@ export {
446
636
  DEFAULT_BATCH_MAX_CHECKPOINTS,
447
637
  DEFAULT_MAX_EVENTS,
448
638
  DEFAULT_MIN_EVENTS,
639
+ DEFAULT_RESTORE_CONTEXT_EVENTS,
640
+ DEFAULT_RESTORE_MAX_EVENTS,
449
641
  MAX_BATCH_MAX_CHECKPOINTS,
642
+ MAX_RESTORE_CONTEXT_EVENTS,
643
+ MAX_RESTORE_EVENTS,
450
644
  };