@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.
@@ -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
+ }