@hellcoder/companion 0.111.2 → 0.111.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/auto-namer.test.ts +4 -0
- package/server/claude-adapter.test.ts +52 -0
- package/server/claude-adapter.ts +28 -0
- package/server/linear-connections.test.ts +4 -0
- package/server/routes/settings-routes.ts +10 -0
- package/server/routes.test.ts +27 -0
- package/server/session-orchestrator.test.ts +99 -0
- package/server/session-orchestrator.ts +32 -3
- package/server/settings-manager.test.ts +3 -0
- package/server/settings-manager.ts +14 -1
- package/server/ws-bridge-codex.test.ts +6 -0
package/package.json
CHANGED
|
@@ -42,6 +42,7 @@ beforeEach(() => {
|
|
|
42
42
|
updateChannel: "stable",
|
|
43
43
|
dockerAutoUpdate: false,
|
|
44
44
|
proactiveKeepaliveEnabled: true,
|
|
45
|
+
keepaliveDetachedSessions: false,
|
|
45
46
|
wedgeKillEnabled: true,
|
|
46
47
|
silenceProbeEnabled: true,
|
|
47
48
|
updatedAt: 0,
|
|
@@ -92,6 +93,7 @@ describe("generateSessionTitle", () => {
|
|
|
92
93
|
updateChannel: "stable",
|
|
93
94
|
dockerAutoUpdate: false,
|
|
94
95
|
proactiveKeepaliveEnabled: true,
|
|
96
|
+
keepaliveDetachedSessions: false,
|
|
95
97
|
wedgeKillEnabled: true,
|
|
96
98
|
silenceProbeEnabled: true,
|
|
97
99
|
updatedAt: 0,
|
|
@@ -149,6 +151,7 @@ describe("generateSessionTitle", () => {
|
|
|
149
151
|
updateChannel: "stable",
|
|
150
152
|
dockerAutoUpdate: false,
|
|
151
153
|
proactiveKeepaliveEnabled: true,
|
|
154
|
+
keepaliveDetachedSessions: false,
|
|
152
155
|
wedgeKillEnabled: true,
|
|
153
156
|
silenceProbeEnabled: true,
|
|
154
157
|
updatedAt: 0,
|
|
@@ -235,6 +238,7 @@ describe("generateSessionTitle", () => {
|
|
|
235
238
|
updateChannel: "stable",
|
|
236
239
|
dockerAutoUpdate: false,
|
|
237
240
|
proactiveKeepaliveEnabled: true,
|
|
241
|
+
keepaliveDetachedSessions: false,
|
|
238
242
|
wedgeKillEnabled: true,
|
|
239
243
|
silenceProbeEnabled: true,
|
|
240
244
|
updatedAt: 0,
|
|
@@ -1864,6 +1864,58 @@ describe("stdio silence probe", () => {
|
|
|
1864
1864
|
expect(adapter.isConnected()).toBe(true);
|
|
1865
1865
|
});
|
|
1866
1866
|
|
|
1867
|
+
it("does NOT kill a session that is silent because a tool is running", async () => {
|
|
1868
|
+
// Production regression: a session running its test suite (a single ~200s
|
|
1869
|
+
// Bash call) was declared dead at 182s of silence and relaunched, losing the
|
|
1870
|
+
// turn. The CLI does not service control requests while a synchronous tool
|
|
1871
|
+
// runs, so an unanswered probe proves nothing.
|
|
1872
|
+
vi.useFakeTimers();
|
|
1873
|
+
const { proc, pushStdout } = createMockProc();
|
|
1874
|
+
adapter.attachStdio(proc);
|
|
1875
|
+
|
|
1876
|
+
pushStdout(
|
|
1877
|
+
JSON.stringify({
|
|
1878
|
+
type: "assistant",
|
|
1879
|
+
message: {
|
|
1880
|
+
role: "assistant",
|
|
1881
|
+
model: "m",
|
|
1882
|
+
content: [{ type: "tool_use", id: "tu-slow", name: "Bash", input: {} }],
|
|
1883
|
+
},
|
|
1884
|
+
session_id: "s",
|
|
1885
|
+
}) + "\n",
|
|
1886
|
+
);
|
|
1887
|
+
|
|
1888
|
+
// Well past probe-after + probe-timeout, which previously killed it.
|
|
1889
|
+
await vi.advanceTimersByTimeAsync(300_000);
|
|
1890
|
+
|
|
1891
|
+
expect(disconnectCb).not.toHaveBeenCalled();
|
|
1892
|
+
expect(adapter.isConnected()).toBe(true);
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
it("still escalates when a tool call blows the grace ceiling", async () => {
|
|
1896
|
+
// A tool that never returns must not suppress recovery forever.
|
|
1897
|
+
vi.useFakeTimers();
|
|
1898
|
+
const { proc, pushStdout } = createMockProc();
|
|
1899
|
+
adapter.attachStdio(proc);
|
|
1900
|
+
|
|
1901
|
+
pushStdout(
|
|
1902
|
+
JSON.stringify({
|
|
1903
|
+
type: "assistant",
|
|
1904
|
+
message: {
|
|
1905
|
+
role: "assistant",
|
|
1906
|
+
model: "m",
|
|
1907
|
+
content: [{ type: "tool_use", id: "tu-stuck", name: "Bash", input: {} }],
|
|
1908
|
+
},
|
|
1909
|
+
session_id: "s",
|
|
1910
|
+
}) + "\n",
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
await vi.advanceTimersByTimeAsync(700_000); // past the 660s ceiling
|
|
1914
|
+
|
|
1915
|
+
expect(disconnectCb).toHaveBeenCalledTimes(1);
|
|
1916
|
+
expect(adapter.isConnected()).toBe(false);
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1867
1919
|
it("does nothing when the silence probe is disabled", async () => {
|
|
1868
1920
|
vi.useFakeTimers();
|
|
1869
1921
|
mockSettings.silenceProbeEnabled = false;
|
package/server/claude-adapter.ts
CHANGED
|
@@ -159,6 +159,18 @@ const SILENCE_CHECK_INTERVAL_MS = Number(process.env.COMPANION_SILENCE_CHECK_INT
|
|
|
159
159
|
*/
|
|
160
160
|
const TURN_STALL_AFTER_MS = Number(process.env.COMPANION_TURN_STALL_AFTER_MS) || 300_000;
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* How long a CLI with an outstanding tool call is allowed to stay silent before
|
|
164
|
+
* the liveness probe is permitted to escalate anyway.
|
|
165
|
+
*
|
|
166
|
+
* The CLI does not answer control requests while a synchronous tool runs, so
|
|
167
|
+
* during a long `Bash` call the probe goes unanswered for reasons that have
|
|
168
|
+
* nothing to do with health. Set above the CLI's own maximum background-command
|
|
169
|
+
* duration (10 minutes), on the same reasoning as TEARDOWN_MAX_MS: only a tool
|
|
170
|
+
* that blows this ceiling is treated as a genuine hang.
|
|
171
|
+
*/
|
|
172
|
+
const TOOL_CALL_PROBE_GRACE_MS = Number(process.env.COMPANION_TOOL_CALL_PROBE_GRACE_MS) || 660_000;
|
|
173
|
+
|
|
162
174
|
/** How often to re-check the descendant count while waiting for teardown. */
|
|
163
175
|
const TEARDOWN_POLL_MS = Number(process.env.COMPANION_TEARDOWN_POLL_MS) || 500;
|
|
164
176
|
|
|
@@ -626,6 +638,22 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
626
638
|
// An unanswered probe is the actual failure signal: a healthy CLI replies
|
|
627
639
|
// to control requests promptly even while working.
|
|
628
640
|
if (this.probeSentAt !== null) {
|
|
641
|
+
// A long tool call is silence with a cause. The CLI does not service
|
|
642
|
+
// control requests while a synchronous tool runs, so an unanswered probe
|
|
643
|
+
// proves nothing here — and killing on it destroys a healthy session
|
|
644
|
+
// mid-work. Observed in production: a session running its test suite (a
|
|
645
|
+
// single ~200s Bash call) was declared dead at 182s of silence and
|
|
646
|
+
// relaunched, losing the turn.
|
|
647
|
+
//
|
|
648
|
+
// Bounded by TOOL_CALL_PROBE_GRACE_MS so a tool that never returns cannot
|
|
649
|
+
// suppress recovery forever; the ceiling sits above the CLI's own maximum
|
|
650
|
+
// background-command duration, matching TEARDOWN_MAX_MS's reasoning.
|
|
651
|
+
if (
|
|
652
|
+
this.pendingToolUses.size > 0 &&
|
|
653
|
+
now - this.lastInboundAt < TOOL_CALL_PROBE_GRACE_MS
|
|
654
|
+
) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
629
657
|
if (now - this.probeSentAt >= SILENCE_PROBE_TIMEOUT_MS) {
|
|
630
658
|
log.warn("claude-adapter", "CLI silent with stdout open and probe unanswered; treating transport as dead", {
|
|
631
659
|
sessionId: this.sessionId,
|
|
@@ -68,6 +68,7 @@ beforeEach(() => {
|
|
|
68
68
|
updateChannel: "stable",
|
|
69
69
|
dockerAutoUpdate: false,
|
|
70
70
|
proactiveKeepaliveEnabled: true,
|
|
71
|
+
keepaliveDetachedSessions: false,
|
|
71
72
|
wedgeKillEnabled: true,
|
|
72
73
|
silenceProbeEnabled: true,
|
|
73
74
|
updatedAt: 0,
|
|
@@ -206,6 +207,7 @@ describe("linear-connections", () => {
|
|
|
206
207
|
updateChannel: "stable",
|
|
207
208
|
dockerAutoUpdate: false,
|
|
208
209
|
proactiveKeepaliveEnabled: true,
|
|
210
|
+
keepaliveDetachedSessions: false,
|
|
209
211
|
wedgeKillEnabled: true,
|
|
210
212
|
silenceProbeEnabled: true,
|
|
211
213
|
updatedAt: 0,
|
|
@@ -254,6 +256,7 @@ describe("linear-connections", () => {
|
|
|
254
256
|
updateChannel: "stable",
|
|
255
257
|
dockerAutoUpdate: false,
|
|
256
258
|
proactiveKeepaliveEnabled: true,
|
|
259
|
+
keepaliveDetachedSessions: false,
|
|
257
260
|
wedgeKillEnabled: true,
|
|
258
261
|
silenceProbeEnabled: true,
|
|
259
262
|
updatedAt: 0,
|
|
@@ -321,6 +324,7 @@ describe("linear-connections", () => {
|
|
|
321
324
|
updateChannel: "stable",
|
|
322
325
|
dockerAutoUpdate: false,
|
|
323
326
|
proactiveKeepaliveEnabled: true,
|
|
327
|
+
keepaliveDetachedSessions: false,
|
|
324
328
|
wedgeKillEnabled: true,
|
|
325
329
|
silenceProbeEnabled: true,
|
|
326
330
|
updatedAt: 0,
|
|
@@ -34,6 +34,7 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
34
34
|
updateChannel: settings.updateChannel,
|
|
35
35
|
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
36
36
|
proactiveKeepaliveEnabled: settings.proactiveKeepaliveEnabled,
|
|
37
|
+
keepaliveDetachedSessions: settings.keepaliveDetachedSessions,
|
|
37
38
|
wedgeKillEnabled: settings.wedgeKillEnabled,
|
|
38
39
|
silenceProbeEnabled: settings.silenceProbeEnabled,
|
|
39
40
|
cliBridgeMode: settings.cliBridgeMode,
|
|
@@ -134,6 +135,9 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
134
135
|
if (body.proactiveKeepaliveEnabled !== undefined && typeof body.proactiveKeepaliveEnabled !== "boolean") {
|
|
135
136
|
return c.json({ error: "proactiveKeepaliveEnabled must be a boolean" }, 400);
|
|
136
137
|
}
|
|
138
|
+
if (body.keepaliveDetachedSessions !== undefined && typeof body.keepaliveDetachedSessions !== "boolean") {
|
|
139
|
+
return c.json({ error: "keepaliveDetachedSessions must be a boolean" }, 400);
|
|
140
|
+
}
|
|
137
141
|
if (body.wedgeKillEnabled !== undefined && typeof body.wedgeKillEnabled !== "boolean") {
|
|
138
142
|
return c.json({ error: "wedgeKillEnabled must be a boolean" }, 400);
|
|
139
143
|
}
|
|
@@ -160,6 +164,7 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
160
164
|
|| body.updateChannel !== undefined
|
|
161
165
|
|| body.dockerAutoUpdate !== undefined
|
|
162
166
|
|| body.proactiveKeepaliveEnabled !== undefined
|
|
167
|
+
|| body.keepaliveDetachedSessions !== undefined
|
|
163
168
|
|| body.wedgeKillEnabled !== undefined
|
|
164
169
|
|| body.silenceProbeEnabled !== undefined
|
|
165
170
|
|| body.cliBridgeMode !== undefined;
|
|
@@ -276,6 +281,10 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
276
281
|
typeof body.proactiveKeepaliveEnabled === "boolean"
|
|
277
282
|
? body.proactiveKeepaliveEnabled
|
|
278
283
|
: undefined,
|
|
284
|
+
keepaliveDetachedSessions:
|
|
285
|
+
typeof body.keepaliveDetachedSessions === "boolean"
|
|
286
|
+
? body.keepaliveDetachedSessions
|
|
287
|
+
: undefined,
|
|
279
288
|
wedgeKillEnabled:
|
|
280
289
|
typeof body.wedgeKillEnabled === "boolean"
|
|
281
290
|
? body.wedgeKillEnabled
|
|
@@ -317,6 +326,7 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
317
326
|
updateChannel: settings.updateChannel,
|
|
318
327
|
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
319
328
|
proactiveKeepaliveEnabled: settings.proactiveKeepaliveEnabled,
|
|
329
|
+
keepaliveDetachedSessions: settings.keepaliveDetachedSessions,
|
|
320
330
|
wedgeKillEnabled: settings.wedgeKillEnabled,
|
|
321
331
|
silenceProbeEnabled: settings.silenceProbeEnabled,
|
|
322
332
|
cliBridgeMode: settings.cliBridgeMode,
|
package/server/routes.test.ts
CHANGED
|
@@ -1158,6 +1158,7 @@ describe("GET /api/sessions/:id/archive-info", () => {
|
|
|
1158
1158
|
updateChannel: "stable",
|
|
1159
1159
|
dockerAutoUpdate: false,
|
|
1160
1160
|
proactiveKeepaliveEnabled: true,
|
|
1161
|
+
keepaliveDetachedSessions: false,
|
|
1161
1162
|
wedgeKillEnabled: true,
|
|
1162
1163
|
silenceProbeEnabled: true,
|
|
1163
1164
|
updatedAt: 0,
|
|
@@ -1535,6 +1536,7 @@ describe("GET /api/settings", () => {
|
|
|
1535
1536
|
updateChannel: "stable",
|
|
1536
1537
|
dockerAutoUpdate: false,
|
|
1537
1538
|
proactiveKeepaliveEnabled: true,
|
|
1539
|
+
keepaliveDetachedSessions: false,
|
|
1538
1540
|
wedgeKillEnabled: true,
|
|
1539
1541
|
silenceProbeEnabled: true,
|
|
1540
1542
|
updatedAt: 123,
|
|
@@ -1570,6 +1572,7 @@ describe("GET /api/settings", () => {
|
|
|
1570
1572
|
updateChannel: "stable",
|
|
1571
1573
|
dockerAutoUpdate: false,
|
|
1572
1574
|
proactiveKeepaliveEnabled: true,
|
|
1575
|
+
keepaliveDetachedSessions: false,
|
|
1573
1576
|
wedgeKillEnabled: true,
|
|
1574
1577
|
silenceProbeEnabled: true,
|
|
1575
1578
|
});
|
|
@@ -1605,6 +1608,7 @@ describe("GET /api/settings", () => {
|
|
|
1605
1608
|
updateChannel: "stable",
|
|
1606
1609
|
dockerAutoUpdate: false,
|
|
1607
1610
|
proactiveKeepaliveEnabled: true,
|
|
1611
|
+
keepaliveDetachedSessions: false,
|
|
1608
1612
|
wedgeKillEnabled: true,
|
|
1609
1613
|
silenceProbeEnabled: true,
|
|
1610
1614
|
updatedAt: 123,
|
|
@@ -1640,6 +1644,7 @@ describe("GET /api/settings", () => {
|
|
|
1640
1644
|
updateChannel: "stable",
|
|
1641
1645
|
dockerAutoUpdate: false,
|
|
1642
1646
|
proactiveKeepaliveEnabled: true,
|
|
1647
|
+
keepaliveDetachedSessions: false,
|
|
1643
1648
|
wedgeKillEnabled: true,
|
|
1644
1649
|
silenceProbeEnabled: true,
|
|
1645
1650
|
});
|
|
@@ -1676,6 +1681,7 @@ describe("GET /api/settings", () => {
|
|
|
1676
1681
|
updateChannel: "stable",
|
|
1677
1682
|
dockerAutoUpdate: false,
|
|
1678
1683
|
proactiveKeepaliveEnabled: true,
|
|
1684
|
+
keepaliveDetachedSessions: false,
|
|
1679
1685
|
wedgeKillEnabled: true,
|
|
1680
1686
|
silenceProbeEnabled: true,
|
|
1681
1687
|
updatedAt: 100,
|
|
@@ -1720,6 +1726,7 @@ describe("PUT /api/settings", () => {
|
|
|
1720
1726
|
updateChannel: "stable",
|
|
1721
1727
|
dockerAutoUpdate: false,
|
|
1722
1728
|
proactiveKeepaliveEnabled: true,
|
|
1729
|
+
keepaliveDetachedSessions: false,
|
|
1723
1730
|
wedgeKillEnabled: true,
|
|
1724
1731
|
silenceProbeEnabled: true,
|
|
1725
1732
|
updatedAt: 456,
|
|
@@ -1777,6 +1784,7 @@ describe("PUT /api/settings", () => {
|
|
|
1777
1784
|
updateChannel: "stable",
|
|
1778
1785
|
dockerAutoUpdate: false,
|
|
1779
1786
|
proactiveKeepaliveEnabled: true,
|
|
1787
|
+
keepaliveDetachedSessions: false,
|
|
1780
1788
|
wedgeKillEnabled: true,
|
|
1781
1789
|
silenceProbeEnabled: true,
|
|
1782
1790
|
});
|
|
@@ -1812,6 +1820,7 @@ describe("PUT /api/settings", () => {
|
|
|
1812
1820
|
updateChannel: "stable",
|
|
1813
1821
|
dockerAutoUpdate: false,
|
|
1814
1822
|
proactiveKeepaliveEnabled: true,
|
|
1823
|
+
keepaliveDetachedSessions: false,
|
|
1815
1824
|
wedgeKillEnabled: true,
|
|
1816
1825
|
silenceProbeEnabled: true,
|
|
1817
1826
|
updatedAt: 789,
|
|
@@ -1864,6 +1873,7 @@ describe("PUT /api/settings", () => {
|
|
|
1864
1873
|
updateChannel: "stable",
|
|
1865
1874
|
dockerAutoUpdate: false,
|
|
1866
1875
|
proactiveKeepaliveEnabled: true,
|
|
1876
|
+
keepaliveDetachedSessions: false,
|
|
1867
1877
|
wedgeKillEnabled: true,
|
|
1868
1878
|
silenceProbeEnabled: true,
|
|
1869
1879
|
updatedAt: 999,
|
|
@@ -2016,6 +2026,7 @@ describe("PUT /api/settings", () => {
|
|
|
2016
2026
|
updateChannel: "stable",
|
|
2017
2027
|
dockerAutoUpdate: false,
|
|
2018
2028
|
proactiveKeepaliveEnabled: true,
|
|
2029
|
+
keepaliveDetachedSessions: false,
|
|
2019
2030
|
wedgeKillEnabled: true,
|
|
2020
2031
|
silenceProbeEnabled: true,
|
|
2021
2032
|
updatedAt: 500,
|
|
@@ -2274,6 +2285,7 @@ describe("GET /api/linear/issues", () => {
|
|
|
2274
2285
|
updateChannel: "stable",
|
|
2275
2286
|
dockerAutoUpdate: false,
|
|
2276
2287
|
proactiveKeepaliveEnabled: true,
|
|
2288
|
+
keepaliveDetachedSessions: false,
|
|
2277
2289
|
wedgeKillEnabled: true,
|
|
2278
2290
|
silenceProbeEnabled: true,
|
|
2279
2291
|
updatedAt: 0,
|
|
@@ -2316,6 +2328,7 @@ describe("GET /api/linear/issues", () => {
|
|
|
2316
2328
|
updateChannel: "stable",
|
|
2317
2329
|
dockerAutoUpdate: false,
|
|
2318
2330
|
proactiveKeepaliveEnabled: true,
|
|
2331
|
+
keepaliveDetachedSessions: false,
|
|
2319
2332
|
wedgeKillEnabled: true,
|
|
2320
2333
|
silenceProbeEnabled: true,
|
|
2321
2334
|
updatedAt: 0,
|
|
@@ -2411,6 +2424,7 @@ describe("GET /api/linear/issues", () => {
|
|
|
2411
2424
|
updateChannel: "stable",
|
|
2412
2425
|
dockerAutoUpdate: false,
|
|
2413
2426
|
proactiveKeepaliveEnabled: true,
|
|
2427
|
+
keepaliveDetachedSessions: false,
|
|
2414
2428
|
wedgeKillEnabled: true,
|
|
2415
2429
|
silenceProbeEnabled: true,
|
|
2416
2430
|
updatedAt: 0,
|
|
@@ -2513,6 +2527,7 @@ describe("GET /api/linear/issues", () => {
|
|
|
2513
2527
|
updateChannel: "stable",
|
|
2514
2528
|
dockerAutoUpdate: false,
|
|
2515
2529
|
proactiveKeepaliveEnabled: true,
|
|
2530
|
+
keepaliveDetachedSessions: false,
|
|
2516
2531
|
wedgeKillEnabled: true,
|
|
2517
2532
|
silenceProbeEnabled: true,
|
|
2518
2533
|
updatedAt: 0,
|
|
@@ -2580,6 +2595,7 @@ describe("GET /api/linear/connection", () => {
|
|
|
2580
2595
|
updateChannel: "stable",
|
|
2581
2596
|
dockerAutoUpdate: false,
|
|
2582
2597
|
proactiveKeepaliveEnabled: true,
|
|
2598
|
+
keepaliveDetachedSessions: false,
|
|
2583
2599
|
wedgeKillEnabled: true,
|
|
2584
2600
|
silenceProbeEnabled: true,
|
|
2585
2601
|
updatedAt: 0,
|
|
@@ -2622,6 +2638,7 @@ describe("GET /api/linear/connection", () => {
|
|
|
2622
2638
|
updateChannel: "stable",
|
|
2623
2639
|
dockerAutoUpdate: false,
|
|
2624
2640
|
proactiveKeepaliveEnabled: true,
|
|
2641
|
+
keepaliveDetachedSessions: false,
|
|
2625
2642
|
wedgeKillEnabled: true,
|
|
2626
2643
|
silenceProbeEnabled: true,
|
|
2627
2644
|
updatedAt: 0,
|
|
@@ -2686,6 +2703,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
|
|
|
2686
2703
|
updateChannel: "stable",
|
|
2687
2704
|
dockerAutoUpdate: false,
|
|
2688
2705
|
proactiveKeepaliveEnabled: true,
|
|
2706
|
+
keepaliveDetachedSessions: false,
|
|
2689
2707
|
wedgeKillEnabled: true,
|
|
2690
2708
|
silenceProbeEnabled: true,
|
|
2691
2709
|
updatedAt: 0,
|
|
@@ -2732,6 +2750,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
|
|
|
2732
2750
|
updateChannel: "stable",
|
|
2733
2751
|
dockerAutoUpdate: false,
|
|
2734
2752
|
proactiveKeepaliveEnabled: true,
|
|
2753
|
+
keepaliveDetachedSessions: false,
|
|
2735
2754
|
wedgeKillEnabled: true,
|
|
2736
2755
|
silenceProbeEnabled: true,
|
|
2737
2756
|
updatedAt: 0,
|
|
@@ -2777,6 +2796,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
|
|
|
2777
2796
|
updateChannel: "stable",
|
|
2778
2797
|
dockerAutoUpdate: false,
|
|
2779
2798
|
proactiveKeepaliveEnabled: true,
|
|
2799
|
+
keepaliveDetachedSessions: false,
|
|
2780
2800
|
wedgeKillEnabled: true,
|
|
2781
2801
|
silenceProbeEnabled: true,
|
|
2782
2802
|
updatedAt: 0,
|
|
@@ -2824,6 +2844,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
|
|
|
2824
2844
|
updateChannel: "stable",
|
|
2825
2845
|
dockerAutoUpdate: false,
|
|
2826
2846
|
proactiveKeepaliveEnabled: true,
|
|
2847
|
+
keepaliveDetachedSessions: false,
|
|
2827
2848
|
wedgeKillEnabled: true,
|
|
2828
2849
|
silenceProbeEnabled: true,
|
|
2829
2850
|
updatedAt: 0,
|
|
@@ -2905,6 +2926,7 @@ describe("POST /api/linear/issues/:id/transition", () => {
|
|
|
2905
2926
|
updateChannel: "stable",
|
|
2906
2927
|
dockerAutoUpdate: false,
|
|
2907
2928
|
proactiveKeepaliveEnabled: true,
|
|
2929
|
+
keepaliveDetachedSessions: false,
|
|
2908
2930
|
wedgeKillEnabled: true,
|
|
2909
2931
|
silenceProbeEnabled: true,
|
|
2910
2932
|
updatedAt: 0,
|
|
@@ -2965,6 +2987,7 @@ describe("GET /api/linear/projects", () => {
|
|
|
2965
2987
|
updateChannel: "stable",
|
|
2966
2988
|
dockerAutoUpdate: false,
|
|
2967
2989
|
proactiveKeepaliveEnabled: true,
|
|
2990
|
+
keepaliveDetachedSessions: false,
|
|
2968
2991
|
wedgeKillEnabled: true,
|
|
2969
2992
|
silenceProbeEnabled: true,
|
|
2970
2993
|
updatedAt: 0,
|
|
@@ -3007,6 +3030,7 @@ describe("GET /api/linear/projects", () => {
|
|
|
3007
3030
|
updateChannel: "stable",
|
|
3008
3031
|
dockerAutoUpdate: false,
|
|
3009
3032
|
proactiveKeepaliveEnabled: true,
|
|
3033
|
+
keepaliveDetachedSessions: false,
|
|
3010
3034
|
wedgeKillEnabled: true,
|
|
3011
3035
|
silenceProbeEnabled: true,
|
|
3012
3036
|
updatedAt: 0,
|
|
@@ -3079,6 +3103,7 @@ describe("GET /api/linear/project-issues", () => {
|
|
|
3079
3103
|
updateChannel: "stable",
|
|
3080
3104
|
dockerAutoUpdate: false,
|
|
3081
3105
|
proactiveKeepaliveEnabled: true,
|
|
3106
|
+
keepaliveDetachedSessions: false,
|
|
3082
3107
|
wedgeKillEnabled: true,
|
|
3083
3108
|
silenceProbeEnabled: true,
|
|
3084
3109
|
updatedAt: 0,
|
|
@@ -3121,6 +3146,7 @@ describe("GET /api/linear/project-issues", () => {
|
|
|
3121
3146
|
updateChannel: "stable",
|
|
3122
3147
|
dockerAutoUpdate: false,
|
|
3123
3148
|
proactiveKeepaliveEnabled: true,
|
|
3149
|
+
keepaliveDetachedSessions: false,
|
|
3124
3150
|
wedgeKillEnabled: true,
|
|
3125
3151
|
silenceProbeEnabled: true,
|
|
3126
3152
|
updatedAt: 0,
|
|
@@ -3208,6 +3234,7 @@ describe("GET /api/linear/project-issues", () => {
|
|
|
3208
3234
|
updateChannel: "stable",
|
|
3209
3235
|
dockerAutoUpdate: false,
|
|
3210
3236
|
proactiveKeepaliveEnabled: true,
|
|
3237
|
+
keepaliveDetachedSessions: false,
|
|
3211
3238
|
wedgeKillEnabled: true,
|
|
3212
3239
|
silenceProbeEnabled: true,
|
|
3213
3240
|
updatedAt: 0,
|
|
@@ -50,6 +50,7 @@ vi.mock("./settings-manager.js", () => ({
|
|
|
50
50
|
openaiApiKey: "",
|
|
51
51
|
onboardingCompleted: false,
|
|
52
52
|
proactiveKeepaliveEnabled: true,
|
|
53
|
+
keepaliveDetachedSessions: false,
|
|
53
54
|
wedgeKillEnabled: true,
|
|
54
55
|
silenceProbeEnabled: true,
|
|
55
56
|
})),
|
|
@@ -249,6 +250,7 @@ describe("SessionOrchestrator", () => {
|
|
|
249
250
|
openaiApiKey: "",
|
|
250
251
|
onboardingCompleted: false,
|
|
251
252
|
proactiveKeepaliveEnabled: true,
|
|
253
|
+
keepaliveDetachedSessions: false,
|
|
252
254
|
wedgeKillEnabled: true,
|
|
253
255
|
silenceProbeEnabled: true,
|
|
254
256
|
} as any);
|
|
@@ -434,6 +436,102 @@ describe("SessionOrchestrator", () => {
|
|
|
434
436
|
|
|
435
437
|
// ── Crash-loop relaunch budget ────────────────────────────────────────────
|
|
436
438
|
|
|
439
|
+
// ── Browser-aware keepalive ───────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Relaunching a session nobody has open buys latency nobody is waiting for,
|
|
443
|
+
* while re-initialising that session's MCP servers from scratch every time.
|
|
444
|
+
* Measured on a 4-core host: 90 relaunches in 21 minutes drove the load
|
|
445
|
+
* average to 36, starving the live sessions into failing and producing more
|
|
446
|
+
* relaunches. Detached sessions revive on demand instead — typing into a
|
|
447
|
+
* terminated session already requests a relaunch and flushes the queued
|
|
448
|
+
* message on attach.
|
|
449
|
+
*/
|
|
450
|
+
describe("keepalive is browser-aware", () => {
|
|
451
|
+
async function crash() {
|
|
452
|
+
companionBus.emit("session:exited", { sessionId: "s1", exitCode: 143 });
|
|
453
|
+
await vi.advanceTimersByTimeAsync(45_000);
|
|
454
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
beforeEach(() => {
|
|
458
|
+
deps.launcher.getSession.mockReturnValue({ archived: false, state: "exited", pid: undefined } as any);
|
|
459
|
+
deps.launcher.relaunch.mockResolvedValue({ ok: true });
|
|
460
|
+
deps.wsBridge.isCliConnected.mockReturnValue(false);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("does NOT relaunch a session with no browser attached", async () => {
|
|
464
|
+
vi.useFakeTimers();
|
|
465
|
+
deps.wsBridge.getSession.mockReturnValue({ browserSockets: new Set() } as any);
|
|
466
|
+
orchestrator.initialize();
|
|
467
|
+
|
|
468
|
+
await crash();
|
|
469
|
+
|
|
470
|
+
expect(deps.launcher.relaunch).not.toHaveBeenCalled();
|
|
471
|
+
vi.useRealTimers();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("relaunches a session someone has open", async () => {
|
|
475
|
+
vi.useFakeTimers();
|
|
476
|
+
deps.wsBridge.getSession.mockReturnValue({ browserSockets: new Set(["ws1"]) } as any);
|
|
477
|
+
orchestrator.initialize();
|
|
478
|
+
|
|
479
|
+
await crash();
|
|
480
|
+
|
|
481
|
+
expect(deps.launcher.relaunch).toHaveBeenCalled();
|
|
482
|
+
vi.useRealTimers();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("always relaunches agent/cron sessions, which run detached by design", async () => {
|
|
486
|
+
// For these a dead CLI means dropped work, not just a slower first reply.
|
|
487
|
+
vi.useFakeTimers();
|
|
488
|
+
deps.launcher.getSession.mockReturnValue({
|
|
489
|
+
archived: false, state: "exited", pid: undefined, agentId: "agent-7",
|
|
490
|
+
} as any);
|
|
491
|
+
deps.wsBridge.getSession.mockReturnValue({ browserSockets: new Set() } as any);
|
|
492
|
+
orchestrator.initialize();
|
|
493
|
+
|
|
494
|
+
await crash();
|
|
495
|
+
|
|
496
|
+
expect(deps.launcher.relaunch).toHaveBeenCalled();
|
|
497
|
+
vi.useRealTimers();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("relaunches detached sessions when the setting opts back in", async () => {
|
|
501
|
+
vi.useFakeTimers();
|
|
502
|
+
const { getSettings } = await import("./settings-manager.js");
|
|
503
|
+
const prior = (getSettings as any).getMockImplementation();
|
|
504
|
+
// The gate reads getSettings() more than once per relaunch decision, so
|
|
505
|
+
// this has to hold for the whole cycle rather than a single call.
|
|
506
|
+
(getSettings as any).mockReturnValue({
|
|
507
|
+
proactiveKeepaliveEnabled: true,
|
|
508
|
+
keepaliveDetachedSessions: true,
|
|
509
|
+
wedgeKillEnabled: true,
|
|
510
|
+
silenceProbeEnabled: true,
|
|
511
|
+
});
|
|
512
|
+
deps.wsBridge.getSession.mockReturnValue({ browserSockets: new Set() } as any);
|
|
513
|
+
orchestrator.initialize();
|
|
514
|
+
|
|
515
|
+
await crash();
|
|
516
|
+
|
|
517
|
+
expect(deps.launcher.relaunch).toHaveBeenCalled();
|
|
518
|
+
(getSettings as any).mockImplementation(prior);
|
|
519
|
+
vi.useRealTimers();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("fails open when the bridge has no record of the session", async () => {
|
|
523
|
+
// A bookkeeping gap must never silently cost a session its recovery.
|
|
524
|
+
vi.useFakeTimers();
|
|
525
|
+
deps.wsBridge.getSession.mockReturnValue(null as any);
|
|
526
|
+
orchestrator.initialize();
|
|
527
|
+
|
|
528
|
+
await crash();
|
|
529
|
+
|
|
530
|
+
expect(deps.launcher.relaunch).toHaveBeenCalled();
|
|
531
|
+
vi.useRealTimers();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
437
535
|
describe("auto-relaunch crash budget", () => {
|
|
438
536
|
// Drives one crash→proactive-relaunch cycle: the CLI exits, the keepalive
|
|
439
537
|
// timer fires, handleAutoRelaunch waits out its grace + cooldown, and the
|
|
@@ -1835,6 +1933,7 @@ describe("SessionOrchestrator", () => {
|
|
|
1835
1933
|
vi.mocked(settingsManager.getSettings).mockReturnValue({
|
|
1836
1934
|
...settingsManager.getSettings(),
|
|
1837
1935
|
proactiveKeepaliveEnabled: false,
|
|
1936
|
+
keepaliveDetachedSessions: false,
|
|
1838
1937
|
wedgeKillEnabled: true,
|
|
1839
1938
|
silenceProbeEnabled: true,
|
|
1840
1939
|
});
|
|
@@ -982,14 +982,14 @@ export class SessionOrchestrator {
|
|
|
982
982
|
// ── Private: Proactive keepalive ────────────────────────────────────────────
|
|
983
983
|
|
|
984
984
|
/**
|
|
985
|
-
* Schedules a proactive relaunch of a crashed CLI process
|
|
986
|
-
*
|
|
987
|
-
* based on the auto-relaunch attempt count.
|
|
985
|
+
* Schedules a proactive relaunch of a crashed CLI process. Uses exponential
|
|
986
|
+
* backoff (3s, 6s, 12s) based on the auto-relaunch attempt count.
|
|
988
987
|
*
|
|
989
988
|
* Skips relaunch for:
|
|
990
989
|
* - Intentional kills (idle-kill, manual delete/archive)
|
|
991
990
|
* - Archived sessions
|
|
992
991
|
* - Sessions that have exhausted their relaunch budget
|
|
992
|
+
* - Detached sessions, unless `keepaliveDetachedSessions` is on (see below)
|
|
993
993
|
*/
|
|
994
994
|
private scheduleProactiveRelaunch(sessionId: string): void {
|
|
995
995
|
// Respect the global kill-switch — lets operators experiment with letting
|
|
@@ -1006,6 +1006,35 @@ export class SessionOrchestrator {
|
|
|
1006
1006
|
const info = this.launcher.getSession(sessionId);
|
|
1007
1007
|
if (!info || info.archived) return;
|
|
1008
1008
|
|
|
1009
|
+
// Detached sessions: nobody is watching, so nobody is waiting on the
|
|
1010
|
+
// latency this relaunch buys. Relaunching them anyway is what turns a
|
|
1011
|
+
// handful of crashes into a load spike — every relaunch re-initialises the
|
|
1012
|
+
// session's MCP servers from scratch (`npm exec` + node boot, plus chromium
|
|
1013
|
+
// for @playwright/mcp). Measured on a 4-core host: 90 relaunches in 21
|
|
1014
|
+
// minutes drove the load average to 36, which starved the *live* sessions
|
|
1015
|
+
// into failing, producing more relaunches.
|
|
1016
|
+
//
|
|
1017
|
+
// Nothing is lost by waiting. Typing into a terminated session already
|
|
1018
|
+
// requests a relaunch on demand (`terminated -> starting` is legal, and the
|
|
1019
|
+
// queued message flushes on attach), which is the same path that replays an
|
|
1020
|
+
// in-flight turn after a wedge-kill. The cost is a one-time resume on first
|
|
1021
|
+
// message instead of a permanent warm process.
|
|
1022
|
+
//
|
|
1023
|
+
// Agent- and cron-spawned sessions are exempt: they legitimately run with no
|
|
1024
|
+
// browser attached, and for them a dead CLI means dropped work, not just a
|
|
1025
|
+
// slower first reply.
|
|
1026
|
+
// Fails open: only a session the bridge positively reports as having zero
|
|
1027
|
+
// browsers is treated as detached. If the bridge has no record of it we
|
|
1028
|
+
// relaunch as before, so a bookkeeping gap can never silently cost a session
|
|
1029
|
+
// its recovery.
|
|
1030
|
+
if (!info.agentId && !getSettings().keepaliveDetachedSessions) {
|
|
1031
|
+
const tracked = this.wsBridge.getSession(sessionId);
|
|
1032
|
+
if (tracked && tracked.browserSockets.size === 0) {
|
|
1033
|
+
log.info("orchestrator", "Session detached; deferring relaunch until it is opened", { sessionId });
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1009
1038
|
// Skip if already at relaunch limit
|
|
1010
1039
|
if (this.relaunchExhaustedNotified.has(sessionId)) return;
|
|
1011
1040
|
|
|
@@ -53,6 +53,7 @@ describe("settings-manager", () => {
|
|
|
53
53
|
updateChannel: "stable",
|
|
54
54
|
dockerAutoUpdate: false,
|
|
55
55
|
proactiveKeepaliveEnabled: true,
|
|
56
|
+
keepaliveDetachedSessions: false,
|
|
56
57
|
wedgeKillEnabled: true,
|
|
57
58
|
silenceProbeEnabled: true,
|
|
58
59
|
cliBridgeMode: "loopback",
|
|
@@ -119,6 +120,7 @@ describe("settings-manager", () => {
|
|
|
119
120
|
updateChannel: "stable",
|
|
120
121
|
dockerAutoUpdate: false,
|
|
121
122
|
proactiveKeepaliveEnabled: true,
|
|
123
|
+
keepaliveDetachedSessions: false,
|
|
122
124
|
wedgeKillEnabled: true,
|
|
123
125
|
silenceProbeEnabled: true,
|
|
124
126
|
cliBridgeMode: "loopback",
|
|
@@ -211,6 +213,7 @@ describe("settings-manager", () => {
|
|
|
211
213
|
updateChannel: "stable",
|
|
212
214
|
dockerAutoUpdate: false,
|
|
213
215
|
proactiveKeepaliveEnabled: true,
|
|
216
|
+
keepaliveDetachedSessions: false,
|
|
214
217
|
wedgeKillEnabled: true,
|
|
215
218
|
silenceProbeEnabled: true,
|
|
216
219
|
cliBridgeMode: "loopback",
|
|
@@ -92,6 +92,16 @@ export interface CompanionSettings {
|
|
|
92
92
|
* cron) alive. Disable to experiment with letting dead sessions stay dead.
|
|
93
93
|
*/
|
|
94
94
|
proactiveKeepaliveEnabled: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Also keep *detached* sessions (no browser attached) auto-relaunching.
|
|
97
|
+
*
|
|
98
|
+
* Off by default: relaunching a session nobody has open buys latency nobody
|
|
99
|
+
* is waiting for, while re-initialising its MCP servers each time. On a busy
|
|
100
|
+
* multi-session host that dominates CPU. Detached sessions still revive on
|
|
101
|
+
* demand the moment a message is sent to them. Agent/cron sessions are exempt
|
|
102
|
+
* from this gate and always keep alive.
|
|
103
|
+
*/
|
|
104
|
+
keepaliveDetachedSessions: boolean;
|
|
95
105
|
/**
|
|
96
106
|
* When true (default), a CLI whose stdout closes but which does not exit is
|
|
97
107
|
* treated as wedged and killed so recovery can proceed.
|
|
@@ -158,6 +168,7 @@ let settings: CompanionSettings = {
|
|
|
158
168
|
updateChannel: "stable",
|
|
159
169
|
dockerAutoUpdate: false,
|
|
160
170
|
proactiveKeepaliveEnabled: true,
|
|
171
|
+
keepaliveDetachedSessions: false,
|
|
161
172
|
wedgeKillEnabled: true,
|
|
162
173
|
silenceProbeEnabled: true,
|
|
163
174
|
cliBridgeMode: "loopback",
|
|
@@ -211,6 +222,7 @@ function normalize(raw: Partial<CompanionSettings> | null | undefined): Companio
|
|
|
211
222
|
updateChannel: raw?.updateChannel === "prerelease" ? "prerelease" : "stable",
|
|
212
223
|
dockerAutoUpdate: typeof raw?.dockerAutoUpdate === "boolean" ? raw.dockerAutoUpdate : false,
|
|
213
224
|
proactiveKeepaliveEnabled: typeof raw?.proactiveKeepaliveEnabled === "boolean" ? raw.proactiveKeepaliveEnabled : true,
|
|
225
|
+
keepaliveDetachedSessions: typeof raw?.keepaliveDetachedSessions === "boolean" ? raw.keepaliveDetachedSessions : false,
|
|
214
226
|
wedgeKillEnabled: typeof raw?.wedgeKillEnabled === "boolean" ? raw.wedgeKillEnabled : true,
|
|
215
227
|
silenceProbeEnabled: typeof raw?.silenceProbeEnabled === "boolean" ? raw.silenceProbeEnabled : true,
|
|
216
228
|
cliBridgeMode: raw?.cliBridgeMode === "jsonHandoff" ? "jsonHandoff" : "loopback",
|
|
@@ -246,7 +258,7 @@ export function getSettings(): CompanionSettings {
|
|
|
246
258
|
}
|
|
247
259
|
|
|
248
260
|
export function updateSettings(
|
|
249
|
-
patch: Partial<Pick<CompanionSettings, "anthropicApiKey" | "anthropicModel" | "claudeCodeOAuthToken" | "openaiApiKey" | "onboardingCompleted" | "linearApiKey" | "linearAutoTransition" | "linearAutoTransitionStateId" | "linearAutoTransitionStateName" | "linearArchiveTransition" | "linearArchiveTransitionStateId" | "linearArchiveTransitionStateName" | "linearOAuthClientId" | "linearOAuthClientSecret" | "linearOAuthWebhookSecret" | "linearOAuthAccessToken" | "linearOAuthRefreshToken" | "aiValidationEnabled" | "aiValidationAutoApprove" | "aiValidationAutoDeny" | "dashboardEnabled" | "dashboardModel" | "dashboardRunHour" | "dashboardMaxSessionsPerRun" | "publicUrl" | "updateChannel" | "dockerAutoUpdate" | "proactiveKeepaliveEnabled" | "wedgeKillEnabled" | "silenceProbeEnabled" | "cliBridgeMode" | "claudeBridgeMode" | "claudeBridgeIngressUrl" | "claudeCompatBannerDismissedVersion">>,
|
|
261
|
+
patch: Partial<Pick<CompanionSettings, "anthropicApiKey" | "anthropicModel" | "claudeCodeOAuthToken" | "openaiApiKey" | "onboardingCompleted" | "linearApiKey" | "linearAutoTransition" | "linearAutoTransitionStateId" | "linearAutoTransitionStateName" | "linearArchiveTransition" | "linearArchiveTransitionStateId" | "linearArchiveTransitionStateName" | "linearOAuthClientId" | "linearOAuthClientSecret" | "linearOAuthWebhookSecret" | "linearOAuthAccessToken" | "linearOAuthRefreshToken" | "aiValidationEnabled" | "aiValidationAutoApprove" | "aiValidationAutoDeny" | "dashboardEnabled" | "dashboardModel" | "dashboardRunHour" | "dashboardMaxSessionsPerRun" | "publicUrl" | "updateChannel" | "dockerAutoUpdate" | "proactiveKeepaliveEnabled" | "keepaliveDetachedSessions" | "wedgeKillEnabled" | "silenceProbeEnabled" | "cliBridgeMode" | "claudeBridgeMode" | "claudeBridgeIngressUrl" | "claudeCompatBannerDismissedVersion">>,
|
|
250
262
|
): CompanionSettings {
|
|
251
263
|
ensureLoaded();
|
|
252
264
|
settings = normalize({
|
|
@@ -278,6 +290,7 @@ export function updateSettings(
|
|
|
278
290
|
updateChannel: patch.updateChannel ?? settings.updateChannel,
|
|
279
291
|
dockerAutoUpdate: patch.dockerAutoUpdate ?? settings.dockerAutoUpdate,
|
|
280
292
|
proactiveKeepaliveEnabled: patch.proactiveKeepaliveEnabled ?? settings.proactiveKeepaliveEnabled,
|
|
293
|
+
keepaliveDetachedSessions: patch.keepaliveDetachedSessions ?? settings.keepaliveDetachedSessions,
|
|
281
294
|
wedgeKillEnabled: patch.wedgeKillEnabled ?? settings.wedgeKillEnabled,
|
|
282
295
|
silenceProbeEnabled: patch.silenceProbeEnabled ?? settings.silenceProbeEnabled,
|
|
283
296
|
cliBridgeMode: patch.cliBridgeMode ?? settings.cliBridgeMode,
|
|
@@ -140,6 +140,7 @@ describe("attachCodexAdapterHandlers", () => {
|
|
|
140
140
|
updateChannel: "stable",
|
|
141
141
|
dockerAutoUpdate: false,
|
|
142
142
|
proactiveKeepaliveEnabled: true,
|
|
143
|
+
keepaliveDetachedSessions: false,
|
|
143
144
|
wedgeKillEnabled: true,
|
|
144
145
|
silenceProbeEnabled: true,
|
|
145
146
|
updatedAt: 0,
|
|
@@ -1133,6 +1134,7 @@ describe("attachCodexAdapterHandlers", () => {
|
|
|
1133
1134
|
updateChannel: "stable",
|
|
1134
1135
|
dockerAutoUpdate: false,
|
|
1135
1136
|
proactiveKeepaliveEnabled: true,
|
|
1137
|
+
keepaliveDetachedSessions: false,
|
|
1136
1138
|
wedgeKillEnabled: true,
|
|
1137
1139
|
silenceProbeEnabled: true,
|
|
1138
1140
|
updatedAt: 0,
|
|
@@ -1316,6 +1318,7 @@ describe("attachCodexAdapterHandlers", () => {
|
|
|
1316
1318
|
updateChannel: "stable",
|
|
1317
1319
|
dockerAutoUpdate: false,
|
|
1318
1320
|
proactiveKeepaliveEnabled: true,
|
|
1321
|
+
keepaliveDetachedSessions: false,
|
|
1319
1322
|
wedgeKillEnabled: true,
|
|
1320
1323
|
silenceProbeEnabled: true,
|
|
1321
1324
|
updatedAt: 0,
|
|
@@ -1369,6 +1372,7 @@ describe("attachCodexAdapterHandlers", () => {
|
|
|
1369
1372
|
updateChannel: "stable",
|
|
1370
1373
|
dockerAutoUpdate: false,
|
|
1371
1374
|
proactiveKeepaliveEnabled: true,
|
|
1375
|
+
keepaliveDetachedSessions: false,
|
|
1372
1376
|
wedgeKillEnabled: true,
|
|
1373
1377
|
silenceProbeEnabled: true,
|
|
1374
1378
|
updatedAt: 0,
|
|
@@ -1487,6 +1491,7 @@ describe("attachCodexAdapterHandlers", () => {
|
|
|
1487
1491
|
updateChannel: "stable",
|
|
1488
1492
|
dockerAutoUpdate: false,
|
|
1489
1493
|
proactiveKeepaliveEnabled: true,
|
|
1494
|
+
keepaliveDetachedSessions: false,
|
|
1490
1495
|
wedgeKillEnabled: true,
|
|
1491
1496
|
silenceProbeEnabled: true,
|
|
1492
1497
|
updatedAt: 0,
|
|
@@ -1624,6 +1629,7 @@ describe("attachCodexAdapterHandlers", () => {
|
|
|
1624
1629
|
updateChannel: "stable",
|
|
1625
1630
|
dockerAutoUpdate: false,
|
|
1626
1631
|
proactiveKeepaliveEnabled: true,
|
|
1632
|
+
keepaliveDetachedSessions: false,
|
|
1627
1633
|
wedgeKillEnabled: true,
|
|
1628
1634
|
silenceProbeEnabled: true,
|
|
1629
1635
|
updatedAt: 0,
|