@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
|
|
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",
|
package/server/claude-adapter.ts
CHANGED
|
@@ -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) {
|