sentinelayer-cli 0.39.2 → 0.41.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/README.md +4 -4
- package/package.json +2 -3
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
package/src/session/listener.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
pollSessionEvents,
|
|
5
|
+
pollSessionEventsBefore,
|
|
6
|
+
streamSessionEvents,
|
|
7
|
+
updateSessionReadCursor,
|
|
8
|
+
} from "./sync.js";
|
|
4
9
|
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
5
10
|
import { isSessionListenerLifecycleEvent } from "./control-events.js";
|
|
6
11
|
|
|
@@ -14,8 +19,10 @@ const BROADCAST_RECIPIENTS = new Set([
|
|
|
14
19
|
"all-agents",
|
|
15
20
|
]);
|
|
16
21
|
|
|
17
|
-
const DEFAULT_ACTIVE_INTERVAL_SECONDS =
|
|
22
|
+
const DEFAULT_ACTIVE_INTERVAL_SECONDS = 60;
|
|
18
23
|
const DEFAULT_ACTIVE_WINDOW_SECONDS = 300;
|
|
24
|
+
const DEFAULT_POLL_JITTER_RATIO = 0.2;
|
|
25
|
+
const DEFAULT_MAX_POLL_BACKOFF_SECONDS = 300;
|
|
19
26
|
const MAX_CLOCK_SKEW_MS = 60_000;
|
|
20
27
|
|
|
21
28
|
function normalizeString(value) {
|
|
@@ -38,10 +45,6 @@ function normalizePositiveInteger(value, fallbackValue) {
|
|
|
38
45
|
return Math.floor(normalized);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
function isListenerStopDirective(event = {}) {
|
|
42
|
-
return normalizeString(event?.event) === "listener_stop";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
48
|
function isPlainObject(value) {
|
|
46
49
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
47
50
|
}
|
|
@@ -128,6 +131,46 @@ function cursorFromEvents(events = [], fallbackCursor = null) {
|
|
|
128
131
|
return cursor;
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
function latestSequenceTarget(events = [], fallbackCursor = null) {
|
|
135
|
+
let targetSequenceId = null;
|
|
136
|
+
let targetCursor = normalizeString(fallbackCursor) || "";
|
|
137
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
138
|
+
const sequence = Number(event?.sequenceId ?? event?.sequence_id ?? event?.sequence);
|
|
139
|
+
if (Number.isFinite(sequence) && sequence > 0) {
|
|
140
|
+
if (targetSequenceId === null || sequence > targetSequenceId) {
|
|
141
|
+
targetSequenceId = Math.floor(sequence);
|
|
142
|
+
targetCursor = normalizeString(event?.cursor) || targetCursor;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const cursor = normalizeString(event?.cursor);
|
|
147
|
+
if (cursor) targetCursor = cursor;
|
|
148
|
+
}
|
|
149
|
+
return { targetSequenceId, targetCursor };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function computeListenerPollDelay({
|
|
153
|
+
baseDelayMs,
|
|
154
|
+
consecutiveFailures = 0,
|
|
155
|
+
retryAfterMs = null,
|
|
156
|
+
jitterRatio = DEFAULT_POLL_JITTER_RATIO,
|
|
157
|
+
maxBackoffMs = DEFAULT_MAX_POLL_BACKOFF_SECONDS * 1000,
|
|
158
|
+
randomValue = Math.random(),
|
|
159
|
+
} = {}) {
|
|
160
|
+
const base = Math.max(1, Math.floor(Number(baseDelayMs) || 1));
|
|
161
|
+
const failures = Math.max(0, Math.floor(Number(consecutiveFailures) || 0));
|
|
162
|
+
const maxBackoff = Math.max(base, Math.floor(Number(maxBackoffMs) || base));
|
|
163
|
+
const exponential = failures > 0
|
|
164
|
+
? Math.min(maxBackoff, base * (2 ** Math.min(failures - 1, 20)))
|
|
165
|
+
: base;
|
|
166
|
+
const retryFloor = Math.max(0, Math.ceil(Number(retryAfterMs) || 0));
|
|
167
|
+
const boundedJitterRatio = Math.max(0, Math.min(0.5, Number(jitterRatio) || 0));
|
|
168
|
+
const boundedRandom = Math.max(0, Math.min(1, Number(randomValue) || 0));
|
|
169
|
+
const jitter = Math.floor(exponential * boundedJitterRatio * boundedRandom);
|
|
170
|
+
const jitteredBackoff = Math.min(maxBackoff, exponential + jitter);
|
|
171
|
+
return Math.max(retryFloor, jitteredBackoff);
|
|
172
|
+
}
|
|
173
|
+
|
|
131
174
|
function eventIdentityKey(event = {}) {
|
|
132
175
|
const cursor = normalizeString(event?.cursor);
|
|
133
176
|
if (cursor) return `cursor:${cursor}`;
|
|
@@ -141,6 +184,41 @@ function eventIdentityKey(event = {}) {
|
|
|
141
184
|
});
|
|
142
185
|
}
|
|
143
186
|
|
|
187
|
+
function listenerControlType(control = {}) {
|
|
188
|
+
return normalizeLower(control?.type || control?.action || control?.event);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function listenerControlIssuedAtMs(control = {}) {
|
|
192
|
+
for (const key of ["issuedAtMs", "issued_at_ms"]) {
|
|
193
|
+
const epoch = Number(control?.[key]);
|
|
194
|
+
if (Number.isFinite(epoch) && epoch > 0) return epoch;
|
|
195
|
+
}
|
|
196
|
+
for (const key of ["issuedAt", "issued_at"]) {
|
|
197
|
+
const epoch = Date.parse(normalizeString(control?.[key]));
|
|
198
|
+
if (Number.isFinite(epoch)) return epoch;
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function listenerControlIdentityKey(control = {}) {
|
|
204
|
+
const explicitId = normalizeString(
|
|
205
|
+
control?.controlId || control?.control_id || control?.id,
|
|
206
|
+
);
|
|
207
|
+
if (explicitId) return `control:${explicitId}`;
|
|
208
|
+
return JSON.stringify({
|
|
209
|
+
type: listenerControlType(control),
|
|
210
|
+
issuedAt: normalizeString(control?.issuedAt || control?.issued_at),
|
|
211
|
+
targetAgentId: normalizeComparableId(
|
|
212
|
+
control?.targetAgentId ||
|
|
213
|
+
control?.target_agent_id ||
|
|
214
|
+
control?.payload?.targetAgentId ||
|
|
215
|
+
control?.payload?.target_agent_id,
|
|
216
|
+
),
|
|
217
|
+
broadcast: Boolean(control?.broadcast || control?.payload?.broadcast),
|
|
218
|
+
reason: normalizeString(control?.reason || control?.payload?.reason),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
144
222
|
function eventTimestampMs(event = {}) {
|
|
145
223
|
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
146
224
|
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
@@ -241,6 +319,8 @@ export async function listenSessionEvents({
|
|
|
241
319
|
intervalSeconds = 60,
|
|
242
320
|
activeIntervalSeconds = DEFAULT_ACTIVE_INTERVAL_SECONDS,
|
|
243
321
|
activeWindowSeconds = DEFAULT_ACTIVE_WINDOW_SECONDS,
|
|
322
|
+
jitterRatio = DEFAULT_POLL_JITTER_RATIO,
|
|
323
|
+
maxBackoffSeconds = DEFAULT_MAX_POLL_BACKOFF_SECONDS,
|
|
244
324
|
limit = 200,
|
|
245
325
|
since = undefined,
|
|
246
326
|
replay = false,
|
|
@@ -249,6 +329,7 @@ export async function listenSessionEvents({
|
|
|
249
329
|
maxPolls = null,
|
|
250
330
|
signal,
|
|
251
331
|
onEvent = async () => {},
|
|
332
|
+
onControl = async () => {},
|
|
252
333
|
onError = async () => {},
|
|
253
334
|
onCatchup = async () => {},
|
|
254
335
|
onLifecycle = async () => {},
|
|
@@ -256,12 +337,14 @@ export async function listenSessionEvents({
|
|
|
256
337
|
_poll = pollSessionEvents,
|
|
257
338
|
_pollLatest = pollSessionEventsBefore,
|
|
258
339
|
_stream = streamSessionEvents,
|
|
340
|
+
_updateReadCursor = updateSessionReadCursor,
|
|
259
341
|
_readCursor = readSyncCursor,
|
|
260
342
|
_writeCursor = writeSyncCursor,
|
|
261
343
|
_sleep = defaultSleep,
|
|
262
344
|
_setInterval = setInterval,
|
|
263
345
|
_clearInterval = clearInterval,
|
|
264
346
|
_nowMs = Date.now,
|
|
347
|
+
_random = Math.random,
|
|
265
348
|
} = {}) {
|
|
266
349
|
const normalizedSessionId = normalizeString(sessionId);
|
|
267
350
|
const normalizedAgentId = normalizeComparableId(agentId) || "cli-user";
|
|
@@ -293,10 +376,16 @@ export async function listenSessionEvents({
|
|
|
293
376
|
let streamAttempted = false;
|
|
294
377
|
let streamFallbackReason = "";
|
|
295
378
|
let heartbeatCount = 0;
|
|
379
|
+
let consecutivePollFailures = 0;
|
|
380
|
+
let readCursorUpdates = 0;
|
|
381
|
+
let readCursorReason = "";
|
|
382
|
+
let deliveredControls = 0;
|
|
383
|
+
let ignoredControls = 0;
|
|
296
384
|
let catchupNotified = false;
|
|
297
385
|
let catchupEventCount = 0;
|
|
298
386
|
let catchupMatchingEventCount = 0;
|
|
299
387
|
const emittedKeys = new Set();
|
|
388
|
+
const deliveredControlKeys = new Set();
|
|
300
389
|
const maxPollCount = normalizePositiveInteger(maxPolls, 0);
|
|
301
390
|
const pollLimit = normalizePositiveInteger(limit, 200);
|
|
302
391
|
const idleSleepMs = Math.max(1, normalizePositiveInteger(intervalSeconds, 60)) * 1000;
|
|
@@ -306,6 +395,9 @@ export async function listenSessionEvents({
|
|
|
306
395
|
const activeWindowMs =
|
|
307
396
|
Math.max(1, normalizePositiveInteger(activeWindowSeconds, DEFAULT_ACTIVE_WINDOW_SECONDS)) *
|
|
308
397
|
1000;
|
|
398
|
+
const maxBackoffMs =
|
|
399
|
+
Math.max(1, normalizePositiveInteger(maxBackoffSeconds, DEFAULT_MAX_POLL_BACKOFF_SECONDS)) *
|
|
400
|
+
1000;
|
|
309
401
|
const autoStreamIdleTimeoutMs =
|
|
310
402
|
normalizedTransport === "auto" ? Math.max(5_000, Math.min(idleSleepMs, 30_000)) : 0;
|
|
311
403
|
|
|
@@ -354,6 +446,11 @@ export async function listenSessionEvents({
|
|
|
354
446
|
? new Date(lastHumanActivityMs).toISOString()
|
|
355
447
|
: null,
|
|
356
448
|
lastSleepMs,
|
|
449
|
+
consecutivePollFailures,
|
|
450
|
+
readCursorUpdates,
|
|
451
|
+
readCursorReason,
|
|
452
|
+
deliveredControls,
|
|
453
|
+
ignoredControls,
|
|
357
454
|
reason: lastReason,
|
|
358
455
|
...extra,
|
|
359
456
|
});
|
|
@@ -397,7 +494,7 @@ export async function listenSessionEvents({
|
|
|
397
494
|
cursor: cursor || null,
|
|
398
495
|
candidateCursor: nextCursor,
|
|
399
496
|
});
|
|
400
|
-
return false;
|
|
497
|
+
return { processed: false, retryAfterMs: null };
|
|
401
498
|
}
|
|
402
499
|
|
|
403
500
|
const observedAtMs = Number(_nowMs()) || Date.now();
|
|
@@ -413,7 +510,6 @@ export async function listenSessionEvents({
|
|
|
413
510
|
if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
|
|
414
511
|
lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
|
|
415
512
|
}
|
|
416
|
-
if (timestampMs && timestampMs < startedAtMs && isListenerStopDirective(event)) continue;
|
|
417
513
|
if (isSessionListenerLifecycleEvent(event)) continue;
|
|
418
514
|
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
419
515
|
visibleEvents.push(event);
|
|
@@ -471,8 +567,71 @@ export async function listenSessionEvents({
|
|
|
471
567
|
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
472
568
|
cursor = nextCursor;
|
|
473
569
|
}
|
|
570
|
+
// A fetch cursor echoed on an empty page is transport state, not new read
|
|
571
|
+
// progress. Writing it would recreate one operational write per idle poll.
|
|
572
|
+
const readTarget = events.length > 0
|
|
573
|
+
? latestSequenceTarget(events, nextCursor)
|
|
574
|
+
: { targetSequenceId: null, targetCursor: "" };
|
|
575
|
+
let readCursorResult = null;
|
|
576
|
+
if (readTarget.targetSequenceId || readTarget.targetCursor) {
|
|
577
|
+
readCursorResult = await _updateReadCursor(normalizedSessionId, {
|
|
578
|
+
targetPath,
|
|
579
|
+
targetSequenceId: readTarget.targetSequenceId,
|
|
580
|
+
targetCursor: readTarget.targetCursor,
|
|
581
|
+
agentId: normalizedAgentId,
|
|
582
|
+
}).catch((error) => ({
|
|
583
|
+
ok: false,
|
|
584
|
+
reason: normalizeString(error?.message) || "read_cursor_write_failed",
|
|
585
|
+
}));
|
|
586
|
+
if (readCursorResult?.ok) {
|
|
587
|
+
readCursorUpdates += 1;
|
|
588
|
+
readCursorReason = "";
|
|
589
|
+
} else {
|
|
590
|
+
readCursorReason = normalizeString(readCursorResult?.reason) || "read_cursor_write_failed";
|
|
591
|
+
}
|
|
592
|
+
}
|
|
474
593
|
primed = true;
|
|
475
|
-
return
|
|
594
|
+
return {
|
|
595
|
+
processed: true,
|
|
596
|
+
retryAfterMs: Number(readCursorResult?.retryAfterMs) || null,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function processControlBatch(controlsInput = []) {
|
|
601
|
+
const controls = Array.isArray(controlsInput) ? controlsInput : [];
|
|
602
|
+
const observedAtMs = Number(_nowMs()) || Date.now();
|
|
603
|
+
for (const control of controls) {
|
|
604
|
+
if (!isPlainObject(control)) {
|
|
605
|
+
ignoredControls += 1;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const issuedAtMs = listenerControlIssuedAtMs(control);
|
|
609
|
+
const fresh =
|
|
610
|
+
issuedAtMs >= startedAtMs - MAX_CLOCK_SKEW_MS &&
|
|
611
|
+
issuedAtMs <= observedAtMs + MAX_CLOCK_SKEW_MS;
|
|
612
|
+
if (
|
|
613
|
+
!listenerControlType(control) ||
|
|
614
|
+
!fresh ||
|
|
615
|
+
!eventMatchesAgent(control, normalizedAgentId)
|
|
616
|
+
) {
|
|
617
|
+
ignoredControls += 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const key = listenerControlIdentityKey(control);
|
|
621
|
+
if (deliveredControlKeys.has(key)) {
|
|
622
|
+
ignoredControls += 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
deliveredControlKeys.add(key);
|
|
626
|
+
try {
|
|
627
|
+
await onControl(control);
|
|
628
|
+
deliveredControls += 1;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
deliveredControlKeys.delete(key);
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
if (signal?.aborted) break;
|
|
634
|
+
}
|
|
476
635
|
}
|
|
477
636
|
|
|
478
637
|
async function notifyHeartbeat({ stopping = false, nextPollMs = null } = {}) {
|
|
@@ -492,7 +651,7 @@ export async function listenSessionEvents({
|
|
|
492
651
|
}
|
|
493
652
|
|
|
494
653
|
let streamHeartbeatTimer = null;
|
|
495
|
-
let
|
|
654
|
+
let streamMaintenanceInFlight = null;
|
|
496
655
|
const streamHeartbeatMs = Math.max(1_000, Math.min(idleSleepMs, 60_000));
|
|
497
656
|
|
|
498
657
|
function stopStreamHeartbeatTimer() {
|
|
@@ -503,14 +662,49 @@ export async function listenSessionEvents({
|
|
|
503
662
|
|
|
504
663
|
function startStreamHeartbeatTimer() {
|
|
505
664
|
stopStreamHeartbeatTimer();
|
|
506
|
-
streamHeartbeatTimer = _setInterval(
|
|
507
|
-
if (signal?.aborted ||
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
await notifyHeartbeat({ nextPollMs: streamHeartbeatMs });
|
|
511
|
-
} finally {
|
|
512
|
-
streamHeartbeatInFlight = false;
|
|
665
|
+
streamHeartbeatTimer = _setInterval(() => {
|
|
666
|
+
if (signal?.aborted || streamMaintenanceInFlight) {
|
|
667
|
+
return streamMaintenanceInFlight;
|
|
513
668
|
}
|
|
669
|
+
const maintenance = (async () => {
|
|
670
|
+
const result = await _poll(normalizedSessionId, {
|
|
671
|
+
targetPath,
|
|
672
|
+
since: cursor,
|
|
673
|
+
limit: 1,
|
|
674
|
+
listenerAgentId: normalizedAgentId,
|
|
675
|
+
signal,
|
|
676
|
+
});
|
|
677
|
+
if (signal?.aborted) return;
|
|
678
|
+
if (result?.ok) {
|
|
679
|
+
await processControlBatch(result.listenerControls);
|
|
680
|
+
} else {
|
|
681
|
+
await onError({
|
|
682
|
+
ok: false,
|
|
683
|
+
reason: `control_poll_${normalizeString(result?.reason) || "failed"}`,
|
|
684
|
+
cursor: cursor || null,
|
|
685
|
+
retryAfterMs: Number(result?.retryAfterMs) || null,
|
|
686
|
+
status: Number(result?.status) || null,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (!signal?.aborted) {
|
|
690
|
+
await notifyHeartbeat({ nextPollMs: streamHeartbeatMs });
|
|
691
|
+
}
|
|
692
|
+
})().catch(async (error) => {
|
|
693
|
+
if (signal?.aborted || shouldAbort(error, signal)) return;
|
|
694
|
+
await onError({
|
|
695
|
+
ok: false,
|
|
696
|
+
reason: "control_poll_failed",
|
|
697
|
+
cursor: cursor || null,
|
|
698
|
+
detail: normalizeString(error?.message),
|
|
699
|
+
}).catch(() => {});
|
|
700
|
+
});
|
|
701
|
+
streamMaintenanceInFlight = maintenance;
|
|
702
|
+
maintenance.finally(() => {
|
|
703
|
+
if (streamMaintenanceInFlight === maintenance) {
|
|
704
|
+
streamMaintenanceInFlight = null;
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return maintenance;
|
|
514
708
|
}, streamHeartbeatMs);
|
|
515
709
|
}
|
|
516
710
|
|
|
@@ -534,6 +728,9 @@ export async function listenSessionEvents({
|
|
|
534
728
|
onEvent: async (event) => {
|
|
535
729
|
await processEventBatch([event], normalizeString(event?.cursor) || cursor);
|
|
536
730
|
},
|
|
731
|
+
onCursor: async (streamCursor) => {
|
|
732
|
+
await processEventBatch([], streamCursor);
|
|
733
|
+
},
|
|
537
734
|
onError: async (error) => {
|
|
538
735
|
lastReason = `stream_${normalizeString(error?.reason) || "error"}`;
|
|
539
736
|
await onError({
|
|
@@ -545,8 +742,9 @@ export async function listenSessionEvents({
|
|
|
545
742
|
onHeartbeat: async () => {
|
|
546
743
|
await notifyHeartbeat({ nextPollMs: streamHeartbeatMs });
|
|
547
744
|
},
|
|
548
|
-
}).finally(() => {
|
|
745
|
+
}).finally(async () => {
|
|
549
746
|
stopStreamHeartbeatTimer();
|
|
747
|
+
await streamMaintenanceInFlight;
|
|
550
748
|
});
|
|
551
749
|
if (!streamResult?.ok) {
|
|
552
750
|
streamFallbackReason = normalizeString(streamResult?.reason) || lastReason || "stream_failed";
|
|
@@ -567,24 +765,44 @@ export async function listenSessionEvents({
|
|
|
567
765
|
targetPath,
|
|
568
766
|
since: cursor,
|
|
569
767
|
limit: pollLimit,
|
|
768
|
+
listenerAgentId: normalizedAgentId,
|
|
570
769
|
});
|
|
571
770
|
|
|
771
|
+
let cursorRetryAfterMs = null;
|
|
572
772
|
if (result?.ok) {
|
|
573
773
|
lastReason = "";
|
|
574
|
-
|
|
774
|
+
consecutivePollFailures = 0;
|
|
775
|
+
await processControlBatch(result.listenerControls);
|
|
776
|
+
if (signal?.aborted) break;
|
|
777
|
+
const processed = await processEventBatch(result.events, result.cursor);
|
|
778
|
+
cursorRetryAfterMs = Number(processed?.retryAfterMs) || null;
|
|
575
779
|
} else {
|
|
576
780
|
lastReason = normalizeString(result?.reason) || "poll_failed";
|
|
781
|
+
consecutivePollFailures += 1;
|
|
577
782
|
await onError({
|
|
578
783
|
ok: false,
|
|
579
784
|
reason: lastReason,
|
|
580
785
|
cursor: result?.cursor || cursor || null,
|
|
786
|
+
retryAfterMs: Number(result?.retryAfterMs) || null,
|
|
787
|
+
status: Number(result?.status) || null,
|
|
581
788
|
});
|
|
582
789
|
}
|
|
583
790
|
|
|
584
791
|
const willStop = maxPollCount > 0 && pollCount >= maxPollCount;
|
|
585
792
|
const heartbeatAtMs = Number(_nowMs()) || Date.now();
|
|
586
793
|
const humanActive = isRecentActivity(lastHumanActivityMs, heartbeatAtMs, activeWindowMs);
|
|
587
|
-
const
|
|
794
|
+
const baseSleepMs = humanActive ? Math.min(idleSleepMs, activeSleepMs) : idleSleepMs;
|
|
795
|
+
const nextSleepMs = computeListenerPollDelay({
|
|
796
|
+
baseDelayMs: baseSleepMs,
|
|
797
|
+
consecutiveFailures: consecutivePollFailures,
|
|
798
|
+
retryAfterMs: Math.max(
|
|
799
|
+
Number(result?.retryAfterMs) || 0,
|
|
800
|
+
Number(cursorRetryAfterMs) || 0,
|
|
801
|
+
),
|
|
802
|
+
jitterRatio,
|
|
803
|
+
maxBackoffMs,
|
|
804
|
+
randomValue: _random(),
|
|
805
|
+
});
|
|
588
806
|
await notifyHeartbeat({ nextPollMs: willStop ? null : nextSleepMs, stopping: willStop });
|
|
589
807
|
|
|
590
808
|
if (willStop) break;
|
|
@@ -619,6 +837,11 @@ export async function listenSessionEvents({
|
|
|
619
837
|
matched,
|
|
620
838
|
emitted,
|
|
621
839
|
persistedCursor,
|
|
840
|
+
readCursorUpdates,
|
|
841
|
+
readCursorReason,
|
|
842
|
+
deliveredControls,
|
|
843
|
+
ignoredControls,
|
|
844
|
+
consecutivePollFailures,
|
|
622
845
|
catchupNotified,
|
|
623
846
|
catchupEventCount,
|
|
624
847
|
catchupMatchingEventCount,
|
package/src/session/listeners.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchSessionPresence } from "./sync.js";
|
|
2
2
|
|
|
3
3
|
const LISTENER_EVENT_TYPES = new Set([
|
|
4
4
|
"session_listener_started",
|
|
@@ -10,9 +10,6 @@ const LISTENER_EVENT_TYPES = new Set([
|
|
|
10
10
|
// listener likely died without a clean stop — show it as stale, not live.
|
|
11
11
|
const DEFAULT_STALE_AFTER_MS = 180_000;
|
|
12
12
|
const MAX_STALE_GRACE_MS = 60_000;
|
|
13
|
-
const DEFAULT_LISTENER_FETCH_LIMIT = 200;
|
|
14
|
-
const DEFAULT_LISTENER_FETCH_MAX_PAGES = 5;
|
|
15
|
-
const MAX_LISTENER_FETCH_MAX_PAGES = 25;
|
|
16
13
|
|
|
17
14
|
function normalizeString(value) {
|
|
18
15
|
return String(value || "").trim();
|
|
@@ -144,94 +141,86 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
144
141
|
return rows;
|
|
145
142
|
}
|
|
146
143
|
|
|
144
|
+
function presenceEntryEpochMs(entry = {}) {
|
|
145
|
+
const explicit = Number(entry.lastSeenMs ?? entry.last_seen_ms);
|
|
146
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
147
|
+
return Math.floor(explicit);
|
|
148
|
+
}
|
|
149
|
+
const parsed = Date.parse(
|
|
150
|
+
normalizeString(entry.lastSeen || entry.last_seen || entry.updatedAt || entry.updated_at),
|
|
151
|
+
);
|
|
152
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function presenceRows(entries = [], nowMs = Date.now()) {
|
|
156
|
+
return (Array.isArray(entries) ? entries : [])
|
|
157
|
+
.map((entry) => {
|
|
158
|
+
const agentId = normalizeString(entry?.agentId || entry?.agent_id || entry?.id);
|
|
159
|
+
if (!agentId) return null;
|
|
160
|
+
const seenAtMs = presenceEntryEpochMs(entry);
|
|
161
|
+
const ageMs = seenAtMs === null ? null : Math.max(0, nowMs - seenAtMs);
|
|
162
|
+
return {
|
|
163
|
+
agentId,
|
|
164
|
+
displayName: normalizeString(entry?.displayName || entry?.display_name) || agentId,
|
|
165
|
+
model: normalizeString(entry?.model),
|
|
166
|
+
status: "present",
|
|
167
|
+
active: null,
|
|
168
|
+
cadenceSeconds: null,
|
|
169
|
+
idleIntervalSeconds: null,
|
|
170
|
+
activeIntervalSeconds: null,
|
|
171
|
+
presenceIntervalSeconds: null,
|
|
172
|
+
presenceKeepaliveSeconds: null,
|
|
173
|
+
nextPollSeconds: null,
|
|
174
|
+
staleAfterSeconds: null,
|
|
175
|
+
lastSeenAt: seenAtMs ? new Date(seenAtMs).toISOString() : null,
|
|
176
|
+
lastSeenAgoSeconds: ageMs === null ? null : Math.round(ageMs / 1000),
|
|
177
|
+
lastHumanActivityAt: null,
|
|
178
|
+
};
|
|
179
|
+
})
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.sort((left, right) => (right.lastSeenAt || "").localeCompare(left.lastSeenAt || ""));
|
|
182
|
+
}
|
|
183
|
+
|
|
147
184
|
/**
|
|
148
|
-
* Fetch
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
185
|
+
* Fetch the authoritative ephemeral presence roster.
|
|
186
|
+
*
|
|
187
|
+
* There is intentionally no event-tail compatibility path. A server without
|
|
188
|
+
* the presence endpoint, a disabled rollout flag, or degraded Redis produces
|
|
189
|
+
* an unknown roster instead of silently making every listener a durable
|
|
190
|
+
* heartbeat writer again.
|
|
153
191
|
*/
|
|
154
192
|
export async function fetchSessionListeners(
|
|
155
193
|
sessionId,
|
|
156
194
|
{
|
|
157
195
|
targetPath = process.cwd(),
|
|
158
|
-
limit = DEFAULT_LISTENER_FETCH_LIMIT,
|
|
159
|
-
maxPages = DEFAULT_LISTENER_FETCH_MAX_PAGES,
|
|
160
196
|
nowMs = Date.now,
|
|
161
197
|
forceCircuitProbe = true,
|
|
162
|
-
|
|
198
|
+
fetchPresence = fetchSessionPresence,
|
|
163
199
|
} = {}
|
|
164
200
|
) {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let latestResult = null;
|
|
175
|
-
let allEvents = [];
|
|
176
|
-
let pageCount = 0;
|
|
177
|
-
let listenerEventCount = 0;
|
|
178
|
-
let partial = false;
|
|
179
|
-
let partialReason = "";
|
|
180
|
-
|
|
181
|
-
for (let page = 0; page < maxPageCount; page += 1) {
|
|
182
|
-
const result = await poll(sessionId, {
|
|
183
|
-
targetPath,
|
|
184
|
-
beforeSequence,
|
|
185
|
-
limit: pageLimit,
|
|
186
|
-
forceCircuitProbe,
|
|
187
|
-
});
|
|
188
|
-
pageCount += 1;
|
|
189
|
-
|
|
190
|
-
if (!result?.ok) {
|
|
191
|
-
if (!latestResult) {
|
|
192
|
-
return {
|
|
193
|
-
ok: false,
|
|
194
|
-
reason: normalizeString(result?.reason) || "fetch_failed",
|
|
195
|
-
listeners: [],
|
|
196
|
-
pageCount,
|
|
197
|
-
scannedEventCount: allEvents.length,
|
|
198
|
-
listenerEventCount: 0,
|
|
199
|
-
partial: false,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
partial = true;
|
|
203
|
-
partialReason = normalizeString(result?.reason) || "partial_fetch";
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
latestResult = result;
|
|
208
|
-
const pageEvents = Array.isArray(result.events) ? result.events : [];
|
|
209
|
-
allEvents = [...pageEvents, ...allEvents];
|
|
210
|
-
listenerEventCount += pageEvents.filter((event) =>
|
|
211
|
-
LISTENER_EVENT_TYPES.has(normalizeString(event?.event))
|
|
212
|
-
).length;
|
|
213
|
-
|
|
214
|
-
if (pageEvents.length < pageLimit) break;
|
|
215
|
-
if (listenerEventCount > 0) break;
|
|
216
|
-
|
|
217
|
-
const candidateBeforeSequence = Number(result.beforeSequence || 0);
|
|
218
|
-
if (!Number.isFinite(candidateBeforeSequence) || candidateBeforeSequence <= 0) break;
|
|
219
|
-
const currentBeforeSequence = Number(beforeSequence || 0);
|
|
220
|
-
if (currentBeforeSequence > 0 && candidateBeforeSequence >= currentBeforeSequence) break;
|
|
221
|
-
beforeSequence = Math.floor(candidateBeforeSequence);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const listeners = summarizeListeners(allEvents, { nowMs: nowMs() });
|
|
201
|
+
const result = await fetchPresence(sessionId, {
|
|
202
|
+
targetPath,
|
|
203
|
+
forceCircuitProbe,
|
|
204
|
+
});
|
|
205
|
+
const presenceStatus = normalizeString(result?.status).toLowerCase() || "degraded";
|
|
206
|
+
const authoritative = Boolean(result?.ok && result?.enabled !== false && presenceStatus === "ok");
|
|
207
|
+
const listeners = authoritative
|
|
208
|
+
? presenceRows(result.present, Number(nowMs()) || Date.now())
|
|
209
|
+
: [];
|
|
225
210
|
return {
|
|
226
|
-
ok:
|
|
211
|
+
ok: authoritative,
|
|
212
|
+
authoritative,
|
|
213
|
+
enabled: result?.enabled ?? null,
|
|
214
|
+
presenceStatus,
|
|
215
|
+
reason: authoritative ? "" : normalizeString(result?.reason) || "presence_unknown",
|
|
216
|
+
retryAfterMs: Number(result?.retryAfterMs) || null,
|
|
227
217
|
sessionId: normalizeString(sessionId),
|
|
228
218
|
listeners,
|
|
229
|
-
pageCount,
|
|
230
|
-
scannedEventCount:
|
|
231
|
-
listenerEventCount,
|
|
232
|
-
beforeSequence:
|
|
233
|
-
partial,
|
|
234
|
-
reason: partial ? partialReason : "",
|
|
219
|
+
pageCount: 0,
|
|
220
|
+
scannedEventCount: 0,
|
|
221
|
+
listenerEventCount: 0,
|
|
222
|
+
beforeSequence: null,
|
|
223
|
+
partial: false,
|
|
235
224
|
};
|
|
236
225
|
}
|
|
237
226
|
|
|
@@ -239,7 +228,9 @@ export function formatListenerLine(row) {
|
|
|
239
228
|
const cadence = row.cadenceSeconds ? `${row.cadenceSeconds}s` : "—";
|
|
240
229
|
const seen = row.lastSeenAgoSeconds === null ? "never" : `${row.lastSeenAgoSeconds}s ago`;
|
|
241
230
|
const statusLabel =
|
|
242
|
-
row.status === "
|
|
231
|
+
row.status === "present"
|
|
232
|
+
? "● present"
|
|
233
|
+
: row.status === "active"
|
|
243
234
|
? "● active"
|
|
244
235
|
: row.status === "idle"
|
|
245
236
|
? "○ idle"
|
package/src/session/paths.js
CHANGED
|
@@ -28,6 +28,8 @@ export function resolveSessionPaths(sessionId, { targetPath = process.cwd() } =
|
|
|
28
28
|
lockPath: path.join(sessionDir, ".stream.lock"),
|
|
29
29
|
fileLocksPath: path.join(sessionDir, "file-locks.json"),
|
|
30
30
|
fileLocksLockPath: path.join(sessionDir, ".file-locks.lock"),
|
|
31
|
+
fileLeaseCapabilitiesPath: path.join(sessionDir, "file-lease-capabilities.json"),
|
|
32
|
+
fileLeaseCapabilitiesLockPath: path.join(sessionDir, ".file-lease-capabilities.lock"),
|
|
31
33
|
tasksPath: path.join(sessionDir, "tasks.json"),
|
|
32
34
|
tasksLockPath: path.join(sessionDir, ".tasks.lock"),
|
|
33
35
|
agentsDir: path.join(sessionDir, "agents"),
|