@oar-sh/oar 0.9.5 → 0.9.6
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 +168 -0
- package/README.md +20 -4
- package/package.json +2 -1
- package/server/claude-worker/claude-session-process.mjs +118 -1
- 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 +7 -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/public/app/api-client.js +17 -2
- package/server/public/app/bootstrap.js +3 -0
- package/server/public/app/composer-control-state.mjs +4 -3
- package/server/public/app/conversation-view.js +95 -4
- package/server/public/app/journal-view.js +6 -0
- package/server/public/app/markdown-strikethrough.mjs +31 -0
- 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 +12 -3
- package/server/public/app/socket-handlers.js +9 -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 +90 -1
- package/server/repositories/message-repository.mjs +10 -1
- package/server/repositories/question-repository.mjs +1 -0
- package/server/repositories/remote-relay-repository.mjs +30 -0
- package/server/routes/messages-routes.mjs +85 -14
- package/server/routes/relay-board-routes.mjs +56 -2
- package/server/routes/sessions-routes.mjs +36 -0
- package/server/server-runtime.mjs +33 -2
- package/server/services/preview-proxy-server.mjs +64 -36
- package/server/services/process-list-service.mjs +122 -0
- package/server/services/relay-console-log.mjs +199 -0
- package/server/services/remote-relay-caller-context.mjs +37 -17
- package/server/services/remote-relay-dispatcher.mjs +72 -8
- package/server/services/remote-relay-inbound.mjs +35 -0
- package/server/services/session-worker-process-service.mjs +85 -32
- package/server/services/session-worker-registry-service.mjs +1 -1
- package/server/services/session-worker-supervisor-service.mjs +33 -1
- package/server/services/usage-limit-pause-service.mjs +441 -0
- package/shared/ask-user-bridge.mjs +14 -39
- 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/question-wait.mjs +108 -0
- package/shared/remote-relay-contract.mjs +16 -2
- package/shared/steer-settle-failure.mjs +16 -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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { execFileSync } from 'child_process';
|
|
3
|
+
import { execFile, execFileSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
import { collectProcessDescendants, isChildProcessOf } from '../../shared/worker-runtime/process-tree.mjs';
|
|
4
6
|
|
|
5
7
|
function normalizeSessionId(value) {
|
|
6
8
|
const text = String(value || '').trim();
|
|
@@ -51,9 +53,39 @@ function normalizeWindowsProcess(proc) {
|
|
|
51
53
|
parentProcessId: Number.isInteger(Number(proc?.parentProcessId)) ? Number(proc.parentProcessId) : null,
|
|
52
54
|
name: String(proc?.name || ''),
|
|
53
55
|
commandLine: String(proc?.commandLine || ''),
|
|
56
|
+
createdAt: Number(proc?.createdAt) > 0 ? Number(proc.createdAt) : 0,
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
// The relay's own session workers: `node <kind>-session-worker.mjs --session-id <id>`.
|
|
61
|
+
function isRelaySessionWorker(proc) {
|
|
62
|
+
return String(proc?.commandLine || '').toLowerCase().includes('-session-worker');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Every process with its parent and the time it was created.
|
|
66
|
+
const WINDOWS_PROCESS_SNAPSHOT_SCRIPT = [
|
|
67
|
+
'$list = Get-CimInstance Win32_Process | ForEach-Object {',
|
|
68
|
+
' [pscustomobject]@{',
|
|
69
|
+
' processId = [int]$_.ProcessId;',
|
|
70
|
+
' parentProcessId = [int]$_.ParentProcessId;',
|
|
71
|
+
' name = [string]$_.Name;',
|
|
72
|
+
' commandLine = [string]$_.CommandLine;',
|
|
73
|
+
' createdAt = $(if ($_.CreationDate) { [int64](($_.CreationDate.ToUniversalTime() - [datetime]"1970-01-01").TotalMilliseconds) } else { [int64]0 });',
|
|
74
|
+
' }',
|
|
75
|
+
'};',
|
|
76
|
+
'$list | ConvertTo-Json -Depth 3 -Compress',
|
|
77
|
+
].join(' ');
|
|
78
|
+
|
|
79
|
+
/** How long the process list may take when a worker waits for it. */
|
|
80
|
+
const WINDOWS_PROCESS_SNAPSHOT_TIMEOUT_MS = 10_000;
|
|
81
|
+
|
|
82
|
+
function parseWindowsProcessSnapshot(output) {
|
|
83
|
+
const text = String(output || '').trim();
|
|
84
|
+
if (!text) return [];
|
|
85
|
+
const parsed = JSON.parse(text);
|
|
86
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
function looksLikeCopilotWorkerProcess(proc) {
|
|
58
90
|
const name = String(proc?.name || '').trim().toLowerCase();
|
|
59
91
|
const cmd = String(proc?.commandLine || '').trim().toLowerCase();
|
|
@@ -94,6 +126,7 @@ function looksLikeCopilotWorkerProcess(proc) {
|
|
|
94
126
|
export function createSessionWorkerProcessInspector({
|
|
95
127
|
platform = process.platform,
|
|
96
128
|
execFileSyncImpl = execFileSync,
|
|
129
|
+
execFileImpl = execFile,
|
|
97
130
|
} = {}) {
|
|
98
131
|
function getPosixProcessSnapshot() {
|
|
99
132
|
if (platform === 'win32') return [];
|
|
@@ -119,24 +152,37 @@ export function createSessionWorkerProcessInspector({
|
|
|
119
152
|
|
|
120
153
|
function getWindowsProcessSnapshot() {
|
|
121
154
|
if (platform !== 'win32') return [];
|
|
122
|
-
const
|
|
123
|
-
'$list = Get-CimInstance Win32_Process | ForEach-Object {',
|
|
124
|
-
' [pscustomobject]@{',
|
|
125
|
-
' processId = [int]$_.ProcessId;',
|
|
126
|
-
' parentProcessId = [int]$_.ParentProcessId;',
|
|
127
|
-
' name = [string]$_.Name;',
|
|
128
|
-
' commandLine = [string]$_.CommandLine;',
|
|
129
|
-
' }',
|
|
130
|
-
'};',
|
|
131
|
-
'$list | ConvertTo-Json -Depth 3 -Compress',
|
|
132
|
-
].join(' ');
|
|
133
|
-
const output = execFileSyncImpl('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
155
|
+
const output = execFileSyncImpl('powershell.exe', ['-NoProfile', '-Command', WINDOWS_PROCESS_SNAPSHOT_SCRIPT], {
|
|
134
156
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
135
157
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
return parseWindowsProcessSnapshot(output);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The same list without holding the caller's thread for it: a session
|
|
163
|
+
* worker reads it while it serves a conversation, and PowerShell takes about
|
|
164
|
+
* a second to start. Rejects when the list could not be read.
|
|
165
|
+
*/
|
|
166
|
+
function readWindowsProcessSnapshot() {
|
|
167
|
+
if (platform !== 'win32') return Promise.resolve([]);
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
execFileImpl(
|
|
170
|
+
'powershell.exe',
|
|
171
|
+
['-NoProfile', '-Command', WINDOWS_PROCESS_SNAPSHOT_SCRIPT],
|
|
172
|
+
{ windowsHide: true, timeout: WINDOWS_PROCESS_SNAPSHOT_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024 },
|
|
173
|
+
(error, stdout) => {
|
|
174
|
+
if (error) {
|
|
175
|
+
reject(error);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
resolve(parseWindowsProcessSnapshot(stdout).map(normalizeWindowsProcess).filter((proc) => proc.processId));
|
|
180
|
+
} catch (parseError) {
|
|
181
|
+
reject(parseError);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
});
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
function isWindowsSessionMatch(proc, target, targetPattern) {
|
|
@@ -175,24 +221,20 @@ export function createSessionWorkerProcessInspector({
|
|
|
175
221
|
.map(normalizeWindowsProcess)
|
|
176
222
|
.filter((proc) => proc.processId);
|
|
177
223
|
const byPid = new Map(snapshot.map((proc) => [proc.processId, proc]));
|
|
178
|
-
const childrenByParent = new Map();
|
|
179
|
-
for (const proc of snapshot) {
|
|
180
|
-
if (!proc.parentProcessId) continue;
|
|
181
|
-
const children = childrenByParent.get(proc.parentProcessId) || [];
|
|
182
|
-
children.push(proc);
|
|
183
|
-
childrenByParent.set(proc.parentProcessId, children);
|
|
184
|
-
}
|
|
185
224
|
|
|
186
225
|
const related = new Map();
|
|
187
226
|
const addProcess = (proc) => {
|
|
188
227
|
if (proc?.processId) related.set(proc.processId, proc);
|
|
189
228
|
};
|
|
190
|
-
const addDescendants = (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
229
|
+
const addDescendants = (proc) => {
|
|
230
|
+
const found = collectProcessDescendants(snapshot, proc, {
|
|
231
|
+
accept: (child) => {
|
|
232
|
+
if (related.has(child.processId)) return false;
|
|
233
|
+
// The worker of another session is never part of this one's tree.
|
|
234
|
+
return !(isRelaySessionWorker(child) && !isWindowsSessionMatch(child, target, targetPattern));
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
for (const child of found) addProcess(child);
|
|
196
238
|
};
|
|
197
239
|
const addWrapperAncestors = (proc) => {
|
|
198
240
|
let current = proc;
|
|
@@ -201,6 +243,8 @@ export function createSessionWorkerProcessInspector({
|
|
|
201
243
|
seen.add(current.parentProcessId);
|
|
202
244
|
const parent = byPid.get(current.parentProcessId);
|
|
203
245
|
if (!parent || !isWindowsWrapperProcess(parent)) return;
|
|
246
|
+
// Windows hands a pid out again: see `isChildProcessOf`.
|
|
247
|
+
if (!isChildProcessOf(current, parent)) return;
|
|
204
248
|
addProcess(parent);
|
|
205
249
|
current = parent;
|
|
206
250
|
}
|
|
@@ -208,7 +252,7 @@ export function createSessionWorkerProcessInspector({
|
|
|
208
252
|
|
|
209
253
|
for (const proc of snapshot.filter((candidate) => isWindowsSessionMatch(candidate, target, targetPattern))) {
|
|
210
254
|
addProcess(proc);
|
|
211
|
-
addDescendants(proc
|
|
255
|
+
addDescendants(proc);
|
|
212
256
|
addWrapperAncestors(proc);
|
|
213
257
|
}
|
|
214
258
|
|
|
@@ -259,18 +303,26 @@ export function createSessionWorkerProcessInspector({
|
|
|
259
303
|
'$ErrorActionPreference = "Continue"',
|
|
260
304
|
'$ids = @(' + ids.join(',') + ')',
|
|
261
305
|
'try {',
|
|
262
|
-
|
|
306
|
+
// A child is a process started after its parent: the pid of an ended
|
|
307
|
+
// process is handed out again, and the worker of another session must
|
|
308
|
+
// not be taken for a child because of it.
|
|
309
|
+
' $snapshot = @(Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ processId = [int]$_.ProcessId; parentProcessId = [int]$_.ParentProcessId; createdAt = $(if ($_.CreationDate) { [int64](($_.CreationDate.ToUniversalTime() - [datetime]"1970-01-01").TotalMilliseconds) } else { [int64]0 }); worker = ([string]$_.CommandLine -like "*-session-worker*") } })',
|
|
263
310
|
'} catch {',
|
|
264
311
|
' Write-Error $_',
|
|
265
312
|
' exit 2',
|
|
266
313
|
'}',
|
|
267
314
|
'$targets = [System.Collections.Generic.HashSet[int]]::new()',
|
|
268
315
|
'foreach ($id in $ids) { [void]$targets.Add([int]$id) }',
|
|
316
|
+
'$createdAt = @{}',
|
|
317
|
+
'foreach ($proc in $snapshot) { $createdAt[[int]$proc.processId] = [int64]$proc.createdAt }',
|
|
269
318
|
'$changed = $true',
|
|
270
319
|
'while ($changed) {',
|
|
271
320
|
' $changed = $false',
|
|
272
321
|
' foreach ($proc in $snapshot) {',
|
|
273
322
|
' if ($targets.Contains([int]$proc.parentProcessId) -and -not $targets.Contains([int]$proc.processId)) {',
|
|
323
|
+
' if ($proc.worker) { continue }',
|
|
324
|
+
' $parentCreatedAt = [int64]$createdAt[[int]$proc.parentProcessId]',
|
|
325
|
+
' if ($parentCreatedAt -gt 0 -and [int64]$proc.createdAt -gt 0 -and [int64]$proc.createdAt -lt $parentCreatedAt) { continue }',
|
|
274
326
|
' [void]$targets.Add([int]$proc.processId)',
|
|
275
327
|
' $changed = $true',
|
|
276
328
|
' }',
|
|
@@ -314,6 +366,7 @@ export function createSessionWorkerProcessInspector({
|
|
|
314
366
|
findWindowsProcessForSession,
|
|
315
367
|
findWindowsProcessTreeForSession,
|
|
316
368
|
getWindowsProcessSnapshot,
|
|
369
|
+
readWindowsProcessSnapshot,
|
|
317
370
|
stopWindowsPids,
|
|
318
371
|
};
|
|
319
372
|
}
|
|
@@ -60,7 +60,7 @@ function normalizeStatus(value) {
|
|
|
60
60
|
return ALLOWED_STATUSES.has(mapped) ? mapped : 'new';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const STEERING_HOLD_REASONS = new Set(['question', 'compaction', 'adoption', 'delivery', 'other']);
|
|
63
|
+
const STEERING_HOLD_REASONS = new Set(['question', 'compaction', 'adoption', 'delivery', 'tool', 'other']);
|
|
64
64
|
const STEERING_MESSAGE_ID_MAX_LENGTH = 64;
|
|
65
65
|
const STEERING_CANCELLABLE_IDS_MAX = 50;
|
|
66
66
|
|
|
@@ -778,9 +778,41 @@ export function createSessionWorkerSupervisor({
|
|
|
778
778
|
return evicted;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
/**
|
|
782
|
+
* An idle worker whose process is gone (killed from outside, crashed
|
|
783
|
+
* between turns) leaves the registry, so it is not listed as ready with a
|
|
784
|
+
* pid that no longer exists; the next message starts a new one either way.
|
|
785
|
+
* A worker that is processing, or that a question card waits on, is left
|
|
786
|
+
* to the stale-pid handling, which knows about the work that is open. A
|
|
787
|
+
* heartbeat inside the stale window outranks the pid: the pid on record
|
|
788
|
+
* can be a launcher's. A worker that was stopped from outside therefore
|
|
789
|
+
* stays listed until that window has passed (90 s by default); one that
|
|
790
|
+
* the relay's own kill route stopped leaves the list at once.
|
|
791
|
+
*/
|
|
792
|
+
function retireDeadIdleWorkers({ keep = new Set() } = {}) {
|
|
793
|
+
const retired = [];
|
|
794
|
+
const heartbeatCutoff = nowMs() - heartbeatStaleAfterMs;
|
|
795
|
+
for (const worker of registry.listWorkers()) {
|
|
796
|
+
const sessionId = normalizeSessionId(worker.sdkSessionId);
|
|
797
|
+
if (!sessionId || worker.status !== 'ready') continue;
|
|
798
|
+
if (pendingStarts.has(sessionId) || keep.has(sessionId)) continue;
|
|
799
|
+
const pidProbe = checkPidLiveness(worker);
|
|
800
|
+
if (!pidProbe.checked || pidProbe.alive) continue;
|
|
801
|
+
const lifecycle = getOrCreateLifecycle(sessionId);
|
|
802
|
+
if (lifecycle.awaitingHeartbeat) continue;
|
|
803
|
+
if (lifecycle.lastHeartbeatAtMs && lifecycle.lastHeartbeatAtMs > heartbeatCutoff) continue;
|
|
804
|
+
if (!registry.removeWorker(sessionId)) continue;
|
|
805
|
+
lifecycleBySession.delete(sessionId);
|
|
806
|
+
emitMonitorLog(`worker ${worker.workerId || sessionId} was idle and its process (pid ${worker.pid}) is gone; no longer listed`);
|
|
807
|
+
retired.push({ sdkSessionId: sessionId, workerId: worker.workerId || null, pid: worker.pid });
|
|
808
|
+
}
|
|
809
|
+
return retired;
|
|
810
|
+
}
|
|
811
|
+
|
|
781
812
|
function snapshot({ pendingQuestionSessionIds = null } = {}) {
|
|
782
|
-
const workers = registry.listWorkers();
|
|
783
813
|
const questionPendingSet = normalizePendingQuestionSessionIds(pendingQuestionSessionIds);
|
|
814
|
+
retireDeadIdleWorkers({ keep: questionPendingSet });
|
|
815
|
+
const workers = registry.listWorkers();
|
|
784
816
|
const workerBySession = new Map();
|
|
785
817
|
for (const worker of workers) {
|
|
786
818
|
const sid = normalizeSessionId(worker?.sdkSessionId);
|