@hellcoder/companion 0.104.2 → 0.104.3
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/package.json +1 -1
- package/server/claude-adapter.test.ts +43 -12
- package/server/claude-adapter.ts +35 -14
package/package.json
CHANGED
|
@@ -1530,21 +1530,52 @@ function createMockProc() {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
1532
1532
|
describe("stdio transport disconnect propagation", () => {
|
|
1533
|
-
it("
|
|
1533
|
+
it("kills a wedged process that does not exit within the grace period", async () => {
|
|
1534
1534
|
// The wedge case: the worker stops emitting (e.g. after a 429) and closes
|
|
1535
|
-
// stdout but the process keeps running
|
|
1536
|
-
// relaunch path's PID-liveness guard
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1535
|
+
// stdout but the process keeps running and never exits. After the grace
|
|
1536
|
+
// window the adapter must kill it (so the relaunch path's PID-liveness guard
|
|
1537
|
+
// is cleared) and report disconnect.
|
|
1538
|
+
vi.useFakeTimers();
|
|
1539
|
+
try {
|
|
1540
|
+
const { proc, endStdout } = createMockProc();
|
|
1541
|
+
adapter.attachStdio(proc);
|
|
1542
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1543
|
+
|
|
1544
|
+
endStdout();
|
|
1545
|
+
// Within the grace window the process is still alive → not killed yet.
|
|
1546
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
1547
|
+
expect(proc.kill).not.toHaveBeenCalled();
|
|
1548
|
+
|
|
1549
|
+
// Past the grace window with no self-exit → treated as wedged and killed.
|
|
1550
|
+
await vi.advanceTimersByTimeAsync(2100);
|
|
1551
|
+
expect(proc.kill).toHaveBeenCalledTimes(1);
|
|
1552
|
+
expect(adapter.isConnected()).toBe(false);
|
|
1553
|
+
expect(disconnectCb).toHaveBeenCalledTimes(1);
|
|
1554
|
+
} finally {
|
|
1555
|
+
vi.useRealTimers();
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1540
1558
|
|
|
1541
|
-
|
|
1542
|
-
//
|
|
1543
|
-
|
|
1559
|
+
it("does NOT kill a process that exits on its own within the grace period", async () => {
|
|
1560
|
+
// The common, healthy case: the CLI closes stdout a beat before it exits
|
|
1561
|
+
// cleanly (e.g. right after a `result`). Killing here would convert a code-0
|
|
1562
|
+
// exit into a SIGTERM (143) and force a needless relaunch, so the adapter
|
|
1563
|
+
// must wait out the grace and let the process exit on its own.
|
|
1564
|
+
vi.useFakeTimers();
|
|
1565
|
+
try {
|
|
1566
|
+
const { proc, endStdout, resolveExit } = createMockProc();
|
|
1567
|
+
adapter.attachStdio(proc);
|
|
1544
1568
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1569
|
+
endStdout(); // stdout closes while the process is still alive
|
|
1570
|
+
resolveExit(); // ...but the process exits on its own during the grace
|
|
1571
|
+
await vi.advanceTimersByTimeAsync(2100);
|
|
1572
|
+
|
|
1573
|
+
expect(proc.kill).not.toHaveBeenCalled();
|
|
1574
|
+
expect(adapter.isConnected()).toBe(false);
|
|
1575
|
+
expect(disconnectCb).toHaveBeenCalledTimes(1);
|
|
1576
|
+
} finally {
|
|
1577
|
+
vi.useRealTimers();
|
|
1578
|
+
}
|
|
1548
1579
|
});
|
|
1549
1580
|
|
|
1550
1581
|
it("fires disconnect on process exit", async () => {
|
package/server/claude-adapter.ts
CHANGED
|
@@ -55,6 +55,16 @@ import { reportProtocolDrift } from "./protocol-monitor.js";
|
|
|
55
55
|
/** Number of recent CLI message hashes to track for deduplication on WS reconnect. */
|
|
56
56
|
const CLI_DEDUP_WINDOW = 2000;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Grace period after stdout closes before we kill a still-alive CLI process.
|
|
60
|
+
* The CLI's stdout often EOFs a beat *before* the process exits cleanly (e.g.
|
|
61
|
+
* right after emitting a `result`). Killing immediately turns that code-0 exit
|
|
62
|
+
* into a SIGTERM (143) and forces a needless relaunch. We wait this long for the
|
|
63
|
+
* process to exit on its own; only a process that is still alive afterwards is
|
|
64
|
+
* treated as genuinely wedged and killed. Overridable for tests/tuning.
|
|
65
|
+
*/
|
|
66
|
+
const STDOUT_CLOSE_GRACE_MS = Number(process.env.COMPANION_STDOUT_CLOSE_GRACE_MS) || 2000;
|
|
67
|
+
|
|
58
68
|
// --- Claude Code Adapter ------------------------------------------------------
|
|
59
69
|
|
|
60
70
|
export class ClaudeAdapter implements IBackendAdapter {
|
|
@@ -277,22 +287,33 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
277
287
|
});
|
|
278
288
|
} finally {
|
|
279
289
|
// The stdout stream ended: the stream-json transport is dead and cannot
|
|
280
|
-
// be revived on this process.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
290
|
+
// be revived on this process. The process is now in one of two states:
|
|
291
|
+
// (a) Exiting cleanly — its stdout EOFs a beat before it exits (e.g.
|
|
292
|
+
// right after a `result`). Killing here converts a code-0 exit into
|
|
293
|
+
// a SIGTERM (143) and forces a needless relaunch.
|
|
294
|
+
// (b) Wedged — it stopped emitting (e.g. after a fatal API error such
|
|
295
|
+
// as a 429) but will never exit, leaving `handleAutoRelaunch`'s
|
|
296
|
+
// PID-liveness guard satisfied and blocking recovery indefinitely.
|
|
297
|
+
// Give it a short grace to exit on its own; only kill if it is STILL alive
|
|
298
|
+
// afterwards. That distinguishes (a) from (b) instead of clobbering every
|
|
299
|
+
// session that simply closed stdout on the way out.
|
|
286
300
|
const proc = this.stdioProc;
|
|
287
301
|
if (proc && proc.exitCode === null && !proc.killed) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
302
|
+
const exitedOnOwn = await Promise.race([
|
|
303
|
+
proc.exited.then(() => true),
|
|
304
|
+
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), STDOUT_CLOSE_GRACE_MS)),
|
|
305
|
+
]);
|
|
306
|
+
if (!exitedOnOwn && proc.exitCode === null && !proc.killed) {
|
|
307
|
+
log.warn("claude-adapter", "stdout closed and process did not exit within grace; killing stale process", {
|
|
308
|
+
sessionId: this.sessionId,
|
|
309
|
+
pid: proc.pid,
|
|
310
|
+
graceMs: STDOUT_CLOSE_GRACE_MS,
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
proc.kill();
|
|
314
|
+
} catch {
|
|
315
|
+
// Process may have exited between the check and the kill.
|
|
316
|
+
}
|
|
296
317
|
}
|
|
297
318
|
}
|
|
298
319
|
this.notifyStdioDisconnect();
|