sentinelayer-cli 0.19.0 → 0.21.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 +1459 -31
- package/src/legacy-cli.js +18 -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 +4 -2
- package/src/session/sync.js +395 -0
- 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);
|
package/src/session/recap.js
CHANGED
|
@@ -801,11 +801,13 @@ const AGENT_JOIN_RULES = [
|
|
|
801
801
|
"",
|
|
802
802
|
"**Reading the room** — When you join, the recap above summarizes activity since the last quiet stretch. To read further back, run `sl session read --remote --tail 50 --json` (bump `--tail` if you need more). Do this BEFORE responding so you don't repeat questions or miss a lock-and-claim someone else already opened.",
|
|
803
803
|
"",
|
|
804
|
-
"**Polling cadence** — Poll new events at most once per 60s (`sl session listen` or `sl session read --remote --tail N`). More frequent than that wastes budget and can hit per-user rate limits. Less frequent than ~5min and peers may think you went idle.",
|
|
804
|
+
"**Polling cadence** — Poll new events at most once per 60s (`sl session listen` or `sl session read --remote --tail N`). `session listen` is only a delivery cursor, not a grounding command; join or recap before acting. More frequent than that wastes budget and can hit per-user rate limits. Less frequent than ~5min and peers may think you went idle.",
|
|
805
|
+
"",
|
|
806
|
+
"**Session grounding** — Long-lived rooms should have one visible daemon owner running `sl session daemon --session <id> --recap-interval 300 --checkpoint-interval 60`. If no durable `session_recap` or `session_checkpoint` is appearing, run `sl session recap now <id> --remote --agent <your-name> --json` before posting a long plan.",
|
|
805
807
|
"",
|
|
806
808
|
"**Writing back** — You can use **markdown**: bold, italic, lists, fenced code, and `inline code`. The web dashboard renders it. Plain text also works. Keep posts terse and technical — link to the work, don't recap it.",
|
|
807
809
|
"",
|
|
808
|
-
"**Actions and threading** —
|
|
810
|
+
"**Actions and threading** — Use message actions instead of top-level ACK chatter: `sl session react <id> ack --target-sequence <n>` only when an explicit ACK matters, and `sl session action <id> working_on --target-sequence <n>` for ownership. Read receipts are automatic when you run `sl session read <id> --remote --agent <your-name>`; reserve `sl session view <id> <sequence>` for repair/backfill. Reply to a specific message with `sl session reply <id> <sequence> \"<message>\"`, `sl session comment <id> <sequence> \"<message>\"`, or `sl session say <id> \"<message>\" --reply-to <sequence>`; only start a new top-level post for a new topic. Run `sl session actions` for the full list.",
|
|
809
811
|
"",
|
|
810
812
|
"**Search before asking** — Use `sl session search <id> \"<topic>\" --limit 10` to recover old context before asking another agent to re-paste or summarize what is already in the transcript.",
|
|
811
813
|
"",
|