sentinelayer-cli 0.11.0 → 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 +1 -1
- package/src/commands/session.js +30 -0
- package/src/session/listener.js +52 -24
- package/src/session/remote-hydrate.js +8 -4
- package/src/session/sync-cursor.js +76 -0
- package/src/session/sync.js +23 -1
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
|
}
|
|
@@ -2488,6 +2509,8 @@ export function registerSessionCommand(program) {
|
|
|
2488
2509
|
count: remote.count,
|
|
2489
2510
|
nextCursor: remote.nextCursor || null,
|
|
2490
2511
|
hasMore: Boolean(remote.hasMore),
|
|
2512
|
+
truncated: Boolean(remote.truncated),
|
|
2513
|
+
warnings: Array.isArray(remote.warnings) ? remote.warnings : [],
|
|
2491
2514
|
sessions: trimmed,
|
|
2492
2515
|
};
|
|
2493
2516
|
if (emitJson) {
|
|
@@ -2531,6 +2554,13 @@ export function registerSessionCommand(program) {
|
|
|
2531
2554
|
),
|
|
2532
2555
|
);
|
|
2533
2556
|
}
|
|
2557
|
+
if (remote.truncated) {
|
|
2558
|
+
console.log(
|
|
2559
|
+
pc.yellow(
|
|
2560
|
+
"Remote session listing is truncated by the page cap; JSON output includes nextCursor for resume.",
|
|
2561
|
+
),
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2534
2564
|
return;
|
|
2535
2565
|
}
|
|
2536
2566
|
|
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");
|
package/src/session/sync.js
CHANGED
|
@@ -1255,7 +1255,7 @@ export async function pollSessionEventsBefore(
|
|
|
1255
1255
|
* @param {number} [options.maxPages]
|
|
1256
1256
|
* @param {Function} [options.resolveAuthSession]
|
|
1257
1257
|
* @param {Function} [options.fetchImpl]
|
|
1258
|
-
* @returns {Promise<{ok: boolean, reason: string, sessions: Array<object>, count: number, nextCursor: string|null, hasMore: boolean}>}
|
|
1258
|
+
* @returns {Promise<{ok: boolean, reason: string, sessions: Array<object>, count: number, nextCursor: string|null, hasMore: boolean, truncated: boolean, warnings: Array<object>}>}
|
|
1259
1259
|
*/
|
|
1260
1260
|
export async function listSessionsFromApi({
|
|
1261
1261
|
targetPath = process.cwd(),
|
|
@@ -1283,6 +1283,8 @@ export async function listSessionsFromApi({
|
|
|
1283
1283
|
count: 0,
|
|
1284
1284
|
nextCursor: null,
|
|
1285
1285
|
hasMore: false,
|
|
1286
|
+
truncated: false,
|
|
1287
|
+
warnings: [],
|
|
1286
1288
|
};
|
|
1287
1289
|
}
|
|
1288
1290
|
if (!session || !session.token) {
|
|
@@ -1293,6 +1295,8 @@ export async function listSessionsFromApi({
|
|
|
1293
1295
|
count: 0,
|
|
1294
1296
|
nextCursor: null,
|
|
1295
1297
|
hasMore: false,
|
|
1298
|
+
truncated: false,
|
|
1299
|
+
warnings: [],
|
|
1296
1300
|
};
|
|
1297
1301
|
}
|
|
1298
1302
|
|
|
@@ -1330,6 +1334,8 @@ export async function listSessionsFromApi({
|
|
|
1330
1334
|
count: 0,
|
|
1331
1335
|
nextCursor: null,
|
|
1332
1336
|
hasMore: false,
|
|
1337
|
+
truncated: false,
|
|
1338
|
+
warnings: [],
|
|
1333
1339
|
};
|
|
1334
1340
|
}
|
|
1335
1341
|
if (!response || !response.ok) {
|
|
@@ -1340,6 +1346,8 @@ export async function listSessionsFromApi({
|
|
|
1340
1346
|
count: 0,
|
|
1341
1347
|
nextCursor: null,
|
|
1342
1348
|
hasMore: false,
|
|
1349
|
+
truncated: false,
|
|
1350
|
+
warnings: [],
|
|
1343
1351
|
};
|
|
1344
1352
|
}
|
|
1345
1353
|
const payload = await response.json().catch(() => ({}));
|
|
@@ -1356,6 +1364,18 @@ export async function listSessionsFromApi({
|
|
|
1356
1364
|
if (!fetchAll || !hasMore) break;
|
|
1357
1365
|
}
|
|
1358
1366
|
|
|
1367
|
+
const truncated = Boolean(fetchAll && hasMore && nextCursor);
|
|
1368
|
+
const warnings = truncated
|
|
1369
|
+
? [
|
|
1370
|
+
{
|
|
1371
|
+
code: "SESSION_LIST_MAX_PAGES_REACHED",
|
|
1372
|
+
message: `Session list stopped after ${normalizedMaxPages} pages; output is partial and nextCursor can resume the listing.`,
|
|
1373
|
+
maxPages: normalizedMaxPages,
|
|
1374
|
+
nextCursor,
|
|
1375
|
+
},
|
|
1376
|
+
]
|
|
1377
|
+
: [];
|
|
1378
|
+
|
|
1359
1379
|
return {
|
|
1360
1380
|
ok: true,
|
|
1361
1381
|
reason: "",
|
|
@@ -1363,6 +1383,8 @@ export async function listSessionsFromApi({
|
|
|
1363
1383
|
count,
|
|
1364
1384
|
nextCursor: nextCursor || null,
|
|
1365
1385
|
hasMore,
|
|
1386
|
+
truncated,
|
|
1387
|
+
warnings,
|
|
1366
1388
|
};
|
|
1367
1389
|
}
|
|
1368
1390
|
|