shraga 0.1.99 → 0.1.101

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.
@@ -54,7 +54,7 @@ export function writeRuntimeDirective(text: string, sel: RuntimeSelection): stri
54
54
  }
55
55
  }
56
56
  // An engine the chosen model already implies is not written: `[model:cursor/composer-2.5]` and
57
- // `[engine:agentx,model:cursor/composer-2.5]` mean the same thing to the parser, and emitting the
57
+ // `[engine:ext-agent,model:cursor/composer-2.5]` mean the same thing to the parser, and emitting the
58
58
  // longer one would stamp a pin the user never chose onto every schedule that carries a model-only
59
59
  // directive (11 of the 15 live ones) the moment the picker is touched.
60
60
  const impliedEngine = sel.model ? resolveModelAlias(sel.model)?.engine : undefined;
@@ -6,10 +6,10 @@
6
6
  // polling is not the fix; something has to WAKE the session when the process exits.
7
7
  //
8
8
  // Shape:
9
- // • The registry is owned by the SERVER (module singleton), not by the turn. agentx builds a fresh
9
+ // • The registry is owned by the SERVER (module singleton), not by the turn. An add-on engine builds a fresh
10
10
  // `Agent` per turn, so a registry created there would die with the turn — the very failure above.
11
11
  // Hosts hand each turn a thin per-session VIEW (`sessionJobRegistry`) over this one store; the
12
- // view is duck-compatible with agentx's `ShellJobRegistry`, so `Shell({background:true})` and the
12
+ // view is duck-compatible with the add-on engine's `ShellJobRegistry`, so `Shell({background:true})` and the
13
13
  // ShellOutput/ShellStatus/ShellKill tools light up with no folklore `nohup`.
14
14
  // • On exit we report through wake.ts — the same "close then report" path polls.ts already uses in
15
15
  // production, so a job outcome lands wherever the session speaks (Slack thread / web UI).
@@ -193,10 +193,10 @@ function pidAgeSeconds(pid: number): number | null {
193
193
  /**
194
194
  * Env for a job's shell: the server's own, minus anything that looks like a credential.
195
195
  *
196
- * agentx's foreground `Bash` redacts these by default (RealShellOptions.redactEnv). Backgrounding a
196
+ * An add-on engine's foreground `Bash` redacts these by default (RealShellOptions.redactEnv). Backgrounding a
197
197
  * command must not be a way around that — otherwise `Bash({background:true})` becomes a strictly
198
198
  * weaker sandbox than `Bash({})`, and `echo $ANTHROPIC_API_KEY` lands in a job log we then feed back
199
- * into the model. Mirrors agentx's SECRET_ENV_RE; a job that genuinely needs a credential gets it
199
+ * into the model. Mirrors the add-on engine's SECRET_ENV_RE; a job that genuinely needs a credential gets it
200
200
  * explicitly via `JobOwner.env`.
201
201
  */
202
202
  const SECRET_ENV_RE = /(API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$|^SLACK_)/i;
@@ -519,7 +519,7 @@ function pruneOld(): void {
519
519
  // ── The per-turn view handed to the agent engine ───────────────────────────────
520
520
 
521
521
  /**
522
- * A session-scoped facade over the process-wide store, duck-compatible with agentx's
522
+ * A session-scoped facade over the process-wide store, duck-compatible with the add-on engine's
523
523
  * `ShellJobRegistry` — pass it as `makeRealShellTool({ registry })` and to `makeShellJobTools`.
524
524
  *
525
525
  * Cheap to build per turn (it holds no state); the jobs it starts belong to the server, so they
@@ -1022,84 +1022,146 @@ function abortUpgrade(socket: import('node:stream').Duplex, code: number, messag
1022
1022
  catch { socket.destroy(); }
1023
1023
  }
1024
1024
 
1025
- function proxySidecarWebSocket(req: import('node:http').IncomingMessage, socket: import('node:stream').Duplex, head: Buffer, port: number) {
1025
+ /** Claude Code speaks the kitty keyboard protocol, where `CSI <code>;<mods> u` IS a modified keypress —
1026
+ * `CSI 107;9u` is cmd+k, which clears its screen and, arriving twice, runs `/clear` and destroys the
1027
+ * conversation. Our terminal is xterm.js 5.5, which has no kitty support and can never emit that byte,
1028
+ * so anything shaped like it on this socket is spurious, not a keystroke the user made. Drop it and say
1029
+ * where it came from — repeated unexplained `/clear`s cost real sessions. Remove once a client that
1030
+ * genuinely speaks the protocol attaches here. */
1031
+ const CSI_U_KEY = /\x1b\[\d+;\d+u/;
1032
+ function isSpuriousKittyKey(data: import('ws').RawData, isBinary: boolean): string | null {
1033
+ if (isBinary) return null;
1034
+ try {
1035
+ const msg = JSON.parse(data.toString());
1036
+ if (msg?.type !== 'input' || typeof msg.data !== 'string') return null;
1037
+ const input = Buffer.from(msg.data, 'base64').toString();
1038
+ return CSI_U_KEY.test(input) ? input : null;
1039
+ } catch { return null; }
1040
+ }
1041
+
1042
+ function proxySidecarWebSocket(req: import('node:http').IncomingMessage, socket: import('node:stream').Duplex, head: Buffer, port: number, authed: Promise<import('./auth.ts').AuthUser | null>) {
1026
1043
  const targetUrl = `ws://127.0.0.1:${port}${req.url}`;
1027
1044
  sidecarWss.handleUpgrade(req, socket as any, head, (clientWs) => {
1028
- // The caller paused the socket for the async auth gap; ws has now attached its own 'data' listener
1029
- // (handleUpgrade → setSocket runs before this callback), so it's safe — and necessary — to resume:
1030
- // an EXPLICITLY paused socket does not re-enter flowing mode just because a listener was added, so
1031
- // without this every buffered byte and every subsequent keystroke would sit unread forever.
1045
+ // Belt-and-braces: nothing pauses this socket any more (the auth gap moved past the handshake, see
1046
+ // AUTH GATE below), but an explicitly paused socket does not re-enter flowing mode just because a
1047
+ // listener was added — so if anything upstream ever pauses again, resuming here is what keeps every
1048
+ // buffered byte and every subsequent keystroke from sitting unread forever.
1032
1049
  socket.resume();
1033
- const targetWs = new WebSocket(targetUrl);
1034
- let opened = false;
1035
1050
 
1036
- // Bound IMMEDIATELY, not inside targetWs 'open': the BROWSER's socket is OPEN the moment
1037
- // handleUpgrade returns, so it reports "connected" and starts sending while we're still dialing the
1038
- // sidecar. Registering the listener on open discarded everything typed in that window — silently.
1039
- // Queue instead, and flush in order once upstream is up.
1040
- const pending: Array<{ data: import('ws').RawData; isBinary: boolean }> = [];
1051
+ // AUTH GATE — deliberately AFTER the handshake, not before it. The upgrade must complete in the
1052
+ // SAME tick as the 'upgrade' event: on Bun (reproduced on 1.4.0) a `handleUpgrade` deferred past a
1053
+ // macrotask — exactly what awaiting a token verification costs on a cold key cache — lands in ws's
1054
+ // `completeUpgrade` abort path, and ws's own `abortHandshake` then throws
1055
+ // (`undefined is not an object (evaluating 'message')`). The browser sees a bare close, caddy logs
1056
+ // `502 EOF`, and every terminal pane stays blank. Isolated repro: a microtask-deferred handleUpgrade
1057
+ // succeeds, a `setTimeout(…, 0)`-deferred one throws — which is why this only bit when auth actually
1058
+ // hit the network. Security is unchanged: NOTHING is dialed or forwarded until the token verifies,
1059
+ // and a failed check closes the socket with 1008 before a single byte reaches the sidecar.
1060
+ let targetWs: WebSocket;
1061
+ let opened = false;
1062
+ // Anything typed between the 101 and the auth verdict. Bounded like `pending` below; on rejection it
1063
+ // is dropped with the socket.
1064
+ const preAuth: Array<{ data: import('ws').RawData; isBinary: boolean }> = [];
1065
+ let gateOpen = false;
1041
1066
  clientWs.on('message', (data, isBinary) => {
1042
- // App-level liveness probe (Layer 2): the client can't read protocol pongs from JS, so it sends
1043
- // `{type:'ping'}` and expects `{type:'pong'}`. We RELAY it — we must not answer it here. A
1044
- // proxy-local reply only proves THIS hop is alive: if the proxy→sidecar leg is half-open, or the
1045
- // sidecar has already dropped this client from its subscriber set, the browser still gets pongs,
1046
- // keeps `readyState === OPEN`, shows a green "connected" dot, and every keystroke disappears.
1047
- // The probe is only worth anything end-to-end, so the sidecar owns the reply (it answers in its
1048
- // own ws message handler); a sidecar that doesn't reply fails the probe, which is the honest
1049
- // outcome — the client then reconnects rather than trusting a dead pipe.
1050
- if (targetWs.readyState === WebSocket.OPEN) targetWs.send(data, { binary: isBinary });
1051
- else if (!opened && pending.length < 256) pending.push({ data, isBinary }); // bounded: never buffer unboundedly
1052
- });
1053
-
1054
- targetWs.on('open', () => {
1055
- opened = true;
1056
- for (const m of pending) {
1057
- if (targetWs.readyState === WebSocket.OPEN) targetWs.send(m.data, { binary: m.isBinary });
1067
+ if (gateOpen || preAuth.length >= 256) return; // bounded: never buffer unboundedly
1068
+ // Same kitty-key filter the bridged handler applies. These frames are flushed straight into
1069
+ // `pending`, so without it a spurious `CSI …u` sent during the auth window would reach the shell
1070
+ // by the one path that skips the guard.
1071
+ if (isSpuriousKittyKey(data, isBinary)) {
1072
+ console.warn('[ws-proxy] BLOCKED kitty key input (pre-auth)', JSON.stringify({ url: req.url?.split('?')[0], ua: req.headers['user-agent'] }));
1073
+ return;
1058
1074
  }
1059
- pending.length = 0;
1060
- targetWs.on('message', (data, isBinary) => {
1061
- if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data, { binary: isBinary });
1062
- });
1075
+ preAuth.push({ data, isBinary });
1063
1076
  });
1064
-
1065
- // Keepalive: a proxied sidecar socket carries no app-level heartbeat, so an idle WS gets silently dropped
1066
- // by an intermediary (Cloudflare tunnel idles WS at ~100s) leaving the BROWSER half-open — readyState
1067
- // stays OPEN, no onclose fires, the "connected" dot stays green and keystrokes vanish into a dead pipe.
1068
- // Ping the client (browsers auto-pong at the protocol level) to keep intermediaries from idling us out,
1069
- // and terminate a peer that misses a pong so the client gets a real close → its reconnect kicks in.
1070
- // Tolerate ONE missed pong before terminating (~2 intervals of grace): a backgrounded mobile tab is
1071
- // JS/network-frozen and can't auto-pong for a cycle, so a 1-strike policy force-closed it every 30s and
1072
- // churned reconnects. Two strikes lets a brief freeze ride through; a truly dead pipe still gets cut.
1073
- let missedPongs = 0;
1074
- clientWs.on('pong', () => { missedPongs = 0; });
1075
- const pingInterval = setInterval(() => {
1077
+ void authed.then((user) => {
1076
1078
  if (clientWs.readyState !== WebSocket.OPEN) return;
1077
- if (missedPongs >= 2) { console.warn('[ws-proxy] client missed pongs — terminating (likely backgrounded/frozen client)'); clientWs.terminate(); return; }
1078
- missedPongs++;
1079
- clientWs.ping();
1080
- }, WS_PING_INTERVAL);
1081
-
1082
- targetWs.on('close', () => { clearInterval(pingInterval); clientWs.close(); });
1083
- targetWs.on('error', (e) => {
1084
- // Pre-open failure = the sidecar daemon is unreachable (e.g. it idle-exited, or is not up yet
1085
- // after a restart). This is TRANSIENT: the daemon (and its shells) survive a server/proxy blip, and
1086
- // we revive it right below — so flag `fatal:false`. The client must keep the pane alive and re-attach
1087
- // (a mobile client that backgrounded for minutes recovers its still-running sidecar on resume), NOT show a
1088
- // permanent "session unavailable". Only the daemon's own `session not found` (post-open) is fatal.
1089
- if (!opened) {
1090
- if (clientWs.readyState === WebSocket.OPEN) {
1091
- try { clientWs.send(JSON.stringify({ type: 'error', message: 'sidecar daemon unavailable', fatal: false })); } catch { /* socket gone */ }
1092
- }
1093
- } else {
1094
- console.warn('[ws-proxy] target error:', e.message);
1079
+ if (!user) {
1080
+ console.warn(`[ws-proxy] rejected unauthenticated upgrade for ${req.url?.split('?')[0]}`);
1081
+ clientWs.close(1008, 'unauthorized');
1082
+ return;
1095
1083
  }
1096
- clientWs.close();
1084
+ gateOpen = true;
1085
+ startBridge();
1097
1086
  });
1098
- clientWs.on('close', () => { clearInterval(pingInterval); if (targetWs.readyState === WebSocket.OPEN) targetWs.close(); });
1099
- clientWs.on('error', (e) => { console.error(`[ws-proxy] client error:`, e.message); targetWs.close(); });
1087
+
1088
+ function startBridge() {
1089
+ targetWs = new WebSocket(targetUrl);
1090
+
1091
+ // Bound IMMEDIATELY, not inside targetWs 'open': the BROWSER's socket is OPEN the moment
1092
+ // handleUpgrade returns, so it reports "connected" and starts sending while we're still dialing the
1093
+ // sidecar. Registering the listener on open discarded everything typed in that window — silently.
1094
+ // Queue instead, and flush in order once upstream is up.
1095
+ const pending: Array<{ data: import('ws').RawData; isBinary: boolean }> = preAuth.splice(0);
1096
+ clientWs.on('message', (data, isBinary) => {
1097
+ // App-level liveness probe (Layer 2): the client can't read protocol pongs from JS, so it sends
1098
+ // `{type:'ping'}` and expects `{type:'pong'}`. We RELAY it — we must not answer it here. A
1099
+ // proxy-local reply only proves THIS hop is alive: if the proxy→sidecar leg is half-open, or the
1100
+ // sidecar has already dropped this client from its subscriber set, the browser still gets pongs,
1101
+ // keeps `readyState === OPEN`, shows a green "connected" dot, and every keystroke disappears.
1102
+ // The probe is only worth anything end-to-end, so the sidecar owns the reply (para-pty answers in
1103
+ // its ws message handler); a sidecar that doesn't reply fails the probe, which is the honest
1104
+ // outcome — the client then reconnects rather than trusting a dead pipe.
1105
+ const spurious = isSpuriousKittyKey(data, isBinary);
1106
+ if (spurious) {
1107
+ console.warn('[ws-proxy] BLOCKED kitty key input', JSON.stringify({
1108
+ url: req.url?.split('?')[0], seq: spurious, ua: req.headers['user-agent'], ref: req.headers.referer,
1109
+ }));
1110
+ return;
1111
+ }
1112
+ if (targetWs.readyState === WebSocket.OPEN) targetWs.send(data, { binary: isBinary });
1113
+ else if (!opened && pending.length < 256) pending.push({ data, isBinary }); // bounded: never buffer unboundedly
1114
+ });
1115
+
1116
+ targetWs.on('open', () => {
1117
+ opened = true;
1118
+ for (const m of pending) {
1119
+ if (targetWs.readyState === WebSocket.OPEN) targetWs.send(m.data, { binary: m.isBinary });
1120
+ }
1121
+ pending.length = 0;
1122
+ targetWs.on('message', (data, isBinary) => {
1123
+ if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data, { binary: isBinary });
1124
+ });
1125
+ });
1126
+
1127
+ // Keepalive: a proxied sidecar socket carries no app-level heartbeat, so an idle WS gets silently dropped
1128
+ // by an intermediary (Cloudflare tunnel idles WS at ~100s) leaving the BROWSER half-open — readyState
1129
+ // stays OPEN, no onclose fires, the "connected" dot stays green and keystrokes vanish into a dead pipe.
1130
+ // Ping the client (browsers auto-pong at the protocol level) to keep intermediaries from idling us out,
1131
+ // and terminate a peer that misses a pong so the client gets a real close → its reconnect kicks in.
1132
+ // Tolerate ONE missed pong before terminating (~2 intervals of grace): a backgrounded mobile tab is
1133
+ // JS/network-frozen and can't auto-pong for a cycle, so a 1-strike policy force-closed it every 30s and
1134
+ // churned reconnects. Two strikes lets a brief freeze ride through; a truly dead pipe still gets cut.
1135
+ let missedPongs = 0;
1136
+ clientWs.on('pong', () => { missedPongs = 0; });
1137
+ const pingInterval = setInterval(() => {
1138
+ if (clientWs.readyState !== WebSocket.OPEN) return;
1139
+ if (missedPongs >= 2) { console.warn('[ws-proxy] client missed pongs — terminating (likely backgrounded/frozen client)'); clientWs.terminate(); return; }
1140
+ missedPongs++;
1141
+ clientWs.ping();
1142
+ }, WS_PING_INTERVAL);
1143
+
1144
+ targetWs.on('close', () => { clearInterval(pingInterval); clientWs.close(); });
1145
+ targetWs.on('error', (e) => {
1146
+ // Pre-open failure = the sidecar daemon is unreachable (e.g. it idle-exited, or is not up yet
1147
+ // after a restart). This is TRANSIENT: the daemon (and its shells) survive a server/proxy blip, and
1148
+ // we revive it right below — so flag `fatal:false`. The client must keep the pane alive and re-attach
1149
+ // (a mobile client that backgrounded for minutes recovers its still-running sidecar on resume), NOT show a
1150
+ // permanent "session unavailable". Only the daemon's own `session not found` (post-open) is fatal.
1151
+ if (!opened) {
1152
+ if (clientWs.readyState === WebSocket.OPEN) {
1153
+ try { clientWs.send(JSON.stringify({ type: 'error', message: 'sidecar daemon unavailable', fatal: false })); } catch { /* socket gone */ }
1154
+ }
1155
+ } else {
1156
+ console.warn('[ws-proxy] target error:', e.message);
1157
+ }
1158
+ clientWs.close();
1159
+ });
1160
+ clientWs.on('close', () => { clearInterval(pingInterval); if (targetWs.readyState === WebSocket.OPEN) targetWs.close(); });
1161
+ clientWs.on('error', (e) => { console.error(`[ws-proxy] client error:`, e.message); targetWs.close(); });
1162
+ }
1100
1163
  });
1101
1164
  }
1102
-
1103
1165
  server.on('upgrade', (req, socket, head) => {
1104
1166
  if (req.url === '/ws') {
1105
1167
  wss.handleUpgrade(req, socket as any, head, (ws) => {
@@ -1125,7 +1187,6 @@ server.on('upgrade', (req, socket, head) => {
1125
1187
  // is reading it while we verify; pausing makes that explicit and guarantees bytes the client sends
1126
1188
  // between the handshake and our decision are buffered, not dropped. proxySidecarWebSocket resumes
1127
1189
  // it once ws owns the socket.
1128
- socket.pause();
1129
1190
  // Fail CLOSED on anything: a rejected/thrown/slow auth must destroy the socket, never leave it
1130
1191
  // dangling. Without the catch a throw in authenticateWsUpgrade (token store unavailable, malformed
1131
1192
  // header) produced an unhandled rejection AND an open, unauthenticated, un-proxied socket; without
@@ -1139,17 +1200,15 @@ server.on('upgrade', (req, socket, head) => {
1139
1200
  // upgrade even when auth resolved in milliseconds.
1140
1201
  let authTimer: ReturnType<typeof setTimeout> | undefined;
1141
1202
  const timedOut = new Promise<null>((resolve) => { authTimer = setTimeout(() => resolve(null), WS_AUTH_TIMEOUT_MS); authTimer.unref?.(); });
1142
- void Promise.race([authed, timedOut])
1143
- .then((user) => {
1144
- if (authTimer) clearTimeout(authTimer);
1145
- if (socket.destroyed) return;
1146
- if (!user) {
1147
- console.warn(`[ws-proxy] rejected unauthenticated upgrade for ${req.url?.split('?')[0]}`);
1148
- abortUpgrade(socket, 401, 'Unauthorized: missing or invalid token');
1149
- return;
1150
- }
1151
- proxySidecarWebSocket(req, socket, head, port);
1152
- });
1203
+ const verdict = Promise.race([authed, timedOut]).then((user) => { if (authTimer) clearTimeout(authTimer); return user; });
1204
+ // The handshake is NOT deferred behind that verification: on Bun (reproduced on 1.4.0) a
1205
+ // `handleUpgrade` deferred past a macrotask lands in ws's `completeUpgrade` abort path, and ws's
1206
+ // own `abortHandshake` then throws (`undefined is not an object (evaluating 'message')`). The
1207
+ // browser sees a bare close, caddy logs `502 EOF`, and every terminal pane stays blank — and an
1208
+ // UNAUTHENTICATED upgrade hangs instead of being refused. Complete the upgrade now, hand the
1209
+ // in-flight verification down, and let it decide whether to bridge or close (see the AUTH GATE
1210
+ // note in proxySidecarWebSocket).
1211
+ proxySidecarWebSocket(req, socket, head, port, verdict);
1153
1212
  } else {
1154
1213
  // No sidecar registered for this path — a client error, not an auth failure.
1155
1214
  abortUpgrade(socket, 400, 'Unknown upgrade path');
@@ -38,7 +38,7 @@ const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine
38
38
  const isQualifiedModel = (v: string) => /^[a-z0-9._-]+\/[a-z0-9./_-]+$/.test(v);
39
39
 
40
40
  /** Resolves a model token the alias table doesn't know against the REGISTERED engines' own model
41
- * lists, and reports which engine owns it. Without this, `[composer-2.5]` (an agentx model) was
41
+ * lists, and reports which engine owns it. Without this, `[composer-2.5]` (an add-on engine's model) was
42
42
  * warned about and dropped, so the turn silently ran on the previous engine/model. Injected by
43
43
  * `initEngines()` — directives.ts stays pure and dependency-free for CE and for tests. */
44
44
  export type ModelResolver = (token: string) => { model: string; engine?: string } | null;
@@ -70,7 +70,7 @@ export function resolveAndGetEngine(directives?: { engine?: string }, agentConfi
70
70
  const name = resolveEngine(directives, agentConfig);
71
71
  // An optional engine may be unregistered on a given boot (add-on not loaded, missing API key or
72
72
  // failed init). Rerouting to claude-code here silently switched PROVIDER AND BILLING under the
73
- // caller — a cursor/agentx run billed Anthropic while the UI still showed the cursor chips. Fail
73
+ // caller — an add-on engine's run billed Anthropic while the UI still showed the cursor chips. Fail
74
74
  // loudly instead; the degradation is surfaced to whoever asked (user, schedule) as a turn error.
75
75
  if (!hasEngine(name)) throw new EngineUnavailableError(name, getAvailableEngines());
76
76
  return getEngine(name);
@@ -4,7 +4,7 @@
4
4
  * `kickstart -k` starts the replacement while the old process drains (up to 90s), so the new one
5
5
  * meets EADDRINUSE. Exiting immediately hands the problem to the service manager — which respawns
6
6
  * on a ~10s throttle, meets the same still-draining owner, and exits again. MEASURED 2026-09-06 on
7
- * the feedox box: one watchdog kick produced ~20 start/exit cycles over five minutes, every one of
7
+ * a production box: one watchdog kick produced ~20 start/exit cycles over five minutes, every one of
8
8
  * them `port already in use`, and every scheduled run in that window died with it. The manager was
9
9
  * doing exactly what it was asked to; the retry it was standing in for just belonged here, where
10
10
  * the wait is one process holding still instead of N processes racing.
@@ -68,7 +68,7 @@ export const APP_ROOT = resolveAppRoot();
68
68
  * (shipped assets) explicitly.
69
69
  *
70
70
  * Aliased to APP_ROOT, not PACKAGE_ROOT, deliberately: every remaining external consumer of this
71
- * export (shraga-ee `engine/cursor.ts`, `engine/agentx.ts`) uses it as the agent's project root —
71
+ * export (shraga-ee's optional engines, e.g. `engine/cursor.ts`) uses it as the agent's project root —
72
72
  * i.e. they meant APP_ROOT and were hitting the same npm-layout bug. Pointing the alias here fixes
73
73
  * them without an EE change. Nothing in this package reads shipped assets through it.
74
74
  */
@@ -7,7 +7,7 @@ import { execFileSync } from 'node:child_process';
7
7
  * when the server's launchd/systemd parent dies first: the server reparents to pid 1, nothing will
8
8
  * ever signal it, and it holds the port FOREVER. The replacement then dies on every respawn while
9
9
  * the orphan keeps serving the OLD code, so `kickstart -k` looks successful and changes nothing
10
- * (observed on feedox 2026-08-28: a deploy "restarted" the service four times, and the process
10
+ * (observed on a production deployment 2026-08-28: a deploy "restarted" the service four times, and the process
11
11
  * answering :3032 was three hours and one version old).
12
12
  *
13
13
  * Reclaiming is deliberately narrow — an over-broad "kill whatever holds my port" is how a deploy
@@ -22,7 +22,7 @@ export function reclaimStalePort(port: number, opts: { entrypoint?: string; cwd?
22
22
 
23
23
  // Any state, not just LISTEN: an orphan that closed its listener inside a drain it never finished
24
24
  // still OWNS the socket, and looking only for LISTEN made it invisible while it blocked every
25
- // replacement (feedox 2026-09-06: ppid-1 orphan, listener closed, port held for six minutes and
25
+ // replacement (a production deployment 2026-09-06: ppid-1 orphan, listener closed, port held for six minutes and
26
26
  // counting). `isStaleSelf` is what keeps this narrow — a bystander is never a candidate.
27
27
  const holders = [...new Set([...listeners(port), ...portUsers(port)])]
28
28
  .filter((pid) => pid !== process.pid && isStaleSelf(pid, entrypoint, cwd));
@@ -112,7 +112,7 @@ export function backfillScope(schedules: Schedule[]): void {
112
112
  }
113
113
  // Undo the `engine: 'claude-code'` pin reconcile stamped onto the failure notifier. It shipped on
114
114
  // a premise the box's own logs disprove — nothing was ever misrouted; the deployment was simply
115
- // globally on claude-code then and on agentx now — and it made the alarm the second casualty of
115
+ // globally on claude-code then and on an add-on engine now — and it made the alarm the second casualty of
116
116
  // the real failure mode (an org cap on the pinned provider). Narrow on purpose: exactly the
117
117
  // state that clause produced (this id, that engine, no model), so it cannot eat a real choice.
118
118
  if (s.id === FAILURE_NOTIFIER_SCHEDULE_ID && task.engine === 'claude-code' && !task.model) {
@@ -139,7 +139,7 @@ wait_for_version() { # version timeout
139
139
  # One dropped probe is not a crash-loop. This runs on a busy box where a single request can lose a
140
140
  # 10s race (MCP mounts, a GC pause, the drain window of the restart we just did), and a zero-
141
141
  # tolerance soak turns that blip into an automatic rollback of a perfectly good version — observed
142
- # reverting 0.1.48 on feedox while the process stayed up throughout. Require CONSECUTIVE misses, so
142
+ # reverting 0.1.48 on a production deployment while the process stayed up throughout. Require CONSECUTIVE misses, so
143
143
  # a real crash (which never answers again) still fails fast.
144
144
  SOAK_MISS_LIMIT="${SOAK_MISS_LIMIT:-3}"
145
145
  soak() { # version seconds