sentinelayer-cli 0.18.1 → 0.19.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/daemon.js +37 -19
- package/src/session/recap.js +137 -2
- package/src/session/sync.js +18 -2
package/package.json
CHANGED
package/src/session/daemon.js
CHANGED
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
DEFAULT_RECAP_INTERVAL_MS,
|
|
40
40
|
emitPeriodicRecap,
|
|
41
41
|
} from "./recap.js";
|
|
42
|
+
import { hydrateSessionFromRemote } from "./remote-hydrate.js";
|
|
42
43
|
import { stopRuntimeRunsForSession } from "./runtime-bridge.js";
|
|
43
|
-
import { pollHumanMessages } from "./sync.js";
|
|
44
44
|
import { getSession, renewSession } from "./store.js";
|
|
45
45
|
import { appendToStream, readStream, tailStream } from "./stream.js";
|
|
46
46
|
import { handleTaskDirective } from "./tasks.js";
|
|
@@ -214,6 +214,7 @@ function createSentiState({
|
|
|
214
214
|
checkpointCloseoutOnStop,
|
|
215
215
|
helpResponder,
|
|
216
216
|
llmInvoker,
|
|
217
|
+
remoteHydrator,
|
|
217
218
|
telemetrySessionId,
|
|
218
219
|
}) {
|
|
219
220
|
return {
|
|
@@ -243,6 +244,7 @@ function createSentiState({
|
|
|
243
244
|
lastCheckpointResult: null,
|
|
244
245
|
helpResponder,
|
|
245
246
|
llmInvoker,
|
|
247
|
+
remoteHydrator,
|
|
246
248
|
telemetrySessionId,
|
|
247
249
|
running: true,
|
|
248
250
|
tickTimer: null,
|
|
@@ -255,6 +257,7 @@ function createSentiState({
|
|
|
255
257
|
lastTickSummary: null,
|
|
256
258
|
recapEmitter: null,
|
|
257
259
|
humanMessageCursor: null,
|
|
260
|
+
sessionEventsCursor: null,
|
|
258
261
|
humanMessagePollInFlight: false,
|
|
259
262
|
};
|
|
260
263
|
}
|
|
@@ -1055,6 +1058,9 @@ function createHealthSummaryBase(nowIso, session, agents) {
|
|
|
1055
1058
|
relayed: 0,
|
|
1056
1059
|
dropped: 0,
|
|
1057
1060
|
cursor: null,
|
|
1061
|
+
sessionEventsRelayed: 0,
|
|
1062
|
+
sessionEventsCursor: null,
|
|
1063
|
+
eventsBackfillComplete: null,
|
|
1058
1064
|
reason: "",
|
|
1059
1065
|
},
|
|
1060
1066
|
recap: {
|
|
@@ -1236,7 +1242,7 @@ async function maybeRenewActiveSession(
|
|
|
1236
1242
|
};
|
|
1237
1243
|
}
|
|
1238
1244
|
|
|
1239
|
-
async function
|
|
1245
|
+
async function hydrateRemoteSessionEvents(
|
|
1240
1246
|
daemonState,
|
|
1241
1247
|
summary,
|
|
1242
1248
|
nowIso = new Date().toISOString()
|
|
@@ -1248,37 +1254,44 @@ async function pollAndRelayHumanMessages(
|
|
|
1248
1254
|
|
|
1249
1255
|
daemonState.humanMessagePollInFlight = true;
|
|
1250
1256
|
try {
|
|
1251
|
-
const
|
|
1257
|
+
const hydrated = await daemonState.remoteHydrator({
|
|
1258
|
+
sessionId: daemonState.sessionId,
|
|
1252
1259
|
targetPath: daemonState.targetPath,
|
|
1253
|
-
since: daemonState.humanMessageCursor,
|
|
1254
1260
|
});
|
|
1255
|
-
if (!
|
|
1256
|
-
summary.humanMessages.reason = normalizeString(
|
|
1261
|
+
if (!hydrated.ok) {
|
|
1262
|
+
summary.humanMessages.reason = normalizeString(hydrated.reason) || "poll_failed";
|
|
1257
1263
|
summary.humanMessages.cursor = daemonState.humanMessageCursor;
|
|
1264
|
+
summary.humanMessages.sessionEventsCursor = daemonState.sessionEventsCursor;
|
|
1258
1265
|
return;
|
|
1259
1266
|
}
|
|
1260
1267
|
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
daemonState.humanMessageCursor = normalizeString(polled.cursor) || daemonState.humanMessageCursor;
|
|
1268
|
+
const relayed = Math.max(0, Math.floor(Number(hydrated.relayed || 0)));
|
|
1269
|
+
const humanRelayed = Math.max(0, Math.floor(Number(hydrated.humanRelayed || 0)));
|
|
1270
|
+
const sessionEventsRelayed = Math.max(0, Math.floor(Number(hydrated.eventsRelayed || 0)));
|
|
1271
|
+
daemonState.humanMessageCursor = normalizeString(hydrated.cursor) || daemonState.humanMessageCursor;
|
|
1272
|
+
daemonState.sessionEventsCursor =
|
|
1273
|
+
normalizeString(hydrated.eventsCursor) || daemonState.sessionEventsCursor;
|
|
1269
1274
|
|
|
1270
|
-
summary.humanMessages.relayed =
|
|
1271
|
-
summary.humanMessages.dropped =
|
|
1275
|
+
summary.humanMessages.relayed = relayed;
|
|
1276
|
+
summary.humanMessages.dropped = Math.max(0, Math.floor(Number(hydrated.dropped || 0)));
|
|
1272
1277
|
summary.humanMessages.cursor = daemonState.humanMessageCursor;
|
|
1278
|
+
summary.humanMessages.sessionEventsRelayed = sessionEventsRelayed;
|
|
1279
|
+
summary.humanMessages.sessionEventsCursor = daemonState.sessionEventsCursor;
|
|
1280
|
+
summary.humanMessages.humanEventsRelayed = humanRelayed;
|
|
1281
|
+
summary.humanMessages.eventsBackfillComplete = Boolean(hydrated.eventsBackfillComplete);
|
|
1282
|
+
summary.humanMessages.eventsPageCount = Math.max(0, Math.floor(Number(hydrated.eventsPageCount || 0)));
|
|
1283
|
+
summary.humanMessages.localAppendComplete = hydrated.localAppendComplete !== false;
|
|
1273
1284
|
summary.humanMessages.reason = "";
|
|
1274
1285
|
|
|
1275
|
-
if (
|
|
1286
|
+
if (humanRelayed > 0) {
|
|
1276
1287
|
await emitSentiEvent(
|
|
1277
1288
|
daemonState.sessionId,
|
|
1278
1289
|
"daemon_alert",
|
|
1279
1290
|
{
|
|
1280
1291
|
alert: "human_directive_received",
|
|
1281
|
-
relayedCount:
|
|
1292
|
+
relayedCount: relayed,
|
|
1293
|
+
humanEventsRelayed: humanRelayed,
|
|
1294
|
+
sessionEventsRelayed,
|
|
1282
1295
|
droppedCount: summary.humanMessages.dropped,
|
|
1283
1296
|
},
|
|
1284
1297
|
{
|
|
@@ -1291,6 +1304,7 @@ async function pollAndRelayHumanMessages(
|
|
|
1291
1304
|
summary.humanMessages.reason =
|
|
1292
1305
|
normalizeString(error?.message) || "poll_relay_failed";
|
|
1293
1306
|
summary.humanMessages.cursor = daemonState.humanMessageCursor;
|
|
1307
|
+
summary.humanMessages.sessionEventsCursor = daemonState.sessionEventsCursor;
|
|
1294
1308
|
} finally {
|
|
1295
1309
|
daemonState.humanMessagePollInFlight = false;
|
|
1296
1310
|
}
|
|
@@ -1466,6 +1480,7 @@ export async function runSentiHealthTick(
|
|
|
1466
1480
|
checkpointCloseoutOnStop: true,
|
|
1467
1481
|
helpResponder: null,
|
|
1468
1482
|
llmInvoker: invokeViaProxy,
|
|
1483
|
+
remoteHydrator: hydrateSessionFromRemote,
|
|
1469
1484
|
telemetrySessionId: null,
|
|
1470
1485
|
});
|
|
1471
1486
|
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
@@ -1486,7 +1501,7 @@ export async function runSentiHealthTick(
|
|
|
1486
1501
|
await emitStaleAndRecoveryAlerts(resolvedDaemonState, summary, staleAgents, normalizedNow);
|
|
1487
1502
|
await emitConflictAlerts(resolvedDaemonState, summary, filteredAgents, normalizedNow);
|
|
1488
1503
|
await maybeRenewActiveSession(resolvedDaemonState, summary, session, normalizedNow);
|
|
1489
|
-
await
|
|
1504
|
+
await hydrateRemoteSessionEvents(resolvedDaemonState, summary, normalizedNow);
|
|
1490
1505
|
await maybeEmitPeriodicRecap(resolvedDaemonState, summary, normalizedNow);
|
|
1491
1506
|
await maybeGenerateSessionCheckpoint(resolvedDaemonState, summary, normalizedNow);
|
|
1492
1507
|
return summary;
|
|
@@ -1513,6 +1528,7 @@ export async function startSenti(
|
|
|
1513
1528
|
checkpointCloseoutOnStop = true,
|
|
1514
1529
|
helpResponder = null,
|
|
1515
1530
|
llmInvoker = invokeViaProxy,
|
|
1531
|
+
remoteHydrator = hydrateSessionFromRemote,
|
|
1516
1532
|
} = {}
|
|
1517
1533
|
) {
|
|
1518
1534
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -1597,6 +1613,7 @@ export async function startSenti(
|
|
|
1597
1613
|
checkpointCloseoutOnStop: checkpointCloseoutOnStop !== false,
|
|
1598
1614
|
helpResponder,
|
|
1599
1615
|
llmInvoker: typeof llmInvoker === "function" ? llmInvoker : invokeViaProxy,
|
|
1616
|
+
remoteHydrator: typeof remoteHydrator === "function" ? remoteHydrator : hydrateSessionFromRemote,
|
|
1600
1617
|
telemetrySessionId: telemetrySession?.id || null,
|
|
1601
1618
|
});
|
|
1602
1619
|
|
|
@@ -1792,6 +1809,7 @@ export async function startSenti(
|
|
|
1792
1809
|
lastCheckpointSourceSequenceId: daemonState.lastCheckpointSourceSequenceId,
|
|
1793
1810
|
lastCheckpointResult: daemonState.lastCheckpointResult,
|
|
1794
1811
|
humanMessageCursor: daemonState.humanMessageCursor,
|
|
1812
|
+
sessionEventsCursor: daemonState.sessionEventsCursor,
|
|
1795
1813
|
}),
|
|
1796
1814
|
};
|
|
1797
1815
|
|
package/src/session/recap.js
CHANGED
|
@@ -17,6 +17,9 @@ const DEFAULT_RECAP_INTERVAL_MS = 300_000;
|
|
|
17
17
|
const DEFAULT_RECAP_INACTIVITY_MS = 600_000;
|
|
18
18
|
const DEFAULT_RECAP_ACTIVITY_THRESHOLD = 5;
|
|
19
19
|
const DEFAULT_TASK_SUMMARY_LIMIT = 3;
|
|
20
|
+
const DEFAULT_WORK_PLAN_SUMMARY_LIMIT = 5;
|
|
21
|
+
const MAX_WORK_PLAN_BYTES = 128_000;
|
|
22
|
+
const WORK_PLAN_RELATIVE_PATH = "tasks/todo.md";
|
|
20
23
|
const RECAP_SOURCE_IGNORED_EVENTS = new Set([
|
|
21
24
|
"agent_heartbeat",
|
|
22
25
|
"agent_join",
|
|
@@ -399,6 +402,105 @@ function emptyTaskLedgerSummary() {
|
|
|
399
402
|
};
|
|
400
403
|
}
|
|
401
404
|
|
|
405
|
+
function emptyWorkPlanSummary() {
|
|
406
|
+
return {
|
|
407
|
+
path: WORK_PLAN_RELATIVE_PATH,
|
|
408
|
+
exists: false,
|
|
409
|
+
truncated: false,
|
|
410
|
+
total: 0,
|
|
411
|
+
open: 0,
|
|
412
|
+
completed: 0,
|
|
413
|
+
currentSection: "",
|
|
414
|
+
recentOpen: [],
|
|
415
|
+
recent: [],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function shortWorkPlanText(value) {
|
|
420
|
+
const text = normalizeString(value)
|
|
421
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
422
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
423
|
+
.replace(/\s+/g, " ");
|
|
424
|
+
if (text.length <= 100) {
|
|
425
|
+
return text;
|
|
426
|
+
}
|
|
427
|
+
return `${text.slice(0, 97)}...`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function summarizeWorkPlanMarkdown(raw = "", { limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT, truncated = false } = {}) {
|
|
431
|
+
const summary = emptyWorkPlanSummary();
|
|
432
|
+
summary.exists = true;
|
|
433
|
+
summary.truncated = Boolean(truncated);
|
|
434
|
+
|
|
435
|
+
const records = [];
|
|
436
|
+
let section = "";
|
|
437
|
+
for (const line of String(raw || "").split(/\r?\n/)) {
|
|
438
|
+
const headingMatch = /^(#{1,4})\s+(.+?)\s*$/.exec(line);
|
|
439
|
+
if (headingMatch) {
|
|
440
|
+
section = shortWorkPlanText(headingMatch[2]);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const taskMatch = /^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/.exec(line);
|
|
445
|
+
if (!taskMatch) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const completed = taskMatch[1].toLowerCase() === "x";
|
|
449
|
+
const record = {
|
|
450
|
+
status: completed ? "completed" : "open",
|
|
451
|
+
section,
|
|
452
|
+
task: shortWorkPlanText(taskMatch[2]),
|
|
453
|
+
};
|
|
454
|
+
if (!record.task) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
records.push(record);
|
|
458
|
+
summary.total += 1;
|
|
459
|
+
if (completed) {
|
|
460
|
+
summary.completed += 1;
|
|
461
|
+
} else {
|
|
462
|
+
summary.open += 1;
|
|
463
|
+
}
|
|
464
|
+
summary.currentSection = section || summary.currentSection;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const normalizedLimit = Math.max(1, normalizePositiveInteger(limit, DEFAULT_WORK_PLAN_SUMMARY_LIMIT));
|
|
468
|
+
summary.recentOpen = records
|
|
469
|
+
.filter((record) => record.status === "open")
|
|
470
|
+
.slice(-normalizedLimit);
|
|
471
|
+
summary.recent = records.slice(-normalizedLimit);
|
|
472
|
+
return summary;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function readWorkPlanSummary({ targetPath = process.cwd(), limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT } = {}) {
|
|
476
|
+
const filePath = path.join(path.resolve(String(targetPath || ".")), WORK_PLAN_RELATIVE_PATH);
|
|
477
|
+
try {
|
|
478
|
+
const stats = await fsp.stat(filePath);
|
|
479
|
+
let source = "";
|
|
480
|
+
let truncated = false;
|
|
481
|
+
if (stats.size > MAX_WORK_PLAN_BYTES) {
|
|
482
|
+
truncated = true;
|
|
483
|
+
const handle = await fsp.open(filePath, "r");
|
|
484
|
+
try {
|
|
485
|
+
const buffer = Buffer.alloc(MAX_WORK_PLAN_BYTES);
|
|
486
|
+
const position = Math.max(0, stats.size - MAX_WORK_PLAN_BYTES);
|
|
487
|
+
const { bytesRead } = await handle.read(buffer, 0, MAX_WORK_PLAN_BYTES, position);
|
|
488
|
+
source = buffer.subarray(0, bytesRead).toString("utf-8");
|
|
489
|
+
} finally {
|
|
490
|
+
await handle.close();
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
source = await fsp.readFile(filePath, "utf-8");
|
|
494
|
+
}
|
|
495
|
+
return summarizeWorkPlanMarkdown(source, { limit, truncated });
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
498
|
+
return emptyWorkPlanSummary();
|
|
499
|
+
}
|
|
500
|
+
return emptyWorkPlanSummary();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
402
504
|
function summarizeTaskLedger(tasks = [], { limit = DEFAULT_TASK_SUMMARY_LIMIT } = {}) {
|
|
403
505
|
const summary = emptyTaskLedgerSummary();
|
|
404
506
|
const owners = new Map();
|
|
@@ -541,6 +643,7 @@ function buildRecapText({
|
|
|
541
643
|
activeLocks = 0,
|
|
542
644
|
pendingTasks = 0,
|
|
543
645
|
taskLedger = emptyTaskLedgerSummary(),
|
|
646
|
+
workPlan = emptyWorkPlanSummary(),
|
|
544
647
|
usageSummary = normalizeUsageSummary(),
|
|
545
648
|
snippets = [],
|
|
546
649
|
} = {}) {
|
|
@@ -554,8 +657,9 @@ function buildRecapText({
|
|
|
554
657
|
pendingTasks > 0 ? `You have ${pendingTasks} pending task${pendingTasks === 1 ? "" : "s"}.` : "";
|
|
555
658
|
const taskText = buildTaskLedgerText(taskLedger);
|
|
556
659
|
const usageText = buildUsageLedgerText(usageSummary);
|
|
660
|
+
const workPlanText = buildWorkPlanText(workPlan);
|
|
557
661
|
const snippetText = snippets.length > 0 ? `Recent: ${snippets.join(" | ")}` : "";
|
|
558
|
-
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${usageText} ${snippetText}`.replace(
|
|
662
|
+
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${workPlanText} ${usageText} ${snippetText}`.replace(
|
|
559
663
|
/\s+/g,
|
|
560
664
|
" "
|
|
561
665
|
).trim();
|
|
@@ -596,6 +700,31 @@ function buildTaskLedgerText(taskLedger = emptyTaskLedgerSummary()) {
|
|
|
596
700
|
.join(". ");
|
|
597
701
|
}
|
|
598
702
|
|
|
703
|
+
function buildWorkPlanText(workPlan = emptyWorkPlanSummary()) {
|
|
704
|
+
if (!workPlan || typeof workPlan !== "object" || !workPlan.exists) {
|
|
705
|
+
return "";
|
|
706
|
+
}
|
|
707
|
+
const open = Number(workPlan.open || 0);
|
|
708
|
+
const completed = Number(workPlan.completed || 0);
|
|
709
|
+
const pathText = normalizeString(workPlan.path) || WORK_PLAN_RELATIVE_PATH;
|
|
710
|
+
const currentSection = normalizeString(workPlan.currentSection);
|
|
711
|
+
const currentText = currentSection ? ` Current: ${currentSection}.` : "";
|
|
712
|
+
const recentOpen = Array.isArray(workPlan.recentOpen) ? workPlan.recentOpen : [];
|
|
713
|
+
const nextText =
|
|
714
|
+
recentOpen.length > 0
|
|
715
|
+
? ` Next: ${recentOpen
|
|
716
|
+
.map((item) => {
|
|
717
|
+
const section = normalizeString(item.section);
|
|
718
|
+
const task = normalizeString(item.task);
|
|
719
|
+
return section ? `${section} - ${task}` : task;
|
|
720
|
+
})
|
|
721
|
+
.filter(Boolean)
|
|
722
|
+
.join("; ")}.`
|
|
723
|
+
: "";
|
|
724
|
+
const truncatedText = workPlan.truncated ? " Recent window only." : "";
|
|
725
|
+
return `Plan: ${open} open / ${completed} done in ${pathText}.${currentText}${nextText}${truncatedText}`;
|
|
726
|
+
}
|
|
727
|
+
|
|
599
728
|
function roundCurrency(value) {
|
|
600
729
|
const normalized = Number(value || 0);
|
|
601
730
|
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
@@ -702,11 +831,12 @@ function buildPeriodicText(recap = {}) {
|
|
|
702
831
|
const lastActor = normalizeString(summary.lastActorId);
|
|
703
832
|
const actorText = lastActor ? `${lastActor} active` : "no active actor";
|
|
704
833
|
const taskText = buildTaskLedgerText(summary.taskLedger);
|
|
834
|
+
const workPlanText = buildWorkPlanText(summary.workPlan);
|
|
705
835
|
const usageText = buildUsageLedgerText({
|
|
706
836
|
totals: summary.usageTotals,
|
|
707
837
|
topAgents: summary.usageTopAgents,
|
|
708
838
|
});
|
|
709
|
-
return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${usageText} ${actorText}.`.replace(
|
|
839
|
+
return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${workPlanText} ${usageText} ${actorText}.`.replace(
|
|
710
840
|
/\s+/g,
|
|
711
841
|
" ",
|
|
712
842
|
).trim();
|
|
@@ -774,6 +904,9 @@ export async function buildSessionRecap(
|
|
|
774
904
|
const taskLedger = await readTaskLedgerSummary(normalizedSessionId, {
|
|
775
905
|
targetPath: normalizedTargetPath,
|
|
776
906
|
});
|
|
907
|
+
const workPlan = await readWorkPlanSummary({
|
|
908
|
+
targetPath: normalizedTargetPath,
|
|
909
|
+
});
|
|
777
910
|
const snippets = summarizeRecentActivity(visibleEvents, {
|
|
778
911
|
forAgentId: normalizedForAgentId,
|
|
779
912
|
limit: 2,
|
|
@@ -789,6 +922,7 @@ export async function buildSessionRecap(
|
|
|
789
922
|
activeLocks,
|
|
790
923
|
pendingTasks,
|
|
791
924
|
taskLedger,
|
|
925
|
+
workPlan,
|
|
792
926
|
usageSummary,
|
|
793
927
|
snippets,
|
|
794
928
|
});
|
|
@@ -809,6 +943,7 @@ export async function buildSessionRecap(
|
|
|
809
943
|
activeLocks,
|
|
810
944
|
pendingTasksForAgent: pendingTasks,
|
|
811
945
|
taskLedger,
|
|
946
|
+
workPlan,
|
|
812
947
|
usageTotals: usageSummary.totals,
|
|
813
948
|
usageTopAgents: usageSummary.topAgents,
|
|
814
949
|
snippets,
|
package/src/session/sync.js
CHANGED
|
@@ -243,6 +243,23 @@ function eventSequenceNumber(event = {}) {
|
|
|
243
243
|
return 0;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
function nextBeforeSequenceFromPayload(payload = {}, events = []) {
|
|
247
|
+
const explicit = Number(payload?.next_before_sequence ?? payload?.nextBeforeSequence);
|
|
248
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
249
|
+
return Math.floor(explicit);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let minimumSequence = 0;
|
|
253
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
254
|
+
const sequence = eventSequenceNumber(event);
|
|
255
|
+
if (sequence <= 0) continue;
|
|
256
|
+
if (minimumSequence === 0 || sequence < minimumSequence) {
|
|
257
|
+
minimumSequence = sequence;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return minimumSequence || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
246
263
|
function chronologicalSessionEvents(events = []) {
|
|
247
264
|
return (Array.isArray(events) ? events : [])
|
|
248
265
|
.map((event, index) => ({ event, index }))
|
|
@@ -1219,13 +1236,12 @@ export async function pollSessionEventsBefore(
|
|
|
1219
1236
|
|
|
1220
1237
|
const events = chronologicalSessionEvents(payload?.events || []);
|
|
1221
1238
|
const lastEvent = events[events.length - 1] || null;
|
|
1222
|
-
const firstEvent = events[0] || null;
|
|
1223
1239
|
return {
|
|
1224
1240
|
ok: true,
|
|
1225
1241
|
reason: "",
|
|
1226
1242
|
events,
|
|
1227
1243
|
cursor: normalizeString(lastEvent?.cursor) || null,
|
|
1228
|
-
beforeSequence:
|
|
1244
|
+
beforeSequence: nextBeforeSequenceFromPayload(payload, payload?.events || []),
|
|
1229
1245
|
};
|
|
1230
1246
|
} catch (error) {
|
|
1231
1247
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|