sentinelayer-cli 0.31.0 → 0.31.2
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 +13 -0
- package/src/session/remote-hydrate.js +27 -8
- package/src/session/stream.js +58 -1
package/package.json
CHANGED
package/src/session/recap.js
CHANGED
|
@@ -407,6 +407,8 @@ function emptyWorkPlanSummary() {
|
|
|
407
407
|
path: WORK_PLAN_RELATIVE_PATH,
|
|
408
408
|
exists: false,
|
|
409
409
|
truncated: false,
|
|
410
|
+
detailSuppressed: false,
|
|
411
|
+
detailSuppressionReason: "",
|
|
410
412
|
total: 0,
|
|
411
413
|
open: 0,
|
|
412
414
|
completed: 0,
|
|
@@ -431,6 +433,10 @@ function summarizeWorkPlanMarkdown(raw = "", { limit = DEFAULT_WORK_PLAN_SUMMARY
|
|
|
431
433
|
const summary = emptyWorkPlanSummary();
|
|
432
434
|
summary.exists = true;
|
|
433
435
|
summary.truncated = Boolean(truncated);
|
|
436
|
+
if (summary.truncated) {
|
|
437
|
+
summary.detailSuppressed = true;
|
|
438
|
+
summary.detailSuppressionReason = "large_plan_recent_window";
|
|
439
|
+
}
|
|
434
440
|
|
|
435
441
|
const records = [];
|
|
436
442
|
let section = "";
|
|
@@ -469,6 +475,10 @@ function summarizeWorkPlanMarkdown(raw = "", { limit = DEFAULT_WORK_PLAN_SUMMARY
|
|
|
469
475
|
.filter((record) => record.status === "open")
|
|
470
476
|
.slice(-normalizedLimit);
|
|
471
477
|
summary.recent = records.slice(-normalizedLimit);
|
|
478
|
+
if (summary.detailSuppressed) {
|
|
479
|
+
summary.currentSection = "";
|
|
480
|
+
summary.recentOpen = [];
|
|
481
|
+
}
|
|
472
482
|
return summary;
|
|
473
483
|
}
|
|
474
484
|
|
|
@@ -707,6 +717,9 @@ function buildWorkPlanText(workPlan = emptyWorkPlanSummary()) {
|
|
|
707
717
|
const open = Number(workPlan.open || 0);
|
|
708
718
|
const completed = Number(workPlan.completed || 0);
|
|
709
719
|
const pathText = normalizeString(workPlan.path) || WORK_PLAN_RELATIVE_PATH;
|
|
720
|
+
if (workPlan.detailSuppressed || workPlan.truncated) {
|
|
721
|
+
return `Plan: ${open} open / ${completed} done in recent ${pathText} window. Current/next items suppressed because the plan file is large.`;
|
|
722
|
+
}
|
|
710
723
|
const currentSection = normalizeString(workPlan.currentSection);
|
|
711
724
|
const currentText = currentSection ? ` Current: ${currentSection}.` : "";
|
|
712
725
|
const recentOpen = Array.isArray(workPlan.recentOpen) ? workPlan.recentOpen : [];
|
|
@@ -67,11 +67,18 @@ async function readExistingRelayState(sessionId, { targetPath = process.cwd() }
|
|
|
67
67
|
const knownKeys = new Set();
|
|
68
68
|
const events = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
69
69
|
const indexByKey = new Map();
|
|
70
|
+
const duplicateKeys = new Set();
|
|
70
71
|
for (const [index, event] of events.entries()) {
|
|
72
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
73
|
+
const existingIndex = indexByKey.get(key);
|
|
74
|
+
if (Number.isInteger(existingIndex) && existingIndex >= 0 && existingIndex !== index) {
|
|
75
|
+
duplicateKeys.add(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
71
78
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
72
79
|
indexRelayEventIdentityKeys(indexByKey, events, event, index);
|
|
73
80
|
}
|
|
74
|
-
return { knownKeys, events, indexByKey };
|
|
81
|
+
return { knownKeys, events, indexByKey, duplicateKeys };
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
async function ensureLocalSessionShell(sessionId, { targetPath = process.cwd() } = {}) {
|
|
@@ -380,13 +387,19 @@ export async function hydrateSessionFromRemote({
|
|
|
380
387
|
const successfulRelayKeys = relayState.knownKeys;
|
|
381
388
|
const existingRelayEvents = relayState.events;
|
|
382
389
|
const existingRelayIndexByKey = relayState.indexByKey;
|
|
390
|
+
const duplicateRelayKeys = relayState.duplicateKeys || new Set();
|
|
383
391
|
const newEvents = [];
|
|
392
|
+
const eventsNeedingMerge = new Set();
|
|
384
393
|
for (const event of merged) {
|
|
385
394
|
const existingIndex = findRelayEventIdentityIndex(existingRelayIndexByKey, event);
|
|
386
395
|
if (existingIndex >= 0) {
|
|
387
396
|
const existing = existingRelayEvents[existingIndex];
|
|
388
|
-
if (
|
|
397
|
+
if (
|
|
398
|
+
sessionEventUpgradesExisting(existing, event) ||
|
|
399
|
+
sessionEventHasKnownIdentity(event, duplicateRelayKeys)
|
|
400
|
+
) {
|
|
389
401
|
newEvents.push(event);
|
|
402
|
+
eventsNeedingMerge.add(event);
|
|
390
403
|
}
|
|
391
404
|
continue;
|
|
392
405
|
}
|
|
@@ -416,14 +429,20 @@ export async function hydrateSessionFromRemote({
|
|
|
416
429
|
}
|
|
417
430
|
}
|
|
418
431
|
|
|
419
|
-
const appendEvents =
|
|
420
|
-
remoteStatus && !["active", "pending"].includes(remoteStatus)
|
|
421
|
-
?
|
|
422
|
-
:
|
|
432
|
+
const appendEvents = materialNewEvents.map((event) => ({
|
|
433
|
+
event: remoteStatus && !["active", "pending"].includes(remoteStatus)
|
|
434
|
+
? markPostKillEvent(event)
|
|
435
|
+
: event,
|
|
436
|
+
mergeExisting: eventsNeedingMerge.has(event),
|
|
437
|
+
}));
|
|
423
438
|
const failedRelayKeys = new Set();
|
|
424
|
-
for (const event of appendEvents) {
|
|
439
|
+
for (const { event, mergeExisting } of appendEvents) {
|
|
425
440
|
try {
|
|
426
|
-
const persisted = await _append(sessionId, event, {
|
|
441
|
+
const persisted = await _append(sessionId, event, {
|
|
442
|
+
targetPath,
|
|
443
|
+
syncRemote: false,
|
|
444
|
+
mergeExisting,
|
|
445
|
+
});
|
|
427
446
|
relayed += 1;
|
|
428
447
|
const persistedEvent = persisted && typeof persisted === "object" ? persisted : event;
|
|
429
448
|
existingRelayEvents.push(persistedEvent);
|
package/src/session/stream.js
CHANGED
|
@@ -3,6 +3,10 @@ import process from "node:process";
|
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
4
|
|
|
5
5
|
import { createAgentEvent, normalizeAgentEvent } from "../events/schema.js";
|
|
6
|
+
import {
|
|
7
|
+
dedupeSessionEvents,
|
|
8
|
+
sessionEventIdentityKeys,
|
|
9
|
+
} from "./event-identity.js";
|
|
6
10
|
import { enrichEventWithMentions } from "./mentions.js";
|
|
7
11
|
import { resolveSessionPaths } from "./paths.js";
|
|
8
12
|
import { redactEventPayload } from "./redact.js";
|
|
@@ -348,6 +352,58 @@ async function rotateStreamIfNeeded(paths, maxEvents) {
|
|
|
348
352
|
await fsp.writeFile(paths.streamPath, retainedPayload, "utf-8");
|
|
349
353
|
}
|
|
350
354
|
|
|
355
|
+
function eventsShareIdentity(left = {}, right = {}) {
|
|
356
|
+
const leftKeys = new Set(sessionEventIdentityKeys(left));
|
|
357
|
+
if (leftKeys.size === 0) return false;
|
|
358
|
+
return sessionEventIdentityKeys(right).some((key) => leftKeys.has(key));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function appendOrMergeCurrentStreamEvent(paths, canonicalEvent, { mergeExisting = false } = {}) {
|
|
362
|
+
if (!mergeExisting) {
|
|
363
|
+
await fsp.appendFile(paths.streamPath, `${JSON.stringify(canonicalEvent)}\n`, "utf-8");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const currentEvents = await readEventsFromFile(paths.streamPath);
|
|
368
|
+
const matchingIndexes = [];
|
|
369
|
+
for (const [index, event] of currentEvents.entries()) {
|
|
370
|
+
if (eventsShareIdentity(event, canonicalEvent)) {
|
|
371
|
+
matchingIndexes.push(index);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (matchingIndexes.length === 0) {
|
|
376
|
+
await fsp.appendFile(paths.streamPath, `${JSON.stringify(canonicalEvent)}\n`, "utf-8");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const replacement = dedupeSessionEvents([
|
|
381
|
+
...matchingIndexes.map((index) => currentEvents[index]),
|
|
382
|
+
canonicalEvent,
|
|
383
|
+
])[0] || canonicalEvent;
|
|
384
|
+
const firstMatch = matchingIndexes[0];
|
|
385
|
+
const matchingIndexSet = new Set(matchingIndexes);
|
|
386
|
+
const mergedEvents = [];
|
|
387
|
+
for (const [index, event] of currentEvents.entries()) {
|
|
388
|
+
if (index === firstMatch) {
|
|
389
|
+
mergedEvents.push(replacement);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (matchingIndexSet.has(index)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
mergedEvents.push(event);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await fsp.writeFile(
|
|
399
|
+
paths.streamPath,
|
|
400
|
+
mergedEvents.length > 0
|
|
401
|
+
? `${mergedEvents.map((event) => JSON.stringify(event)).join("\n")}\n`
|
|
402
|
+
: "",
|
|
403
|
+
"utf-8",
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
351
407
|
function filterBySince(events = [], since) {
|
|
352
408
|
const normalizedSince = normalizeString(since);
|
|
353
409
|
if (!normalizedSince) {
|
|
@@ -371,6 +427,7 @@ export async function appendToStream(
|
|
|
371
427
|
maxEvents = DEFAULT_MAX_STREAM_EVENTS,
|
|
372
428
|
syncRemote = true,
|
|
373
429
|
awaitRemoteSync = false,
|
|
430
|
+
mergeExisting = false,
|
|
374
431
|
} = {}
|
|
375
432
|
) {
|
|
376
433
|
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
@@ -403,7 +460,7 @@ export async function appendToStream(
|
|
|
403
460
|
await acquireLock(paths.lockPath);
|
|
404
461
|
try {
|
|
405
462
|
await fsp.mkdir(paths.sessionDir, { recursive: true });
|
|
406
|
-
await
|
|
463
|
+
await appendOrMergeCurrentStreamEvent(paths, canonicalEvent, { mergeExisting });
|
|
407
464
|
await rotateStreamIfNeeded(paths, normalizedMaxEvents);
|
|
408
465
|
|
|
409
466
|
metadata.lastInteractionAt = nowIso;
|