@oar-sh/oar 0.9.5 → 0.9.7
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/.github/extensions/web-relay/polling/polling-loop.mjs +12 -32
- package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
- package/CHANGELOG.md +340 -0
- package/README.md +196 -26
- package/package.json +2 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
- package/server/claude-worker/claude-session-process.mjs +151 -1
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/claude-turn-failure.mjs +92 -0
- package/server/claude-worker/claude-turn-publisher.mjs +52 -3
- package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
- package/server/copilot-worker/copilot-model-switch.mjs +29 -3
- package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
- package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
- package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
- package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
- package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
- package/server/db-schema.mjs +24 -0
- package/server/grok-worker/acp-client.mjs +68 -12
- package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
- package/server/grok-worker/grok-turn-runner.mjs +6 -2
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +160 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +347 -31
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +15 -3
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +178 -8
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +704 -28
- package/server/public/app/markdown-strikethrough.mjs +31 -0
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/relay-board-inline.mjs +70 -0
- package/server/public/app/relay-board-view.js +157 -61
- package/server/public/app/relay-error-ctas.mjs +28 -5
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +60 -0
- package/server/public/app/store.js +4 -1
- package/server/public/app/subagent-fold.mjs +61 -0
- package/server/public/app/usage-limit-ui.js +225 -0
- package/server/public/index.html +471 -8
- package/server/relay-tools.md +13 -1
- package/server/repositories/message-repository.mjs +10 -1
- package/server/repositories/question-repository.mjs +1 -0
- package/server/repositories/remote-relay-repository.mjs +72 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +202 -26
- package/server/routes/relay-board-routes.mjs +56 -2
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +538 -72
- package/server/server-runtime.mjs +291 -27
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/preview-proxy-server.mjs +64 -36
- package/server/services/process-list-service.mjs +122 -0
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/relay-console-log.mjs +199 -0
- package/server/services/remote-relay-caller-context.mjs +78 -19
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +451 -39
- package/server/services/remote-relay-inbound.mjs +83 -1
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +410 -138
- package/server/services/session-worker-registry-service.mjs +1 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +89 -2
- package/server/services/usage-limit-pause-service.mjs +443 -0
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/ask-user-bridge.mjs +14 -39
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/claude-usage-limit.mjs +257 -0
- package/shared/control-poller.mjs +69 -20
- package/shared/failure-note-text.mjs +38 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/question-wait.mjs +108 -0
- package/shared/remote-relay-contract.mjs +167 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +17 -0
- package/shared/steer-settle-markers.mjs +8 -0
- package/shared/worker-runtime/process-tree.mjs +483 -0
- package/shared/worker-runtime/relay-errors.mjs +31 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
- package/shared/worker-runtime/turn-liveness.mjs +249 -0
|
@@ -20,18 +20,22 @@ function toPidList(rows, extraPid) {
|
|
|
20
20
|
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// The process list is read off the relay's thread, fresh (a stop must see
|
|
24
|
+
// the children started a moment ago). Throws when the list could not be
|
|
25
|
+
// read: the caller must not pretend it stopped what it could not see.
|
|
26
|
+
async function enumeratePids({ sdkSessionId, worker, processInspector, platform }) {
|
|
24
27
|
const rows = platform === 'win32'
|
|
25
|
-
? (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|| processInspector?.findProcessesForSession?.(sdkSessionId)
|
|
29
|
-
|| []
|
|
30
|
-
)
|
|
31
|
-
: (processInspector?.findProcessesForSession?.(sdkSessionId) || []);
|
|
32
|
-
return toPidList(rows, worker?.pid);
|
|
28
|
+
? await processInspector?.findWindowsProcessTreeForSessionAsync?.(sdkSessionId, { fresh: true })
|
|
29
|
+
: await processInspector?.findProcessesForSessionAsync?.(sdkSessionId, { fresh: true });
|
|
30
|
+
return toPidList(rows || [], worker?.pid);
|
|
33
31
|
}
|
|
34
32
|
|
|
33
|
+
async function stopWindowsPids(processInspector, pids) {
|
|
34
|
+
await processInspector?.stopWindowsPidsAsync?.(pids);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const PROCESS_LIST_UNREADABLE = 'process-list-unreadable';
|
|
38
|
+
|
|
35
39
|
export async function stopSessionWorkerProcesses({
|
|
36
40
|
sdkSessionId,
|
|
37
41
|
worker = null,
|
|
@@ -61,7 +65,14 @@ export async function stopSessionWorkerProcesses({
|
|
|
61
65
|
|
|
62
66
|
if (!sid) return done({ ok: false, error: 'Missing session id' });
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
let pids;
|
|
69
|
+
try {
|
|
70
|
+
pids = await enumeratePids({ sdkSessionId: sid, worker, processInspector, platform });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Nothing is stopped blind: the known pid alone would leave the tree
|
|
73
|
+
// running, and the caller would relaunch next to it.
|
|
74
|
+
return done({ ok: false, pids: [], remainingPids: [], error: `${PROCESS_LIST_UNREADABLE}: ${error?.message || error}` });
|
|
75
|
+
}
|
|
65
76
|
if (!pids.length) return done({ pids: [] });
|
|
66
77
|
|
|
67
78
|
const survivors = (list) => list.filter((pid) => isPidAliveImpl(pid));
|
|
@@ -80,7 +91,7 @@ export async function stopSessionWorkerProcesses({
|
|
|
80
91
|
if (platform === 'win32') {
|
|
81
92
|
// stopWindowsPids already walks the descendant tree leaf-first and uses
|
|
82
93
|
// Stop-Process -Force, so there is no gentler first pass to attempt.
|
|
83
|
-
|
|
94
|
+
await stopWindowsPids(processInspector, pids);
|
|
84
95
|
} else {
|
|
85
96
|
for (const pid of pids) {
|
|
86
97
|
try {
|
|
@@ -104,9 +115,9 @@ export async function stopSessionWorkerProcesses({
|
|
|
104
115
|
try {
|
|
105
116
|
if (platform === 'win32') {
|
|
106
117
|
// Re-enumerate: children spawned after the first snapshot are invisible to it.
|
|
107
|
-
const rediscovered = enumeratePids({ sdkSessionId: sid, worker, processInspector, platform });
|
|
118
|
+
const rediscovered = await enumeratePids({ sdkSessionId: sid, worker, processInspector, platform });
|
|
108
119
|
escalationTargets = [...new Set([...remainingPids, ...rediscovered.filter((pid) => isPidAliveImpl(pid))])];
|
|
109
|
-
|
|
120
|
+
await stopWindowsPids(processInspector, escalationTargets);
|
|
110
121
|
} else {
|
|
111
122
|
for (const pid of escalationTargets) {
|
|
112
123
|
try {
|
|
@@ -63,6 +63,19 @@ export function createSessionWorkerSupervisor({
|
|
|
63
63
|
maxRestartRetries = 5,
|
|
64
64
|
restartBackoffBaseMs = 1_000,
|
|
65
65
|
restartBackoffMaxMs = 30_000,
|
|
66
|
+
// Exhausted is not for ever: what failed the launches can be gone — a
|
|
67
|
+
// process list the relay could not read for an hour was (2026-10-01) — and
|
|
68
|
+
// the pending work would otherwise wait for a hand-made launch or a relay
|
|
69
|
+
// restart. One more launch is allowed this long after the last failure; a
|
|
70
|
+
// failure exhausts the session again at once. 0 keeps it exhausted.
|
|
71
|
+
restartExhaustedRetryAfterMs = 60_000,
|
|
72
|
+
// ...and not for longer than this after the first exhaustion: then the
|
|
73
|
+
// session stays exhausted until a launch is asked for (the Retry button,
|
|
74
|
+
// the launch or kill route, a relay restart). 0 retries without end.
|
|
75
|
+
restartExhaustedRetryWindowMs = 10 * 60_000,
|
|
76
|
+
// Told about launch episodes, for the note in the conversation:
|
|
77
|
+
// ({ type: 'exhausted' | 'retry-failed' | 'stopped' | 'launched', sdkSessionId, lifecycle }).
|
|
78
|
+
onLaunchState = null,
|
|
66
79
|
idleEvictionMs = 0,
|
|
67
80
|
// Copilot-kind workers boot a full CLI (auth, model catalog, workspace scan)
|
|
68
81
|
// before their extension can heartbeat; measured median is ~35s. A 30s
|
|
@@ -88,6 +101,8 @@ export function createSessionWorkerSupervisor({
|
|
|
88
101
|
const retryLimit = Math.max(0, clampInt(maxRestartRetries, 5));
|
|
89
102
|
const backoffBaseMs = Math.max(1, clampInt(restartBackoffBaseMs, 1_000));
|
|
90
103
|
const backoffMaxMs = Math.max(backoffBaseMs, clampInt(restartBackoffMaxMs, 30_000));
|
|
104
|
+
const exhaustedRetryAfterMs = Math.max(0, clampInt(restartExhaustedRetryAfterMs, 60_000));
|
|
105
|
+
const exhaustedRetryWindowMs = Math.max(0, clampInt(restartExhaustedRetryWindowMs, 10 * 60_000));
|
|
91
106
|
const idleTimeoutMs = clampInt(idleEvictionMs, 0);
|
|
92
107
|
const heartbeatStaleAfterMs = Math.max(1_000, clampInt(heartbeatTimeoutMs, 90_000));
|
|
93
108
|
const recoveryGraceMs = Math.max(0, clampInt(degradedRecoveryGraceMs, 10_000));
|
|
@@ -129,6 +144,10 @@ export function createSessionWorkerSupervisor({
|
|
|
129
144
|
backoffMs: 0,
|
|
130
145
|
nextRestartAtMs: null,
|
|
131
146
|
restartExhausted: false,
|
|
147
|
+
// The launch episode: when the launches were first exhausted, and
|
|
148
|
+
// whether the relay has stopped trying again by itself.
|
|
149
|
+
exhaustedSinceMs: null,
|
|
150
|
+
retriesStopped: false,
|
|
132
151
|
lastError: null,
|
|
133
152
|
lastFailureAtMs: null,
|
|
134
153
|
lastActivityAtMs: nowAtMs,
|
|
@@ -167,6 +186,11 @@ export function createSessionWorkerSupervisor({
|
|
|
167
186
|
backoffMs: clampInt(lifecycle.backoffMs, 0),
|
|
168
187
|
nextRestartAt: lifecycle.nextRestartAtMs ? new Date(lifecycle.nextRestartAtMs).toISOString() : null,
|
|
169
188
|
restartExhausted: Boolean(lifecycle.restartExhausted),
|
|
189
|
+
exhaustedSince: lifecycle.exhaustedSinceMs ? new Date(lifecycle.exhaustedSinceMs).toISOString() : null,
|
|
190
|
+
retriesStopped: Boolean(lifecycle.retriesStopped),
|
|
191
|
+
retryAgainAt: lifecycle.restartExhausted && !lifecycle.retriesStopped && lifecycle.lastFailureAtMs && exhaustedRetryAfterMs > 0
|
|
192
|
+
? new Date(lifecycle.lastFailureAtMs + exhaustedRetryAfterMs).toISOString()
|
|
193
|
+
: null,
|
|
170
194
|
lastError: lifecycle.lastError || null,
|
|
171
195
|
lastFailureAt: lifecycle.lastFailureAtMs ? new Date(lifecycle.lastFailureAtMs).toISOString() : null,
|
|
172
196
|
lastActivityAt: lifecycle.lastActivityAtMs ? new Date(lifecycle.lastActivityAtMs).toISOString() : null,
|
|
@@ -189,6 +213,16 @@ export function createSessionWorkerSupervisor({
|
|
|
189
213
|
};
|
|
190
214
|
}
|
|
191
215
|
|
|
216
|
+
// The caller's listener must not break the supervisor.
|
|
217
|
+
function notifyLaunchState(type, sessionId) {
|
|
218
|
+
if (typeof onLaunchState !== 'function') return;
|
|
219
|
+
try {
|
|
220
|
+
onLaunchState({ type, sdkSessionId: sessionId, lifecycle: toLifecycleSnapshot(sessionId) });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
try { log?.(`launch state listener failed (${type}, session=${String(sessionId).slice(0, 8)}): ${error?.message || error}`); } catch {}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
192
226
|
function touchActivity(sessionId, atMs = nowMs()) {
|
|
193
227
|
return setLifecycle(sessionId, { lastActivityAtMs: atMs });
|
|
194
228
|
}
|
|
@@ -394,16 +428,26 @@ export function createSessionWorkerSupervisor({
|
|
|
394
428
|
const failedAtMs = nowMs();
|
|
395
429
|
|
|
396
430
|
if (nextRetryCount > retryLimit) {
|
|
431
|
+
const firstExhaustion = !lifecycle.restartExhausted;
|
|
432
|
+
const exhaustedSinceMs = lifecycle.exhaustedSinceMs || failedAtMs;
|
|
433
|
+
const retriesStopped = exhaustedRetryAfterMs === 0
|
|
434
|
+
|| (exhaustedRetryWindowMs > 0 && failedAtMs - exhaustedSinceMs >= exhaustedRetryWindowMs);
|
|
397
435
|
const exhausted = setLifecycle(sessionId, {
|
|
398
436
|
retryCount: nextRetryCount,
|
|
399
437
|
backoffMs: 0,
|
|
400
438
|
nextRestartAtMs: null,
|
|
401
439
|
restartExhausted: true,
|
|
440
|
+
exhaustedSinceMs,
|
|
441
|
+
retriesStopped,
|
|
402
442
|
lastError: message,
|
|
403
443
|
lastFailureAtMs: failedAtMs,
|
|
404
444
|
lastActivityAtMs: failedAtMs,
|
|
405
445
|
});
|
|
406
446
|
registerDegradedState(sessionId, 'restart-exhausted', { atMs: failedAtMs });
|
|
447
|
+
notifyLaunchState(
|
|
448
|
+
firstExhaustion ? 'exhausted' : (retriesStopped && !lifecycle.retriesStopped ? 'stopped' : 'retry-failed'),
|
|
449
|
+
sessionId,
|
|
450
|
+
);
|
|
407
451
|
return { scheduled: false, exhausted: true, lifecycle: toLifecycleSnapshot(sessionId, exhausted) };
|
|
408
452
|
}
|
|
409
453
|
|
|
@@ -432,6 +476,8 @@ export function createSessionWorkerSupervisor({
|
|
|
432
476
|
backoffMs: 0,
|
|
433
477
|
nextRestartAtMs: null,
|
|
434
478
|
restartExhausted: false,
|
|
479
|
+
exhaustedSinceMs: null,
|
|
480
|
+
retriesStopped: false,
|
|
435
481
|
lastError: null,
|
|
436
482
|
lastFailureAtMs: null,
|
|
437
483
|
lastActivityAtMs: nowAtMs,
|
|
@@ -454,7 +500,13 @@ export function createSessionWorkerSupervisor({
|
|
|
454
500
|
const lifecycle = lifecycleBySession.get(sessionId);
|
|
455
501
|
if (!lifecycle) return { allowed: true, reason: null, lifecycle: null };
|
|
456
502
|
if (lifecycle.restartExhausted) {
|
|
457
|
-
|
|
503
|
+
const failedAtMs = Number(lifecycle.lastFailureAtMs || 0);
|
|
504
|
+
const dueAgain = !lifecycle.retriesStopped
|
|
505
|
+
&& exhaustedRetryAfterMs > 0 && failedAtMs > 0 && nowMs() - failedAtMs >= exhaustedRetryAfterMs;
|
|
506
|
+
if (!dueAgain) {
|
|
507
|
+
return { allowed: false, reason: 'restart-exhausted', lifecycle: toLifecycleSnapshot(sessionId, lifecycle) };
|
|
508
|
+
}
|
|
509
|
+
return { allowed: true, reason: null, lifecycle: toLifecycleSnapshot(sessionId, lifecycle) };
|
|
458
510
|
}
|
|
459
511
|
if (lifecycle.nextRestartAtMs && nowMs() < lifecycle.nextRestartAtMs) {
|
|
460
512
|
return { allowed: false, reason: 'restart-delayed', lifecycle: toLifecycleSnapshot(sessionId, lifecycle) };
|
|
@@ -545,6 +597,7 @@ export function createSessionWorkerSupervisor({
|
|
|
545
597
|
// backoff/exhausted lifecycle state was left behind by earlier failures.
|
|
546
598
|
clearRestartSchedule(sessionId, { resetDegradedState: false });
|
|
547
599
|
assessSessionHealth(sessionId, reusedWorker, { nowAtMs, questionPending: false });
|
|
600
|
+
notifyLaunchState('launched', sessionId);
|
|
548
601
|
return { ok: true, reused: true, worker: reusedWorker, lifecycle: toLifecycleSnapshot(sessionId) };
|
|
549
602
|
}
|
|
550
603
|
|
|
@@ -609,6 +662,7 @@ export function createSessionWorkerSupervisor({
|
|
|
609
662
|
});
|
|
610
663
|
clearRestartSchedule(sessionId);
|
|
611
664
|
assessSessionHealth(sessionId, readyState, { questionPending: false });
|
|
665
|
+
notifyLaunchState('launched', sessionId);
|
|
612
666
|
return { ok: true, reused: false, worker: readyState, lifecycle: toLifecycleSnapshot(sessionId) };
|
|
613
667
|
}
|
|
614
668
|
const simulatedReady = setWorkerState(sessionId, {
|
|
@@ -625,6 +679,7 @@ export function createSessionWorkerSupervisor({
|
|
|
625
679
|
});
|
|
626
680
|
clearRestartSchedule(sessionId);
|
|
627
681
|
assessSessionHealth(sessionId, simulatedReady, { questionPending: false });
|
|
682
|
+
notifyLaunchState('launched', sessionId);
|
|
628
683
|
return { ok: true, reused: false, worker: simulatedReady, lifecycle: toLifecycleSnapshot(sessionId) };
|
|
629
684
|
} catch (error) {
|
|
630
685
|
const scheduled = scheduleRestart(sessionId, error, { incrementRetry: true });
|
|
@@ -778,9 +833,41 @@ export function createSessionWorkerSupervisor({
|
|
|
778
833
|
return evicted;
|
|
779
834
|
}
|
|
780
835
|
|
|
836
|
+
/**
|
|
837
|
+
* An idle worker whose process is gone (killed from outside, crashed
|
|
838
|
+
* between turns) leaves the registry, so it is not listed as ready with a
|
|
839
|
+
* pid that no longer exists; the next message starts a new one either way.
|
|
840
|
+
* A worker that is processing, or that a question card waits on, is left
|
|
841
|
+
* to the stale-pid handling, which knows about the work that is open. A
|
|
842
|
+
* heartbeat inside the stale window outranks the pid: the pid on record
|
|
843
|
+
* can be a launcher's. A worker that was stopped from outside therefore
|
|
844
|
+
* stays listed until that window has passed (90 s by default); one that
|
|
845
|
+
* the relay's own kill route stopped leaves the list at once.
|
|
846
|
+
*/
|
|
847
|
+
function retireDeadIdleWorkers({ keep = new Set() } = {}) {
|
|
848
|
+
const retired = [];
|
|
849
|
+
const heartbeatCutoff = nowMs() - heartbeatStaleAfterMs;
|
|
850
|
+
for (const worker of registry.listWorkers()) {
|
|
851
|
+
const sessionId = normalizeSessionId(worker.sdkSessionId);
|
|
852
|
+
if (!sessionId || worker.status !== 'ready') continue;
|
|
853
|
+
if (pendingStarts.has(sessionId) || keep.has(sessionId)) continue;
|
|
854
|
+
const pidProbe = checkPidLiveness(worker);
|
|
855
|
+
if (!pidProbe.checked || pidProbe.alive) continue;
|
|
856
|
+
const lifecycle = getOrCreateLifecycle(sessionId);
|
|
857
|
+
if (lifecycle.awaitingHeartbeat) continue;
|
|
858
|
+
if (lifecycle.lastHeartbeatAtMs && lifecycle.lastHeartbeatAtMs > heartbeatCutoff) continue;
|
|
859
|
+
if (!registry.removeWorker(sessionId)) continue;
|
|
860
|
+
lifecycleBySession.delete(sessionId);
|
|
861
|
+
emitMonitorLog(`worker ${worker.workerId || sessionId} was idle and its process (pid ${worker.pid}) is gone; no longer listed`);
|
|
862
|
+
retired.push({ sdkSessionId: sessionId, workerId: worker.workerId || null, pid: worker.pid });
|
|
863
|
+
}
|
|
864
|
+
return retired;
|
|
865
|
+
}
|
|
866
|
+
|
|
781
867
|
function snapshot({ pendingQuestionSessionIds = null } = {}) {
|
|
782
|
-
const workers = registry.listWorkers();
|
|
783
868
|
const questionPendingSet = normalizePendingQuestionSessionIds(pendingQuestionSessionIds);
|
|
869
|
+
retireDeadIdleWorkers({ keep: questionPendingSet });
|
|
870
|
+
const workers = registry.listWorkers();
|
|
784
871
|
const workerBySession = new Map();
|
|
785
872
|
for (const worker of workers) {
|
|
786
873
|
const sid = normalizeSessionId(worker?.sdkSessionId);
|