sentinelayer-cli 0.18.2 → 0.20.0
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/ai/identity-lifecycle.js +14 -2
- package/src/commands/mcp.js +60 -0
- package/src/commands/session.js +1255 -25
- package/src/legacy-cli.js +16 -11
- package/src/mcp/registry.js +151 -0
- package/src/mcp/session-stdio-server.js +977 -0
- package/src/scan/generator.js +3 -2
- package/src/session/agent-registry.js +118 -0
- package/src/session/checkpoints.js +71 -1
- package/src/session/coordination-guidance.js +3 -2
- package/src/session/listener.js +302 -68
- package/src/session/pricing-ledger.js +34 -4
- package/src/session/recap.js +141 -4
- package/src/session/sync.js +296 -2
- package/src/session/transcript.js +86 -36
- package/src/session/usage.js +5 -5
- package/src/session/wake/claude.js +175 -0
- package/src/session/wake/codex.js +394 -0
- package/src/session/wake/cursor-store.js +69 -0
- package/src/session/wake/dispatcher.js +184 -0
- package/src/session/wake/pump.js +135 -0
- package/src/session/wake/registry.js +80 -0
- package/src/session/wake/resolve-target.js +146 -0
- package/src/session/wake/sentid.js +103 -0
package/src/session/listener.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
|
|
3
|
-
import { pollSessionEvents } from "./sync.js";
|
|
3
|
+
import { pollSessionEvents, pollSessionEventsBefore, streamSessionEvents } from "./sync.js";
|
|
4
4
|
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
5
5
|
|
|
6
6
|
const BROADCAST_RECIPIENTS = new Set([
|
|
@@ -199,6 +199,21 @@ function humanActivityTimestampMs(event = {}, nowMs = Date.now()) {
|
|
|
199
199
|
return eventTimestampMs(event) || nowMs;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
function eventTimeRange(events = []) {
|
|
203
|
+
let oldestMs = 0;
|
|
204
|
+
let newestMs = 0;
|
|
205
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
206
|
+
const timeMs = eventTimestampMs(event);
|
|
207
|
+
if (!timeMs) continue;
|
|
208
|
+
if (!oldestMs || timeMs < oldestMs) oldestMs = timeMs;
|
|
209
|
+
if (!newestMs || timeMs > newestMs) newestMs = timeMs;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
oldestEventAt: oldestMs ? new Date(oldestMs).toISOString() : null,
|
|
213
|
+
newestEventAt: newestMs ? new Date(newestMs).toISOString() : null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
function isRecentActivity(activityMs, nowMs, windowMs) {
|
|
203
218
|
return (
|
|
204
219
|
Number.isFinite(activityMs) &&
|
|
@@ -208,6 +223,14 @@ function isRecentActivity(activityMs, nowMs, windowMs) {
|
|
|
208
223
|
);
|
|
209
224
|
}
|
|
210
225
|
|
|
226
|
+
function isListenerLifecycleEvent(event = {}) {
|
|
227
|
+
if (!isPlainObject(event)) return false;
|
|
228
|
+
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
229
|
+
const eventType = normalizeString(event.event || event.type).toLowerCase();
|
|
230
|
+
if (eventType.startsWith("session_listener_")) return true;
|
|
231
|
+
return normalizeString(payload.source).toLowerCase() === "session_listen";
|
|
232
|
+
}
|
|
233
|
+
|
|
211
234
|
/**
|
|
212
235
|
* Poll session events in the background and emit only events addressed to
|
|
213
236
|
* the current agent or broadcast to everyone. The loop advances its cursor
|
|
@@ -224,11 +247,18 @@ export async function listenSessionEvents({
|
|
|
224
247
|
limit = 200,
|
|
225
248
|
since = undefined,
|
|
226
249
|
replay = false,
|
|
250
|
+
fromNow = false,
|
|
251
|
+
persistStartCursor = false,
|
|
227
252
|
maxPolls = null,
|
|
228
253
|
signal,
|
|
229
254
|
onEvent = async () => {},
|
|
230
255
|
onError = async () => {},
|
|
256
|
+
onCatchup = async () => {},
|
|
257
|
+
onLifecycle = async () => {},
|
|
258
|
+
transport = "poll",
|
|
231
259
|
_poll = pollSessionEvents,
|
|
260
|
+
_pollLatest = pollSessionEventsBefore,
|
|
261
|
+
_stream = streamSessionEvents,
|
|
232
262
|
_readCursor = readSyncCursor,
|
|
233
263
|
_writeCursor = writeSyncCursor,
|
|
234
264
|
_sleep = defaultSleep,
|
|
@@ -240,17 +270,32 @@ export async function listenSessionEvents({
|
|
|
240
270
|
throw new Error("session id is required.");
|
|
241
271
|
}
|
|
242
272
|
|
|
273
|
+
if (fromNow && since !== undefined) {
|
|
274
|
+
throw new Error("Use either fromNow or since, not both.");
|
|
275
|
+
}
|
|
276
|
+
const normalizedTransport = normalizeLower(transport) || "poll";
|
|
277
|
+
if (!["auto", "poll", "stream"].includes(normalizedTransport)) {
|
|
278
|
+
throw new Error("transport must be one of: auto, poll, stream.");
|
|
279
|
+
}
|
|
280
|
+
|
|
243
281
|
const cursorSuffix = listenCursorSuffix(normalizedAgentId);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
const explicitSince = typeof since === "string" || since === null;
|
|
283
|
+
let cursor = explicitSince
|
|
284
|
+
? normalizeString(since) || null
|
|
285
|
+
: await _readCursor(normalizedSessionId, { targetPath, suffix: cursorSuffix });
|
|
286
|
+
let cursorSource = explicitSince ? "explicit" : cursor ? "stored" : "none";
|
|
248
287
|
let primed = Boolean(cursor) || Boolean(replay);
|
|
249
288
|
let pollCount = 0;
|
|
250
289
|
let emitted = 0;
|
|
251
290
|
let matched = 0;
|
|
252
291
|
let persistedCursor = false;
|
|
253
292
|
let lastReason = "";
|
|
293
|
+
let activeTransport = normalizedTransport === "poll" ? "poll" : "stream";
|
|
294
|
+
let streamAttempted = false;
|
|
295
|
+
let streamFallbackReason = "";
|
|
296
|
+
let catchupNotified = false;
|
|
297
|
+
let catchupEventCount = 0;
|
|
298
|
+
let catchupMatchingEventCount = 0;
|
|
254
299
|
const emittedKeys = new Set();
|
|
255
300
|
const maxPollCount = normalizePositiveInteger(maxPolls, 0);
|
|
256
301
|
const pollLimit = normalizePositiveInteger(limit, 200);
|
|
@@ -261,82 +306,264 @@ export async function listenSessionEvents({
|
|
|
261
306
|
const activeWindowMs =
|
|
262
307
|
Math.max(1, normalizePositiveInteger(activeWindowSeconds, DEFAULT_ACTIVE_WINDOW_SECONDS)) *
|
|
263
308
|
1000;
|
|
309
|
+
|
|
310
|
+
if (fromNow) {
|
|
311
|
+
const latest = await _pollLatest(normalizedSessionId, {
|
|
312
|
+
targetPath,
|
|
313
|
+
limit: 1,
|
|
314
|
+
});
|
|
315
|
+
if (!latest?.ok) {
|
|
316
|
+
throw new Error(`Unable to start listener from the latest event (${latest?.reason || "unknown"}).`);
|
|
317
|
+
}
|
|
318
|
+
cursor = normalizeString(latest.cursor) || null;
|
|
319
|
+
cursorSource = cursor ? "from_now" : "none";
|
|
320
|
+
primed = Boolean(cursor) || Boolean(replay);
|
|
321
|
+
if (cursor && persistStartCursor) {
|
|
322
|
+
const writeResult = await _writeCursor(normalizedSessionId, cursor, {
|
|
323
|
+
targetPath,
|
|
324
|
+
suffix: cursorSuffix,
|
|
325
|
+
}).catch(() => null);
|
|
326
|
+
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
264
330
|
const startedAtMs = Number(_nowMs()) || Date.now();
|
|
265
331
|
let lastHumanActivityMs = 0;
|
|
266
332
|
let lastSleepMs = 0;
|
|
267
333
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
334
|
+
const lifecycleSnapshot = (type, extra = {}) => ({
|
|
335
|
+
type,
|
|
336
|
+
sessionId: normalizedSessionId,
|
|
337
|
+
agentId: normalizedAgentId,
|
|
338
|
+
cursor: cursor || null,
|
|
339
|
+
cursorSuffix,
|
|
340
|
+
pollCount,
|
|
341
|
+
matched,
|
|
342
|
+
emitted,
|
|
343
|
+
persistedCursor,
|
|
344
|
+
cursorSource,
|
|
345
|
+
transport: activeTransport,
|
|
346
|
+
idleIntervalSeconds: Math.round(idleSleepMs / 1000),
|
|
347
|
+
activeIntervalSeconds: Math.round(activeSleepMs / 1000),
|
|
348
|
+
activeWindowSeconds: Math.round(activeWindowMs / 1000),
|
|
349
|
+
lastHumanActivityAt: lastHumanActivityMs
|
|
350
|
+
? new Date(lastHumanActivityMs).toISOString()
|
|
351
|
+
: null,
|
|
352
|
+
lastSleepMs,
|
|
353
|
+
reason: lastReason,
|
|
354
|
+
...extra,
|
|
355
|
+
});
|
|
275
356
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const cursorDidNotAdvance = Boolean(nextCursor && cursor && !cursorAdvances(nextCursor, cursor));
|
|
281
|
-
const cursorFault = cursorDidNotAdvance && (nextCursor !== cursor || events.length > 0);
|
|
282
|
-
if (cursorFault) {
|
|
283
|
-
lastReason = "cursor_not_advanced";
|
|
284
|
-
await onError({
|
|
285
|
-
ok: false,
|
|
286
|
-
reason: lastReason,
|
|
287
|
-
cursor: cursor || null,
|
|
288
|
-
candidateCursor: nextCursor,
|
|
289
|
-
});
|
|
290
|
-
} else {
|
|
291
|
-
const observedAtMs = Number(_nowMs()) || Date.now();
|
|
292
|
-
for (const event of events) {
|
|
293
|
-
const activityMs = humanActivityTimestampMs(event, observedAtMs);
|
|
294
|
-
if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
|
|
295
|
-
lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const shouldEmitBatch = primed || Boolean(replay);
|
|
299
|
-
for (const event of events) {
|
|
300
|
-
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
301
|
-
const key = eventIdentityKey(event);
|
|
302
|
-
if (emittedKeys.has(key)) continue;
|
|
303
|
-
matched += 1;
|
|
304
|
-
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
305
|
-
await onEvent(event);
|
|
306
|
-
emittedKeys.add(key);
|
|
307
|
-
emitted += 1;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (nextCursor && nextCursor !== cursor) {
|
|
311
|
-
const writeResult = await _writeCursor(normalizedSessionId, nextCursor, {
|
|
312
|
-
targetPath,
|
|
313
|
-
suffix: cursorSuffix,
|
|
314
|
-
}).catch(() => null);
|
|
315
|
-
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
316
|
-
cursor = nextCursor;
|
|
317
|
-
}
|
|
318
|
-
primed = true;
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
lastReason = normalizeString(result?.reason) || "poll_failed";
|
|
357
|
+
async function notifyCatchup(payload) {
|
|
358
|
+
try {
|
|
359
|
+
await onCatchup(payload);
|
|
360
|
+
} catch (error) {
|
|
322
361
|
await onError({
|
|
323
362
|
ok: false,
|
|
324
|
-
reason:
|
|
325
|
-
cursor:
|
|
363
|
+
reason: "catchup_notice_failed",
|
|
364
|
+
cursor: payload.cursor || cursor || null,
|
|
365
|
+
detail: normalizeString(error?.message),
|
|
326
366
|
});
|
|
327
367
|
}
|
|
368
|
+
}
|
|
328
369
|
|
|
329
|
-
|
|
330
|
-
const sleepAtMs = Number(_nowMs()) || Date.now();
|
|
331
|
-
const humanActive = isRecentActivity(lastHumanActivityMs, sleepAtMs, activeWindowMs);
|
|
332
|
-
const nextSleepMs = humanActive ? Math.min(idleSleepMs, activeSleepMs) : idleSleepMs;
|
|
333
|
-
lastSleepMs = nextSleepMs;
|
|
370
|
+
async function notifyLifecycle(payload) {
|
|
334
371
|
try {
|
|
335
|
-
await
|
|
372
|
+
await onLifecycle(payload);
|
|
336
373
|
} catch (error) {
|
|
337
|
-
|
|
338
|
-
|
|
374
|
+
await onError({
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: `lifecycle_${payload.type}_failed`,
|
|
377
|
+
cursor: payload.cursor || cursor || null,
|
|
378
|
+
detail: normalizeString(error?.message),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function processEventBatch(eventsInput = [], resultCursor = null) {
|
|
384
|
+
const events = Array.isArray(eventsInput) ? eventsInput : [];
|
|
385
|
+
const nextCursor = normalizeString(resultCursor) || cursorFromEvents(events, cursor);
|
|
386
|
+
const cursorDidNotAdvance = Boolean(nextCursor && cursor && !cursorAdvances(nextCursor, cursor));
|
|
387
|
+
const cursorFault = cursorDidNotAdvance && (nextCursor !== cursor || events.length > 0);
|
|
388
|
+
if (cursorFault) {
|
|
389
|
+
lastReason = "cursor_not_advanced";
|
|
390
|
+
await onError({
|
|
391
|
+
ok: false,
|
|
392
|
+
reason: lastReason,
|
|
393
|
+
cursor: cursor || null,
|
|
394
|
+
candidateCursor: nextCursor,
|
|
395
|
+
});
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const observedAtMs = Number(_nowMs()) || Date.now();
|
|
400
|
+
const visibleEvents = [];
|
|
401
|
+
let preStartEventCount = 0;
|
|
402
|
+
for (const event of events) {
|
|
403
|
+
const timestampMs = eventTimestampMs(event);
|
|
404
|
+
if (!timestampMs || timestampMs < startedAtMs) {
|
|
405
|
+
preStartEventCount += 1;
|
|
406
|
+
}
|
|
407
|
+
const activityMs = humanActivityTimestampMs(event, observedAtMs);
|
|
408
|
+
if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
|
|
409
|
+
lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
|
|
410
|
+
}
|
|
411
|
+
if (isListenerLifecycleEvent(event)) continue;
|
|
412
|
+
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
413
|
+
visibleEvents.push(event);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (
|
|
417
|
+
!catchupNotified &&
|
|
418
|
+
cursorSource === "stored" &&
|
|
419
|
+
Boolean(cursor) &&
|
|
420
|
+
events.length > 0 &&
|
|
421
|
+
preStartEventCount > 0
|
|
422
|
+
) {
|
|
423
|
+
catchupNotified = true;
|
|
424
|
+
catchupEventCount = events.length;
|
|
425
|
+
catchupMatchingEventCount = visibleEvents.length;
|
|
426
|
+
await notifyCatchup({
|
|
427
|
+
type: "catchup",
|
|
428
|
+
sessionId: normalizedSessionId,
|
|
429
|
+
agentId: normalizedAgentId,
|
|
430
|
+
cursor: cursor || null,
|
|
431
|
+
candidateCursor: nextCursor || null,
|
|
432
|
+
cursorSuffix,
|
|
433
|
+
cursorSource,
|
|
434
|
+
pollCount,
|
|
435
|
+
eventCount: events.length,
|
|
436
|
+
matchingEventCount: visibleEvents.length,
|
|
437
|
+
preStartEventCount,
|
|
438
|
+
limit: pollLimit,
|
|
439
|
+
replay: Boolean(replay),
|
|
440
|
+
...eventTimeRange(events),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const shouldEmitBatch = primed || Boolean(replay);
|
|
445
|
+
for (const event of visibleEvents) {
|
|
446
|
+
const key = eventIdentityKey(event);
|
|
447
|
+
if (emittedKeys.has(key)) continue;
|
|
448
|
+
matched += 1;
|
|
449
|
+
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
450
|
+
await onEvent(event);
|
|
451
|
+
emittedKeys.add(key);
|
|
452
|
+
emitted += 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (nextCursor && nextCursor !== cursor) {
|
|
456
|
+
const writeResult = await _writeCursor(normalizedSessionId, nextCursor, {
|
|
457
|
+
targetPath,
|
|
458
|
+
suffix: cursorSuffix,
|
|
459
|
+
}).catch(() => null);
|
|
460
|
+
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
461
|
+
cursor = nextCursor;
|
|
462
|
+
}
|
|
463
|
+
primed = true;
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function notifyHeartbeat({ stopping = false, nextPollMs = null } = {}) {
|
|
468
|
+
const heartbeatAtMs = Number(_nowMs()) || Date.now();
|
|
469
|
+
const humanActive = isRecentActivity(lastHumanActivityMs, heartbeatAtMs, activeWindowMs);
|
|
470
|
+
await notifyLifecycle(
|
|
471
|
+
lifecycleSnapshot("heartbeat", {
|
|
472
|
+
active: humanActive,
|
|
473
|
+
state: humanActive ? "active" : "idle",
|
|
474
|
+
nextPollMs,
|
|
475
|
+
stopping,
|
|
476
|
+
transport: activeTransport,
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
return humanActive;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await notifyLifecycle(
|
|
483
|
+
lifecycleSnapshot("started", {
|
|
484
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
485
|
+
transport: activeTransport,
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
if (normalizedTransport !== "poll") {
|
|
491
|
+
streamAttempted = true;
|
|
492
|
+
activeTransport = "stream";
|
|
493
|
+
const streamResult = await _stream(normalizedSessionId, {
|
|
494
|
+
targetPath,
|
|
495
|
+
since: cursor,
|
|
496
|
+
signal,
|
|
497
|
+
onEvent: async (event) => {
|
|
498
|
+
await processEventBatch([event], normalizeString(event?.cursor) || cursor);
|
|
499
|
+
},
|
|
500
|
+
onError: async (error) => {
|
|
501
|
+
lastReason = `stream_${normalizeString(error?.reason) || "error"}`;
|
|
502
|
+
await onError({
|
|
503
|
+
ok: false,
|
|
504
|
+
reason: lastReason,
|
|
505
|
+
cursor: error?.cursor || cursor || null,
|
|
506
|
+
});
|
|
507
|
+
},
|
|
508
|
+
onHeartbeat: async () => {
|
|
509
|
+
await notifyHeartbeat({ nextPollMs: null });
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
if (!streamResult?.ok) {
|
|
513
|
+
streamFallbackReason = normalizeString(streamResult?.reason) || lastReason || "stream_failed";
|
|
514
|
+
lastReason = `stream_${streamFallbackReason}`;
|
|
515
|
+
} else if (!signal?.aborted) {
|
|
516
|
+
streamFallbackReason = "stream_closed";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const shouldPoll =
|
|
521
|
+
!signal?.aborted && (normalizedTransport === "poll" || normalizedTransport === "auto");
|
|
522
|
+
if (shouldPoll) {
|
|
523
|
+
activeTransport = "poll";
|
|
524
|
+
}
|
|
525
|
+
while (shouldPoll && !signal?.aborted) {
|
|
526
|
+
pollCount += 1;
|
|
527
|
+
const result = await _poll(normalizedSessionId, {
|
|
528
|
+
targetPath,
|
|
529
|
+
since: cursor,
|
|
530
|
+
limit: pollLimit,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (result?.ok) {
|
|
534
|
+
lastReason = "";
|
|
535
|
+
await processEventBatch(result.events, result.cursor);
|
|
536
|
+
} else {
|
|
537
|
+
lastReason = normalizeString(result?.reason) || "poll_failed";
|
|
538
|
+
await onError({
|
|
539
|
+
ok: false,
|
|
540
|
+
reason: lastReason,
|
|
541
|
+
cursor: result?.cursor || cursor || null,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const willStop = maxPollCount > 0 && pollCount >= maxPollCount;
|
|
546
|
+
const heartbeatAtMs = Number(_nowMs()) || Date.now();
|
|
547
|
+
const humanActive = isRecentActivity(lastHumanActivityMs, heartbeatAtMs, activeWindowMs);
|
|
548
|
+
const nextSleepMs = humanActive ? Math.min(idleSleepMs, activeSleepMs) : idleSleepMs;
|
|
549
|
+
await notifyHeartbeat({ nextPollMs: willStop ? null : nextSleepMs, stopping: willStop });
|
|
550
|
+
|
|
551
|
+
if (willStop) break;
|
|
552
|
+
lastSleepMs = nextSleepMs;
|
|
553
|
+
try {
|
|
554
|
+
await _sleep(nextSleepMs, { signal });
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (shouldAbort(error, signal)) break;
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
339
559
|
}
|
|
560
|
+
} finally {
|
|
561
|
+
await notifyLifecycle(
|
|
562
|
+
lifecycleSnapshot("stopped", {
|
|
563
|
+
stoppedAt: new Date(Number(_nowMs()) || Date.now()).toISOString(),
|
|
564
|
+
aborted: Boolean(signal?.aborted),
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
340
567
|
}
|
|
341
568
|
|
|
342
569
|
return {
|
|
@@ -345,10 +572,17 @@ export async function listenSessionEvents({
|
|
|
345
572
|
agentId: normalizedAgentId,
|
|
346
573
|
cursor,
|
|
347
574
|
cursorSuffix,
|
|
575
|
+
cursorSource,
|
|
576
|
+
transport: activeTransport,
|
|
577
|
+
streamAttempted,
|
|
578
|
+
streamFallbackReason,
|
|
348
579
|
pollCount,
|
|
349
580
|
matched,
|
|
350
581
|
emitted,
|
|
351
582
|
persistedCursor,
|
|
583
|
+
catchupNotified,
|
|
584
|
+
catchupEventCount,
|
|
585
|
+
catchupMatchingEventCount,
|
|
352
586
|
idleIntervalSeconds: Math.round(idleSleepMs / 1000),
|
|
353
587
|
activeIntervalSeconds: Math.round(activeSleepMs / 1000),
|
|
354
588
|
activeWindowSeconds: Math.round(activeWindowMs / 1000),
|
|
@@ -14,6 +14,30 @@ const SUPPORTED_SESSION_USAGE_SCHEMAS = new Set([
|
|
|
14
14
|
LOCAL_SESSION_USAGE_SCHEMA,
|
|
15
15
|
...LEGACY_SESSION_USAGE_SCHEMAS,
|
|
16
16
|
]);
|
|
17
|
+
const USAGE_HINT_KEYS = [
|
|
18
|
+
"totalTokens",
|
|
19
|
+
"total_tokens",
|
|
20
|
+
"tokens",
|
|
21
|
+
"tokenTotal",
|
|
22
|
+
"token_total",
|
|
23
|
+
"inputTokens",
|
|
24
|
+
"input_tokens",
|
|
25
|
+
"tokensIn",
|
|
26
|
+
"tokens_in",
|
|
27
|
+
"promptTokens",
|
|
28
|
+
"prompt_tokens",
|
|
29
|
+
"outputTokens",
|
|
30
|
+
"output_tokens",
|
|
31
|
+
"tokensOut",
|
|
32
|
+
"tokens_out",
|
|
33
|
+
"completionTokens",
|
|
34
|
+
"completion_tokens",
|
|
35
|
+
"providerCostUsd",
|
|
36
|
+
"provider_cost_usd",
|
|
37
|
+
"costUsd",
|
|
38
|
+
"cost_usd",
|
|
39
|
+
"cost",
|
|
40
|
+
];
|
|
17
41
|
|
|
18
42
|
function n(value) {
|
|
19
43
|
return String(value == null ? "" : value).trim();
|
|
@@ -53,6 +77,11 @@ function pickText(sources, keys) {
|
|
|
53
77
|
return n(pick(sources, keys));
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
function hasUsageHints(value) {
|
|
81
|
+
const bag = object(value);
|
|
82
|
+
return USAGE_HINT_KEYS.some((key) => bag[key] != null && bag[key] !== "");
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
function pickInt(sources, keys) {
|
|
57
86
|
return nonNegativeInt(pick(sources, keys)) ?? 0;
|
|
58
87
|
}
|
|
@@ -109,13 +138,14 @@ export function buildUsageLedgerEntry(
|
|
|
109
138
|
{ sessionId = "", priceBookVersion = DEFAULT_PRICE_BOOK_VERSION, billingTier = "unknown" } = {},
|
|
110
139
|
) {
|
|
111
140
|
const kind = n(event?.event || event?.type);
|
|
112
|
-
if (kind !== SESSION_USAGE_EVENT) return null;
|
|
113
|
-
|
|
114
141
|
const payload = object(event?.payload);
|
|
115
142
|
const schema = n(payload.schema);
|
|
116
|
-
if (!SUPPORTED_SESSION_USAGE_SCHEMAS.has(schema)) return null;
|
|
117
|
-
|
|
118
143
|
const usage = object(payload.usage);
|
|
144
|
+
const legacyUsageEvent = kind !== SESSION_USAGE_EVENT && hasUsageHints(usage);
|
|
145
|
+
if (!legacyUsageEvent) {
|
|
146
|
+
if (kind !== SESSION_USAGE_EVENT) return null;
|
|
147
|
+
if (!SUPPORTED_SESSION_USAGE_SCHEMAS.has(schema)) return null;
|
|
148
|
+
}
|
|
119
149
|
const prompt = object(payload.prompt);
|
|
120
150
|
const response = object(payload.response);
|
|
121
151
|
const agent = object(event?.agent);
|