sentinelayer-cli 0.30.3 → 0.30.4
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
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -76,6 +76,8 @@ import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
|
76
76
|
import {
|
|
77
77
|
addSessionEventIdentityKeys,
|
|
78
78
|
dedupeSessionEvents,
|
|
79
|
+
sessionEventIdentityKeys,
|
|
80
|
+
sessionEventUpgradesExisting,
|
|
79
81
|
sessionEventHasKnownIdentity,
|
|
80
82
|
} from "../session/event-identity.js";
|
|
81
83
|
import { readSessionPreview } from "../session/preview.js";
|
|
@@ -2288,6 +2290,7 @@ async function hydrateJoinBriefingContext(sessionId, { targetPath, limit = 100 }
|
|
|
2288
2290
|
reason: "",
|
|
2289
2291
|
remoteEvents: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
2290
2292
|
appended: appended.appended,
|
|
2293
|
+
upgraded: appended.upgraded,
|
|
2291
2294
|
skipped: appended.skipped,
|
|
2292
2295
|
failed: appended.failed,
|
|
2293
2296
|
cursor: remoteTail.cursor || null,
|
|
@@ -2305,6 +2308,34 @@ async function hydrateJoinBriefingContext(sessionId, { targetPath, limit = 100 }
|
|
|
2305
2308
|
}
|
|
2306
2309
|
}
|
|
2307
2310
|
|
|
2311
|
+
function remoteSessionEventUpgradesLocal(localEvent = {}, remoteEvent = {}) {
|
|
2312
|
+
return sessionEventUpgradesExisting(localEvent, remoteEvent);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
function indexSessionEventIdentityKeys(indexByKey, eventList = [], event = {}, index = -1) {
|
|
2316
|
+
if (!indexByKey || !Array.isArray(eventList) || index < 0) return;
|
|
2317
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
2318
|
+
const existingIndex = indexByKey.get(key);
|
|
2319
|
+
const existingEvent = Number.isInteger(existingIndex) && existingIndex >= 0
|
|
2320
|
+
? eventList[existingIndex]
|
|
2321
|
+
: null;
|
|
2322
|
+
if (!existingEvent || remoteSessionEventUpgradesLocal(existingEvent, event)) {
|
|
2323
|
+
indexByKey.set(key, index);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function findSessionEventIdentityIndex(indexByKey, event = {}) {
|
|
2329
|
+
if (!indexByKey) return -1;
|
|
2330
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
2331
|
+
const index = indexByKey.get(key);
|
|
2332
|
+
if (Number.isInteger(index) && index >= 0) {
|
|
2333
|
+
return index;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return -1;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2308
2339
|
async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetPath } = {}) {
|
|
2309
2340
|
const events = Array.isArray(remoteEvents) ? remoteEvents : [];
|
|
2310
2341
|
if (events.length === 0) {
|
|
@@ -2315,18 +2346,47 @@ async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetP
|
|
|
2315
2346
|
};
|
|
2316
2347
|
}
|
|
2317
2348
|
const knownKeys = new Set();
|
|
2349
|
+
const localIndexByKey = new Map();
|
|
2318
2350
|
const localEvents = await readStream(sessionId, {
|
|
2319
2351
|
targetPath,
|
|
2320
2352
|
tail: 0,
|
|
2321
2353
|
});
|
|
2322
|
-
for (const event of localEvents) {
|
|
2354
|
+
for (const [index, event] of localEvents.entries()) {
|
|
2323
2355
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
2356
|
+
indexSessionEventIdentityKeys(localIndexByKey, localEvents, event, index);
|
|
2324
2357
|
}
|
|
2325
2358
|
|
|
2326
2359
|
let appended = 0;
|
|
2360
|
+
let upgraded = 0;
|
|
2327
2361
|
let skipped = 0;
|
|
2328
2362
|
let failed = 0;
|
|
2329
2363
|
for (const event of events) {
|
|
2364
|
+
const existingIndex = findSessionEventIdentityIndex(localIndexByKey, event);
|
|
2365
|
+
if (existingIndex >= 0) {
|
|
2366
|
+
const existing = localEvents[existingIndex];
|
|
2367
|
+
if (!remoteSessionEventUpgradesLocal(existing, event)) {
|
|
2368
|
+
skipped += 1;
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
// Keep the richer canonical API row in local history when it
|
|
2372
|
+
// upgrades an optimistic/local post that had no durable cursor yet.
|
|
2373
|
+
// Future reads can then page by sequence instead of rediscovering the
|
|
2374
|
+
// upgrade every time.
|
|
2375
|
+
try {
|
|
2376
|
+
const persisted = await appendToStream(sessionId, event, {
|
|
2377
|
+
targetPath,
|
|
2378
|
+
syncRemote: false,
|
|
2379
|
+
});
|
|
2380
|
+
localEvents.push(persisted);
|
|
2381
|
+
addSessionEventIdentityKeys(knownKeys, persisted);
|
|
2382
|
+
indexSessionEventIdentityKeys(localIndexByKey, localEvents, persisted, localEvents.length - 1);
|
|
2383
|
+
upgraded += 1;
|
|
2384
|
+
} catch {
|
|
2385
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
2386
|
+
failed += 1;
|
|
2387
|
+
}
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2330
2390
|
if (sessionEventHasKnownIdentity(event, knownKeys)) {
|
|
2331
2391
|
skipped += 1;
|
|
2332
2392
|
continue;
|
|
@@ -2345,6 +2405,7 @@ async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetP
|
|
|
2345
2405
|
}
|
|
2346
2406
|
return {
|
|
2347
2407
|
appended,
|
|
2408
|
+
upgraded,
|
|
2348
2409
|
skipped,
|
|
2349
2410
|
failed,
|
|
2350
2411
|
};
|
|
@@ -4954,12 +5015,26 @@ export function registerSessionCommand(program) {
|
|
|
4954
5015
|
const displayEvents = [...allEvents];
|
|
4955
5016
|
let remoteTailAppended = 0;
|
|
4956
5017
|
let remoteTailDisplayedOnly = 0;
|
|
5018
|
+
let remoteTailUpgraded = 0;
|
|
4957
5019
|
if (remoteTail?.ok && Array.isArray(remoteTail.events) && remoteTail.events.length > 0) {
|
|
4958
5020
|
const knownKeys = new Set();
|
|
4959
|
-
|
|
5021
|
+
const displayIndexByKey = new Map();
|
|
5022
|
+
for (const [index, event] of displayEvents.entries()) {
|
|
4960
5023
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
5024
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, index);
|
|
4961
5025
|
}
|
|
4962
5026
|
for (const event of remoteTail.events) {
|
|
5027
|
+
const existingIndex = findSessionEventIdentityIndex(displayIndexByKey, event);
|
|
5028
|
+
if (existingIndex >= 0) {
|
|
5029
|
+
const existing = displayEvents[existingIndex];
|
|
5030
|
+
if (remoteSessionEventUpgradesLocal(existing, event)) {
|
|
5031
|
+
displayEvents[existingIndex] = event;
|
|
5032
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
5033
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, existingIndex);
|
|
5034
|
+
remoteTailUpgraded += 1;
|
|
5035
|
+
}
|
|
5036
|
+
continue;
|
|
5037
|
+
}
|
|
4963
5038
|
if (sessionEventHasKnownIdentity(event, knownKeys)) {
|
|
4964
5039
|
continue;
|
|
4965
5040
|
}
|
|
@@ -4970,10 +5045,12 @@ export function registerSessionCommand(program) {
|
|
|
4970
5045
|
});
|
|
4971
5046
|
displayEvents.push(appended);
|
|
4972
5047
|
addSessionEventIdentityKeys(knownKeys, appended);
|
|
5048
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, appended, displayEvents.length - 1);
|
|
4973
5049
|
remoteTailAppended += 1;
|
|
4974
5050
|
} catch {
|
|
4975
5051
|
displayEvents.push(event);
|
|
4976
5052
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
5053
|
+
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, displayEvents.length - 1);
|
|
4977
5054
|
remoteTailDisplayedOnly += 1;
|
|
4978
5055
|
}
|
|
4979
5056
|
}
|
|
@@ -4987,10 +5064,11 @@ export function registerSessionCommand(program) {
|
|
|
4987
5064
|
const recentHumanActivity = remoteActions?.ok
|
|
4988
5065
|
? readProjectionRecentHumanActivity(remoteActions)
|
|
4989
5066
|
: [];
|
|
5067
|
+
const dedupedDisplayEvents = dedupeSessionEvents(displayEvents);
|
|
4990
5068
|
const transcriptEvents = includeControlEvents
|
|
4991
|
-
?
|
|
4992
|
-
:
|
|
4993
|
-
const hiddenControlEventCount =
|
|
5069
|
+
? dedupedDisplayEvents
|
|
5070
|
+
: dedupedDisplayEvents.filter((event) => !isSessionControlEvent(event));
|
|
5071
|
+
const hiddenControlEventCount = dedupedDisplayEvents.length - transcriptEvents.length;
|
|
4994
5072
|
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
4995
5073
|
const autoView = recordSessionReadViews(normalizedSessionId, events, {
|
|
4996
5074
|
targetPath,
|
|
@@ -5040,6 +5118,7 @@ export function registerSessionCommand(program) {
|
|
|
5040
5118
|
verified: Boolean(remoteTail.ok),
|
|
5041
5119
|
appended: remoteTailAppended,
|
|
5042
5120
|
displayedOnly: remoteTailDisplayedOnly,
|
|
5121
|
+
upgraded: remoteTailUpgraded,
|
|
5043
5122
|
controlEventCount: remoteTailStats?.controlEventCount || 0,
|
|
5044
5123
|
materialEventCount: remoteTailStats?.materialEventCount || 0,
|
|
5045
5124
|
latestEvent: remoteTailStats?.latestEvent || null,
|
|
@@ -15,6 +15,24 @@ function timestampKey(...values) {
|
|
|
15
15
|
return "";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function readSessionEventSequence(event = {}) {
|
|
19
|
+
const value = Number(event?.sequenceId ?? event?.sequence_id ?? event?.sequence);
|
|
20
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sessionEventHasDurableCursor(event = {}) {
|
|
24
|
+
return Boolean(keyString(event?.cursor));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessionEventUpgradesExisting(existingEvent = {}, candidateEvent = {}) {
|
|
28
|
+
const existingSequence = readSessionEventSequence(existingEvent);
|
|
29
|
+
const candidateSequence = readSessionEventSequence(candidateEvent);
|
|
30
|
+
if (candidateSequence > 0 && existingSequence <= 0) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return sessionEventHasDurableCursor(candidateEvent) && !sessionEventHasDurableCursor(existingEvent);
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
function stableJsonValue(value) {
|
|
19
37
|
if (Array.isArray(value)) {
|
|
20
38
|
return value.map((item) => stableJsonValue(item));
|
|
@@ -51,6 +69,10 @@ export function sessionEventIdentityKeys(event = {}) {
|
|
|
51
69
|
keys.push(`idempotency:${event.idempotencyToken.trim()}`);
|
|
52
70
|
}
|
|
53
71
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
72
|
+
const clientMessageId = typeof payload.clientMessageId === "string" ? payload.clientMessageId.trim() : "";
|
|
73
|
+
if (clientMessageId) {
|
|
74
|
+
keys.push(`client-message:${clientMessageId}`);
|
|
75
|
+
}
|
|
54
76
|
const messageId = typeof payload.messageId === "string" ? payload.messageId.trim() : "";
|
|
55
77
|
if (messageId) {
|
|
56
78
|
keys.push(`message:${messageId}`);
|
|
@@ -29,20 +29,48 @@ import {
|
|
|
29
29
|
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
30
30
|
import {
|
|
31
31
|
addSessionEventIdentityKeys,
|
|
32
|
+
sessionEventIdentityKeys,
|
|
32
33
|
sessionEventHasKnownIdentity,
|
|
34
|
+
sessionEventUpgradesExisting,
|
|
33
35
|
} from "./event-identity.js";
|
|
34
36
|
|
|
35
37
|
const EVENTS_CURSOR_SUFFIX = "events";
|
|
36
38
|
const DEFAULT_EVENT_PAGE_LIMIT = 200;
|
|
37
39
|
const DEFAULT_MAX_EVENT_PAGES = 25;
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
function indexRelayEventIdentityKeys(indexByKey, eventList = [], event = {}, index = -1) {
|
|
42
|
+
if (!indexByKey || !Array.isArray(eventList) || index < 0) return;
|
|
43
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
44
|
+
const existingIndex = indexByKey.get(key);
|
|
45
|
+
const existingEvent = Number.isInteger(existingIndex) && existingIndex >= 0
|
|
46
|
+
? eventList[existingIndex]
|
|
47
|
+
: null;
|
|
48
|
+
if (!existingEvent || sessionEventUpgradesExisting(existingEvent, event)) {
|
|
49
|
+
indexByKey.set(key, index);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findRelayEventIdentityIndex(indexByKey, event = {}) {
|
|
55
|
+
if (!indexByKey) return -1;
|
|
56
|
+
for (const key of sessionEventIdentityKeys(event)) {
|
|
57
|
+
const index = indexByKey.get(key);
|
|
58
|
+
if (Number.isInteger(index) && index >= 0) {
|
|
59
|
+
return index;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readExistingRelayState(sessionId, { targetPath = process.cwd() } = {}) {
|
|
40
66
|
const knownKeys = new Set();
|
|
41
67
|
const events = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
42
|
-
|
|
68
|
+
const indexByKey = new Map();
|
|
69
|
+
for (const [index, event] of events.entries()) {
|
|
43
70
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
71
|
+
indexRelayEventIdentityKeys(indexByKey, events, event, index);
|
|
44
72
|
}
|
|
45
|
-
return knownKeys;
|
|
73
|
+
return { knownKeys, events, indexByKey };
|
|
46
74
|
}
|
|
47
75
|
|
|
48
76
|
async function ensureLocalSessionShell(sessionId, { targetPath = process.cwd() } = {}) {
|
|
@@ -92,10 +120,13 @@ async function ensureLocalSessionShell(sessionId, { targetPath = process.cwd() }
|
|
|
92
120
|
return { materialized: true, refreshed: false, session: created, remoteStatus };
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
function sourceFullyRelayed(events = [], successfulKeys = new Set()) {
|
|
123
|
+
function sourceFullyRelayed(events = [], successfulKeys = new Set(), failedKeys = new Set()) {
|
|
96
124
|
const relayedEvents = Array.isArray(events) ? events : [];
|
|
97
125
|
if (relayedEvents.length === 0) return true;
|
|
98
|
-
return relayedEvents.every((event) =>
|
|
126
|
+
return relayedEvents.every((event) =>
|
|
127
|
+
!sessionEventHasKnownIdentity(event, failedKeys) &&
|
|
128
|
+
sessionEventHasKnownIdentity(event, successfulKeys)
|
|
129
|
+
);
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
function markPostKillEvent(event = {}) {
|
|
@@ -341,11 +372,26 @@ export async function hydrateSessionFromRemote({
|
|
|
341
372
|
let relayed = 0;
|
|
342
373
|
let materializedLocalSession = false;
|
|
343
374
|
let remoteStatus = "";
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
375
|
+
const relayState = merged.length > 0
|
|
376
|
+
? await readExistingRelayState(sessionId, { targetPath })
|
|
377
|
+
: { knownKeys: new Set(), events: [], indexByKey: new Map() };
|
|
378
|
+
const successfulRelayKeys = relayState.knownKeys;
|
|
379
|
+
const existingRelayEvents = relayState.events;
|
|
380
|
+
const existingRelayIndexByKey = relayState.indexByKey;
|
|
381
|
+
const newEvents = [];
|
|
382
|
+
for (const event of merged) {
|
|
383
|
+
const existingIndex = findRelayEventIdentityIndex(existingRelayIndexByKey, event);
|
|
384
|
+
if (existingIndex >= 0) {
|
|
385
|
+
const existing = existingRelayEvents[existingIndex];
|
|
386
|
+
if (sessionEventUpgradesExisting(existing, event)) {
|
|
387
|
+
newEvents.push(event);
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!sessionEventHasKnownIdentity(event, successfulRelayKeys)) {
|
|
392
|
+
newEvents.push(event);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
349
395
|
if (newEvents.length > 0) {
|
|
350
396
|
try {
|
|
351
397
|
const localSession = await _ensureLocalSession(sessionId, { targetPath });
|
|
@@ -361,20 +407,30 @@ export async function hydrateSessionFromRemote({
|
|
|
361
407
|
remoteStatus && !["active", "pending"].includes(remoteStatus)
|
|
362
408
|
? newEvents.map((event) => markPostKillEvent(event))
|
|
363
409
|
: newEvents;
|
|
410
|
+
const failedRelayKeys = new Set();
|
|
364
411
|
for (const event of appendEvents) {
|
|
365
412
|
try {
|
|
366
|
-
await _append(sessionId, event, { targetPath, syncRemote: false });
|
|
413
|
+
const persisted = await _append(sessionId, event, { targetPath, syncRemote: false });
|
|
367
414
|
relayed += 1;
|
|
368
|
-
|
|
415
|
+
const persistedEvent = persisted && typeof persisted === "object" ? persisted : event;
|
|
416
|
+
existingRelayEvents.push(persistedEvent);
|
|
417
|
+
addSessionEventIdentityKeys(successfulRelayKeys, persistedEvent);
|
|
418
|
+
indexRelayEventIdentityKeys(
|
|
419
|
+
existingRelayIndexByKey,
|
|
420
|
+
existingRelayEvents,
|
|
421
|
+
persistedEvent,
|
|
422
|
+
existingRelayEvents.length - 1,
|
|
423
|
+
);
|
|
369
424
|
} catch {
|
|
425
|
+
addSessionEventIdentityKeys(failedRelayKeys, event);
|
|
370
426
|
// Append errors are observable via the stream but should not
|
|
371
427
|
// abort the rest of the batch — partial relay is still progress.
|
|
372
428
|
}
|
|
373
429
|
}
|
|
374
430
|
|
|
375
431
|
let persistedCursor = false;
|
|
376
|
-
const humanCursorSafe = sourceFullyRelayed(humanResult?.events || [], successfulRelayKeys);
|
|
377
|
-
const eventsCursorSafe = sourceFullyRelayed(eventsResult?.events || [], successfulRelayKeys);
|
|
432
|
+
const humanCursorSafe = sourceFullyRelayed(humanResult?.events || [], successfulRelayKeys, failedRelayKeys);
|
|
433
|
+
const eventsCursorSafe = sourceFullyRelayed(eventsResult?.events || [], successfulRelayKeys, failedRelayKeys);
|
|
378
434
|
if (humanCursorSafe && typeof humanResult?.cursor === "string" && humanResult.cursor.trim()) {
|
|
379
435
|
const result = await writeSyncCursor(sessionId, humanResult.cursor, { targetPath }).catch(() => null);
|
|
380
436
|
persistedCursor = Boolean(result && result.written);
|