sentinelayer-cli 0.31.0 → 0.31.1
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/remote-hydrate.js +27 -8
- package/src/session/stream.js +58 -1
package/package.json
CHANGED
|
@@ -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;
|