sentinelayer-cli 0.11.1 → 0.11.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
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -1397,6 +1397,7 @@ export function registerSessionCommand(program) {
|
|
|
1397
1397
|
}
|
|
1398
1398
|
const persisted = await appendToStream(normalizedSessionId, event, {
|
|
1399
1399
|
targetPath,
|
|
1400
|
+
syncRemote: !localSession.materialized,
|
|
1400
1401
|
});
|
|
1401
1402
|
const payload = {
|
|
1402
1403
|
command: "session say",
|
|
@@ -1768,6 +1769,8 @@ export function registerSessionCommand(program) {
|
|
|
1768
1769
|
tail: 0,
|
|
1769
1770
|
});
|
|
1770
1771
|
const displayEvents = [...allEvents];
|
|
1772
|
+
let remoteTailAppended = 0;
|
|
1773
|
+
let remoteTailDisplayedOnly = 0;
|
|
1771
1774
|
if (remoteTail?.ok && Array.isArray(remoteTail.events) && remoteTail.events.length > 0) {
|
|
1772
1775
|
const knownKeys = new Set();
|
|
1773
1776
|
for (const event of allEvents) {
|
|
@@ -1784,13 +1787,19 @@ export function registerSessionCommand(program) {
|
|
|
1784
1787
|
});
|
|
1785
1788
|
displayEvents.push(appended);
|
|
1786
1789
|
addSessionEventIdentityKeys(knownKeys, appended);
|
|
1790
|
+
remoteTailAppended += 1;
|
|
1787
1791
|
} catch {
|
|
1788
1792
|
displayEvents.push(event);
|
|
1789
1793
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
1794
|
+
remoteTailDisplayedOnly += 1;
|
|
1790
1795
|
}
|
|
1791
1796
|
}
|
|
1792
1797
|
}
|
|
1793
1798
|
const events = dedupeSessionEvents(displayEvents).slice(-tail);
|
|
1799
|
+
const remoteVerified = Boolean(
|
|
1800
|
+
options.remote &&
|
|
1801
|
+
((hydration && hydration.ok) || (remoteTail && remoteTail.ok))
|
|
1802
|
+
);
|
|
1794
1803
|
const payload = {
|
|
1795
1804
|
command: "session read",
|
|
1796
1805
|
targetPath,
|
|
@@ -1798,6 +1807,15 @@ export function registerSessionCommand(program) {
|
|
|
1798
1807
|
tail,
|
|
1799
1808
|
count: events.length,
|
|
1800
1809
|
events,
|
|
1810
|
+
displaySource: !options.remote
|
|
1811
|
+
? "local"
|
|
1812
|
+
: remoteTail?.ok
|
|
1813
|
+
? "remote_verified_tail"
|
|
1814
|
+
: hydration?.ok
|
|
1815
|
+
? "hydrated_local"
|
|
1816
|
+
: "local_only",
|
|
1817
|
+
remoteVerified,
|
|
1818
|
+
localEventCount: allEvents.length,
|
|
1801
1819
|
remote: hydration
|
|
1802
1820
|
? {
|
|
1803
1821
|
...hydration,
|
|
@@ -1807,6 +1825,9 @@ export function registerSessionCommand(program) {
|
|
|
1807
1825
|
reason: remoteTail.reason || "",
|
|
1808
1826
|
count: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
1809
1827
|
cursor: remoteTail.cursor || null,
|
|
1828
|
+
verified: Boolean(remoteTail.ok),
|
|
1829
|
+
appended: remoteTailAppended,
|
|
1830
|
+
displayedOnly: remoteTailDisplayedOnly,
|
|
1810
1831
|
}
|
|
1811
1832
|
: null,
|
|
1812
1833
|
}
|
package/src/session/listener.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
|
|
3
3
|
import { pollSessionEvents } from "./sync.js";
|
|
4
|
-
import { readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
4
|
+
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
5
5
|
|
|
6
6
|
const BROADCAST_RECIPIENTS = new Set([
|
|
7
7
|
"*",
|
|
@@ -123,6 +123,19 @@ function cursorFromEvents(events = [], fallbackCursor = null) {
|
|
|
123
123
|
return cursor;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function eventIdentityKey(event = {}) {
|
|
127
|
+
const cursor = normalizeString(event?.cursor);
|
|
128
|
+
if (cursor) return `cursor:${cursor}`;
|
|
129
|
+
const sequence = normalizeString(event?.sequenceId || event?.sequence_id || event?.sequence);
|
|
130
|
+
if (sequence) return `sequence:${sequence}`;
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
event: normalizeString(event?.event),
|
|
133
|
+
agent: normalizeString(event?.agent?.id || event?.agentId),
|
|
134
|
+
ts: normalizeString(event?.ts || event?.timestamp || event?.createdAt || event?.at),
|
|
135
|
+
message: normalizeString(event?.payload?.message || event?.payload?.text || event?.payload?.detail),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
function eventTimestampMs(event = {}) {
|
|
127
140
|
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
128
141
|
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
@@ -238,6 +251,7 @@ export async function listenSessionEvents({
|
|
|
238
251
|
let matched = 0;
|
|
239
252
|
let persistedCursor = false;
|
|
240
253
|
let lastReason = "";
|
|
254
|
+
const emittedKeys = new Set();
|
|
241
255
|
const maxPollCount = normalizePositiveInteger(maxPolls, 0);
|
|
242
256
|
const pollLimit = normalizePositiveInteger(limit, 200);
|
|
243
257
|
const idleSleepMs = Math.max(1, normalizePositiveInteger(intervalSeconds, 60)) * 1000;
|
|
@@ -262,32 +276,46 @@ export async function listenSessionEvents({
|
|
|
262
276
|
if (result?.ok) {
|
|
263
277
|
lastReason = "";
|
|
264
278
|
const events = Array.isArray(result.events) ? result.events : [];
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
const nextCursor = normalizeString(result.cursor) || cursorFromEvents(events, cursor);
|
|
280
|
+
const cursorMovedBackward = Boolean(nextCursor && cursor && !cursorAdvances(nextCursor, cursor));
|
|
281
|
+
if (cursorMovedBackward) {
|
|
282
|
+
lastReason = "cursor_not_advanced";
|
|
283
|
+
await onError({
|
|
284
|
+
ok: false,
|
|
285
|
+
reason: lastReason,
|
|
286
|
+
cursor: cursor || null,
|
|
287
|
+
candidateCursor: nextCursor,
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
const observedAtMs = Number(_nowMs()) || Date.now();
|
|
291
|
+
for (const event of events) {
|
|
292
|
+
const activityMs = humanActivityTimestampMs(event, observedAtMs);
|
|
293
|
+
if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
|
|
294
|
+
lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const shouldEmitBatch = primed || Boolean(replay);
|
|
298
|
+
for (const event of events) {
|
|
299
|
+
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
300
|
+
const key = eventIdentityKey(event);
|
|
301
|
+
if (emittedKeys.has(key)) continue;
|
|
302
|
+
matched += 1;
|
|
303
|
+
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
304
|
+
await onEvent(event);
|
|
305
|
+
emittedKeys.add(key);
|
|
306
|
+
emitted += 1;
|
|
270
307
|
}
|
|
271
|
-
}
|
|
272
|
-
const shouldEmitBatch = primed || Boolean(replay);
|
|
273
|
-
for (const event of events) {
|
|
274
|
-
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
275
|
-
matched += 1;
|
|
276
|
-
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
277
|
-
await onEvent(event);
|
|
278
|
-
emitted += 1;
|
|
279
|
-
}
|
|
280
308
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
309
|
+
if (nextCursor && nextCursor !== cursor) {
|
|
310
|
+
const writeResult = await _writeCursor(normalizedSessionId, nextCursor, {
|
|
311
|
+
targetPath,
|
|
312
|
+
suffix: cursorSuffix,
|
|
313
|
+
}).catch(() => null);
|
|
314
|
+
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
315
|
+
cursor = nextCursor;
|
|
316
|
+
}
|
|
317
|
+
primed = true;
|
|
289
318
|
}
|
|
290
|
-
primed = true;
|
|
291
319
|
} else {
|
|
292
320
|
lastReason = normalizeString(result?.reason) || "poll_failed";
|
|
293
321
|
await onError({
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
isSessionCacheExpired,
|
|
27
27
|
refreshSessionCacheForRemoteActivity,
|
|
28
28
|
} from "./store.js";
|
|
29
|
-
import { readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
29
|
+
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
30
30
|
import {
|
|
31
31
|
addSessionEventIdentityKeys,
|
|
32
32
|
sessionEventHasKnownIdentity,
|
|
@@ -166,11 +166,15 @@ async function pollSessionEventPages({
|
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
const pageEvents = Array.isArray(result.events) ? result.events : [];
|
|
170
|
-
events.push(...pageEvents);
|
|
171
169
|
const nextCursor =
|
|
172
170
|
typeof result.cursor === "string" && result.cursor.trim() ? result.cursor.trim() : cursor;
|
|
173
|
-
const progressed = nextCursor && nextCursor
|
|
171
|
+
const progressed = nextCursor && cursorAdvances(nextCursor, cursor);
|
|
172
|
+
if (nextCursor && cursor && !progressed) {
|
|
173
|
+
reason = "cursor_not_advanced";
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const pageEvents = Array.isArray(result.events) ? result.events : [];
|
|
177
|
+
events.push(...pageEvents);
|
|
174
178
|
cursor = nextCursor || cursor;
|
|
175
179
|
|
|
176
180
|
if (pageEvents.length < normalizedLimit) {
|
|
@@ -46,6 +46,62 @@ export async function readSyncCursor(sessionId, { targetPath, suffix = "" } = {}
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function parseStableCursor(cursor) {
|
|
50
|
+
const normalized = typeof cursor === "string" ? cursor.trim() : "";
|
|
51
|
+
const match = /^(\d{10,}):([0-9a-fA-F]{1,16})$/.exec(normalized);
|
|
52
|
+
if (!match) return null;
|
|
53
|
+
const timeMs = Number(match[1]);
|
|
54
|
+
const sequence = Number.parseInt(match[2], 16);
|
|
55
|
+
if (!Number.isFinite(timeMs) || !Number.isFinite(sequence)) return null;
|
|
56
|
+
return { timeMs, sequence };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseIsoCursor(cursor) {
|
|
60
|
+
const normalized = typeof cursor === "string" ? cursor.trim() : "";
|
|
61
|
+
if (!normalized || normalized.includes(":") === false) return null;
|
|
62
|
+
const epoch = Date.parse(normalized);
|
|
63
|
+
return Number.isFinite(epoch) ? epoch : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function compareSyncCursors(candidate, current) {
|
|
67
|
+
const next = typeof candidate === "string" ? candidate.trim() : "";
|
|
68
|
+
const previous = typeof current === "string" ? current.trim() : "";
|
|
69
|
+
if (!next) return null;
|
|
70
|
+
if (!previous) return 1;
|
|
71
|
+
if (next === previous) return 0;
|
|
72
|
+
|
|
73
|
+
const nextStable = parseStableCursor(next);
|
|
74
|
+
const previousStable = parseStableCursor(previous);
|
|
75
|
+
if (nextStable && previousStable) {
|
|
76
|
+
if (nextStable.sequence !== previousStable.sequence) {
|
|
77
|
+
return nextStable.sequence > previousStable.sequence ? 1 : -1;
|
|
78
|
+
}
|
|
79
|
+
if (nextStable.timeMs !== previousStable.timeMs) {
|
|
80
|
+
return nextStable.timeMs > previousStable.timeMs ? 1 : -1;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const nextIso = parseIsoCursor(next);
|
|
86
|
+
const previousIso = parseIsoCursor(previous);
|
|
87
|
+
if (nextIso !== null && previousIso !== null) {
|
|
88
|
+
if (nextIso === previousIso) return 0;
|
|
89
|
+
return nextIso > previousIso ? 1 : -1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function cursorAdvances(candidate, current) {
|
|
96
|
+
const comparison = compareSyncCursors(candidate, current);
|
|
97
|
+
if (comparison === null) {
|
|
98
|
+
const next = typeof candidate === "string" ? candidate.trim() : "";
|
|
99
|
+
const previous = typeof current === "string" ? current.trim() : "";
|
|
100
|
+
return Boolean(next && next !== previous);
|
|
101
|
+
}
|
|
102
|
+
return comparison > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
49
105
|
/**
|
|
50
106
|
* Persist the human-message cursor for a session. No-op when cursor is
|
|
51
107
|
* empty so we never overwrite a real value with an empty one.
|
|
@@ -61,6 +117,26 @@ export async function writeSyncCursor(sessionId, cursor, { targetPath, suffix =
|
|
|
61
117
|
if (!sessionId || !normalized) {
|
|
62
118
|
return { written: false, path: filePath };
|
|
63
119
|
}
|
|
120
|
+
const existing = await readSyncCursor(sessionId, { targetPath, suffix });
|
|
121
|
+
const comparison = compareSyncCursors(normalized, existing);
|
|
122
|
+
if (existing && comparison !== null && comparison < 0) {
|
|
123
|
+
return {
|
|
124
|
+
written: false,
|
|
125
|
+
path: filePath,
|
|
126
|
+
reason: "stale_cursor",
|
|
127
|
+
previousCursor: existing,
|
|
128
|
+
cursor: normalized,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (existing && comparison === 0) {
|
|
132
|
+
return {
|
|
133
|
+
written: false,
|
|
134
|
+
path: filePath,
|
|
135
|
+
reason: "unchanged",
|
|
136
|
+
previousCursor: existing,
|
|
137
|
+
cursor: normalized,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
64
140
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
65
141
|
const payload = { cursor: normalized, updatedAt: new Date().toISOString() };
|
|
66
142
|
await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|