@oar-sh/oar 0.9.4 → 0.9.5
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/CHANGELOG.md +31 -0
- package/README.md +17 -4
- package/package.json +1 -1
- package/server/claude-worker/claude-remote-relay-tool.mjs +1 -0
- package/server/claude-worker/claude-session-process.mjs +293 -18
- package/server/claude-worker/claude-session-worker.mjs +5 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +5 -0
- package/server/cursor-worker/cursor-session-worker.mjs +5 -0
- package/server/grok-worker/grok-session-worker.mjs +5 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/steered-turn-merge.mjs +10 -1
- package/server/public/index.html +7 -3
- package/server/routes/messages-routes.mjs +8 -3
- package/server/routes/sessions-routes.mjs +4 -2
- package/server/services/remote-relay-caller-context.mjs +8 -3
- package/server/services/remote-relay-dispatcher.mjs +256 -13
- package/server/services/session-worker-launch-service.mjs +17 -2
- package/shared/remote-relay-contract.mjs +17 -0
- package/shared/steer-settle-failure.mjs +6 -0
- package/shared/steer-settle-markers.mjs +16 -0
- package/shared/worker-runtime/worker-log-file.mjs +97 -0
|
@@ -16,3 +16,19 @@ export const STEER_FOLDED_TEXT = '_(Handled together with the previous reply —
|
|
|
16
16
|
* kind='stopped' — the stable handle the client keys its Resend on.
|
|
17
17
|
*/
|
|
18
18
|
export const STEER_STOPPED_TEXT = '_(Stopped with the turn — not answered.)_';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Not a steer: a message delivered between turns whose answer the worker
|
|
22
|
+
* published on a background-continuation row, because it took the turn for one
|
|
23
|
+
* the CLI had opened by itself. Stamped with a kind of its own: it is a
|
|
24
|
+
* marker, not a reply (no completion push, muted in the transcript, never
|
|
25
|
+
* handed to a calling relay as the answer), and not a steer marker either,
|
|
26
|
+
* which would label the message "steered mid-turn".
|
|
27
|
+
*
|
|
28
|
+
* The wording names the reply by its badge, not by its position: a live
|
|
29
|
+
* append puts the marker below that reply, a reload anchors it under its own
|
|
30
|
+
* message and so above it. The worker infers all this from silence, hence the
|
|
31
|
+
* way out at the end.
|
|
32
|
+
*/
|
|
33
|
+
export const ANSWERED_ELSEWHERE_KIND = 'answered-elsewhere';
|
|
34
|
+
export const DELIVERY_ANSWERED_ELSEWHERE_TEXT = '_(Answered in the reply marked “background continuation” next to this message. Resend the message if that reply does not answer it.)_';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The worker log for launches that cannot redirect the worker's output.
|
|
2
|
+
//
|
|
3
|
+
// On Linux the launcher redirects a Node worker's stdout/stderr into
|
|
4
|
+
// `<log dir>/worker-<session id>.log` (`>> file 2>&1` under tmux, an inherited
|
|
5
|
+
// descriptor on the detached path). On Windows the worker runs in its own
|
|
6
|
+
// console window behind `cmd /c start`: no descriptor of the launcher reaches
|
|
7
|
+
// it, and a shell redirect would empty the window. So the launcher names the
|
|
8
|
+
// file in `COPILOT_WEB_RELAY_WORKER_LOG_FILE` and the worker copies what it
|
|
9
|
+
// writes to stdout/stderr into it, while the console keeps showing the same
|
|
10
|
+
// lines.
|
|
11
|
+
//
|
|
12
|
+
// Out of reach from inside the process: what Node prints natively (a V8 fatal
|
|
13
|
+
// error such as heap exhaustion, a module that fails to load before this is
|
|
14
|
+
// installed) and what a child process writes to an inherited console.
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
|
|
17
|
+
export const WORKER_LOG_FILE_ENV = 'COPILOT_WEB_RELAY_WORKER_LOG_FILE';
|
|
18
|
+
|
|
19
|
+
// Colours are for the console; the Linux file never has them, because there
|
|
20
|
+
// stdout is not a terminal.
|
|
21
|
+
const ANSI_STYLE_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Copies stdout/stderr into the log file the launcher named. Returns null when
|
|
25
|
+
* no file was named or it cannot be opened: best-effort like the launcher's
|
|
26
|
+
* side, a log problem must never stop a worker.
|
|
27
|
+
*/
|
|
28
|
+
export function installWorkerLogFile({
|
|
29
|
+
env = process.env,
|
|
30
|
+
processImpl = process,
|
|
31
|
+
fsImpl = fs,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const logPath = String(env?.[WORKER_LOG_FILE_ENV] || '').trim();
|
|
34
|
+
// Not inherited: the provider CLI, the agent's shells and anything started
|
|
35
|
+
// from them must not write into this worker's log.
|
|
36
|
+
if (env && WORKER_LOG_FILE_ENV in env) delete env[WORKER_LOG_FILE_ENV];
|
|
37
|
+
if (!logPath) return null;
|
|
38
|
+
|
|
39
|
+
let fd = null;
|
|
40
|
+
try {
|
|
41
|
+
fd = fsImpl.openSync(logPath, 'a');
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Synchronous on purpose: the last lines before a process.exit() are the
|
|
47
|
+
// ones an incident needs.
|
|
48
|
+
const append = (chunk, encoding) => {
|
|
49
|
+
if (fd === null) return;
|
|
50
|
+
try {
|
|
51
|
+
if (typeof chunk === 'string') {
|
|
52
|
+
const text = chunk.replace(ANSI_STYLE_PATTERN, '');
|
|
53
|
+
if (text) fsImpl.writeSync(fd, text, null, typeof encoding === 'string' ? encoding : 'utf8');
|
|
54
|
+
} else if (chunk instanceof Uint8Array) {
|
|
55
|
+
fsImpl.writeSync(fd, chunk);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// A file that stopped taking writes (disk full, handle gone) is given up.
|
|
59
|
+
try { fsImpl.closeSync(fd); } catch {}
|
|
60
|
+
fd = null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const restores = [];
|
|
65
|
+
for (const stream of [processImpl.stdout, processImpl.stderr]) {
|
|
66
|
+
if (!stream || typeof stream.write !== 'function') continue;
|
|
67
|
+
const originalWrite = stream.write;
|
|
68
|
+
const teeWrite = function teeWrite(chunk, encoding, callback) {
|
|
69
|
+
append(chunk, encoding);
|
|
70
|
+
return originalWrite.call(this, chunk, encoding, callback);
|
|
71
|
+
};
|
|
72
|
+
stream.write = teeWrite;
|
|
73
|
+
restores.push(() => {
|
|
74
|
+
if (stream.write === teeWrite) stream.write = originalWrite;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Without an uncaughtException handler Node prints the error natively, past
|
|
79
|
+
// the streams above. With one (the worker crash guard) the handler logs it.
|
|
80
|
+
const onUncaught = (error, origin) => {
|
|
81
|
+
if (processImpl.listenerCount('uncaughtException') > 0) return;
|
|
82
|
+
append(`${origin || 'uncaughtException'}: ${error?.stack || error?.message || error}\n`);
|
|
83
|
+
};
|
|
84
|
+
processImpl.on?.('uncaughtExceptionMonitor', onUncaught);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
logPath,
|
|
88
|
+
uninstall() {
|
|
89
|
+
for (const restore of restores) restore();
|
|
90
|
+
processImpl.off?.('uncaughtExceptionMonitor', onUncaught);
|
|
91
|
+
if (fd !== null) {
|
|
92
|
+
try { fsImpl.closeSync(fd); } catch {}
|
|
93
|
+
fd = null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|