sentinelayer-cli 0.18.2 → 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/recap.js +137 -2
- package/src/session/sync.js +18 -2
package/package.json
CHANGED
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);
|