@hellcoder/companion 0.113.1-preview.20260728154019.dc990d1 → 0.113.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellcoder/companion",
3
- "version": "0.113.1-preview.20260728154019.dc990d1",
3
+ "version": "0.113.1",
4
4
  "type": "module",
5
5
  "description": "Web UI for launching and interacting with Claude Code agents — Moritz Edition (fork of the-companion)",
6
6
  "license": "MIT",
@@ -1812,6 +1812,10 @@ describe("stdio silence probe", () => {
1812
1812
  adapter.attachStdio(proc);
1813
1813
  expect(adapter.isConnected()).toBe(true);
1814
1814
 
1815
+ // A turn must be outstanding: the probe no longer escalates on an idle
1816
+ // session, because silence there just means the user has not typed.
1817
+ adapter.send({ type: "user_message", content: "do something" });
1818
+
1815
1819
  // Silence short of the probe threshold: nothing happens yet.
1816
1820
  await vi.advanceTimersByTimeAsync(110_000);
1817
1821
  expect(disconnectCb).not.toHaveBeenCalled();
@@ -1893,12 +1897,58 @@ describe("stdio silence probe", () => {
1893
1897
  expect(adapter.isConnected()).toBe(true);
1894
1898
  });
1895
1899
 
1900
+ /**
1901
+ * Idle sessions must never be torn down by the probe.
1902
+ *
1903
+ * Silence on an idle session means the user has not typed — indistinguishable
1904
+ * from a dead turn by elapsed time alone. Escalating there produced "Response
1905
+ * interrupted — the backend disconnected" banners for turns that never
1906
+ * existed, which is most of the disconnect noise users report. Observed on
1907
+ * this very session: `CLI silent; sending liveness probe silentForMs=133784`
1908
+ * while simply waiting for input.
1909
+ */
1910
+ it("never escalates on an idle session with no turn outstanding", async () => {
1911
+ vi.useFakeTimers();
1912
+ const { proc } = createMockProc();
1913
+ adapter.attachStdio(proc);
1914
+
1915
+ // Ten minutes of silence with no user message ever sent.
1916
+ await vi.advanceTimersByTimeAsync(600_000);
1917
+
1918
+ expect(disconnectCb).not.toHaveBeenCalled();
1919
+ expect(adapter.isConnected()).toBe(true);
1920
+ });
1921
+
1922
+ it("stops escalating once the turn completes", async () => {
1923
+ // The turn ends mid-silence: what was a recoverable stall becomes an idle
1924
+ // session, and must stop being a kill candidate.
1925
+ vi.useFakeTimers();
1926
+ const { proc, pushStdout } = createMockProc();
1927
+ adapter.attachStdio(proc);
1928
+ adapter.send({ type: "user_message", content: "quick question" });
1929
+
1930
+ pushStdout(JSON.stringify({
1931
+ type: "result", subtype: "success", is_error: false,
1932
+ duration_ms: 5, num_turns: 1, session_id: "s", uuid: "r-idle",
1933
+ }) + "\n");
1934
+ await vi.advanceTimersByTimeAsync(10);
1935
+
1936
+ await vi.advanceTimersByTimeAsync(600_000);
1937
+
1938
+ expect(disconnectCb).not.toHaveBeenCalled();
1939
+ expect(adapter.isConnected()).toBe(true);
1940
+ });
1941
+
1896
1942
  it("still escalates when a tool call blows the grace ceiling", async () => {
1897
1943
  // A tool that never returns must not suppress recovery forever.
1898
1944
  vi.useFakeTimers();
1899
1945
  const { proc, pushStdout } = createMockProc();
1900
1946
  adapter.attachStdio(proc);
1901
1947
 
1948
+ // Establish an outstanding turn — escalation is gated on one, so that an
1949
+ // idle session is never torn down for simply having nothing to say.
1950
+ adapter.send({ type: "user_message", content: "run a tool" });
1951
+
1902
1952
  pushStdout(
1903
1953
  JSON.stringify({
1904
1954
  type: "assistant",
@@ -665,6 +665,21 @@ export class ClaudeAdapter implements IBackendAdapter {
665
665
  return;
666
666
  }
667
667
 
668
+ // Nothing to rescue when no turn is outstanding. An idle session is silent
669
+ // because the user has not typed, which is indistinguishable from a dead
670
+ // turn by elapsed time alone — so probing it produced "Response interrupted
671
+ // — the backend disconnected" banners for turns that never existed. That is
672
+ // most of the disconnect noise users report.
673
+ //
674
+ // Recovery is not lost: a message sent to a terminated session already
675
+ // requests a relaunch and flushes on attach, so a dead idle CLI is revived
676
+ // on demand rather than pre-emptively. Same trade as the browser-aware
677
+ // keepalive — pay the cost when someone is actually waiting.
678
+ if (!this.turnInFlight) {
679
+ this.probeSentAt = null;
680
+ return;
681
+ }
682
+
668
683
  // An unanswered probe is the actual failure signal: a healthy CLI replies
669
684
  // to control requests promptly even while working.
670
685
  if (this.probeSentAt !== null) {