switchroom 0.21.17 → 0.21.19
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/dist/cli/switchroom.js +1884 -1512
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/skills/switchroom-release/SKILL.md +6 -1
- package/telegram-plugin/connection-drop.ts +83 -0
- package/telegram-plugin/dist/bridge/bridge.js +41 -2
- package/telegram-plugin/dist/gateway/gateway.js +48 -6
- package/telegram-plugin/dist/server.js +46 -3
- package/telegram-plugin/llm-error-present.ts +25 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +6 -0
- package/telegram-plugin/session-tail.ts +38 -1
- package/telegram-plugin/tests/llm-error-present.test.ts +183 -0
- package/telegram-plugin/uat/flip/allowlist.test.ts +229 -0
- package/telegram-plugin/uat/flip/allowlist.ts +349 -0
- package/telegram-plugin/uat/flip/gate.test.ts +153 -0
- package/telegram-plugin/uat/flip/gate.ts +232 -0
- package/telegram-plugin/uat/flip/probe-scoring.test.ts +210 -0
- package/telegram-plugin/uat/flip/probe-scoring.ts +200 -0
- package/telegram-plugin/uat/flip/probe-suite.test.ts +95 -0
- package/telegram-plugin/uat/flip/probe-suite.ts +155 -0
- package/telegram-plugin/uat/flip/probes/kdogg.probes.json +36 -0
- package/telegram-plugin/uat/flip/probes/test-harness.probes.json +15 -0
- package/telegram-plugin/uat/flip/recall-log.test.ts +131 -0
- package/telegram-plugin/uat/flip/recall-log.ts +178 -0
- package/telegram-plugin/uat/flip/report.ts +95 -0
- package/telegram-plugin/uat/flip/tier1-equivalence.test.ts +470 -0
- package/telegram-plugin/uat/flip/tier1-equivalence.ts +697 -0
- package/telegram-plugin/uat/flip/tier2-probe-runner.ts +327 -0
- package/telegram-plugin/uat/runners/scorer.ts +1 -1
- package/vendor/hindsight-memory/scripts/prefetch.py +33 -5
- package/vendor/hindsight-memory/scripts/recall.py +131 -17
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +97 -6
- package/vendor/hindsight-memory/scripts/tests/test_recall_buffer_join.py +70 -0
|
@@ -21598,7 +21598,7 @@ function allocateAgentUid(name) {
|
|
|
21598
21598
|
}
|
|
21599
21599
|
|
|
21600
21600
|
// src/build-info.ts
|
|
21601
|
-
var VERSION = "0.21.
|
|
21601
|
+
var VERSION = "0.21.19";
|
|
21602
21602
|
|
|
21603
21603
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21604
21604
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.19",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -63,7 +63,12 @@ gh release create "vX.Y.Z" -R switchroom/switchroom \
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
- **`--draft` is mandatory.** A published release with no assets immediately becomes `/releases/latest` and 404s every `curl | sh` install for the entire ~25-minute build window. `release.yml` will forcibly re-draft an incomplete published release within about a minute, but do not rely on the safety net — it exists for the case where this step was done wrong.
|
|
66
|
-
- Creating the release object *before* the tag is
|
|
66
|
+
- **Creating the release object *before* the tag is effectively REQUIRED, not belt-and-suspenders.** `release.yml`'s `guard` job polls ~150s for the release to appear, and if it never does it auto-creates the draft from `CHANGELOG.md` (#4331). But that auto-create routinely lands **too late for the same `release` run's completeness check**: the draft is created at the tail of the poll window, the check re-reads the release list in the *same* job, still cannot see it, exits 4, and **fails the run** — `npm`/`publish`/`finalize` all `needs:` this job, so they SKIP and nothing ships. This is not hypothetical: on **v0.21.18** (`release` run `32095648269`, attempt 1) the tag was pushed with no draft, `guard` retried 10× over ~150s, logged `auto-created draft release v0.21.18 from CHANGELOG.md`, then still exited 4 (`auto-created v0.21.18 but the completeness check still cannot see it`) and the job failed at `03:33:23Z` — npm/publish/latest all SKIPPED. So do not read #4331 as "skipping this step is fine": create the draft here, on the pinned SHA, before the tag push. It also lets you author a title/notes that differ from the raw CHANGELOG section.
|
|
67
|
+
- **If the tag is already pushed without a draft and `guard` has failed, the fix-forward is a re-run, not a re-cut.** The auto-create has by then left a real draft object behind, so re-run the failed jobs — the guard's completeness check now sees the draft and passes:
|
|
68
|
+
```bash
|
|
69
|
+
gh run rerun <release-run-id> --failed -R switchroom/switchroom
|
|
70
|
+
```
|
|
71
|
+
On the second attempt `guard` passes → binaries attach → npm publishes → the GitHub Release un-drafts → `:vX.Y.Z` promotes to `:latest`. This is exactly how v0.21.18 recovered (attempt 2 published `03:37:03Z`). Do NOT cut a new patch version to route around a failed guard — the release for this tag already exists as a draft.
|
|
67
72
|
- **Notes extraction gotcha (historical):** the naive `awk '/^## vX/,/^## v/' CHANGELOG` range collapses to a single line. Use a start-flag awk: `awk 'f{print} /^## vX\.Y\.Z/{print; f=1} f && /^## v/ && !/^## vX\.Y\.Z/{exit}'` — or extract the section to a temp file by line range.
|
|
68
73
|
- **`gh release create` has been silently dropped in past runs.** Verify it exists and is a draft:
|
|
69
74
|
```bash
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection-drop.ts — the ONE canonical "was this a mid-stream connection /
|
|
3
|
+
* SSE drop?" wording matcher.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM this closes
|
|
6
|
+
* -----------------------
|
|
7
|
+
* Two independent classifiers historically disagreed about what a mid-stream
|
|
8
|
+
* "connection lost" / SSE-drop is, and they are consulted on different code
|
|
9
|
+
* paths:
|
|
10
|
+
*
|
|
11
|
+
* - Path A — `parseLlmError` (llm-error-present.ts) → `detectModelUnavailable`
|
|
12
|
+
* (model-unavailable.ts): a network-drop wording maps to
|
|
13
|
+
* `{kind:'transient', source:'network'}`.
|
|
14
|
+
* - Path B — the sub-agent transcript path → `detectErrorInTranscriptLine`
|
|
15
|
+
* (session-tail.ts) → `classifyClaudeError` (operator-events.ts): a
|
|
16
|
+
* connection-drop-worded line that is NOT the exact `server_error` /
|
|
17
|
+
* `api_error` type falls through to `unknown-5xx`/`unknown-4xx` (a generic
|
|
18
|
+
* terminal), because that path never consulted the network-drop wording
|
|
19
|
+
* list.
|
|
20
|
+
*
|
|
21
|
+
* So the SAME dropped-stream line was a "transient network" event on one path
|
|
22
|
+
* and a "generic terminal" event on the other. This module gives both paths a
|
|
23
|
+
* single predicate to consult so a connection-drop is identifiable as such on
|
|
24
|
+
* BOTH.
|
|
25
|
+
*
|
|
26
|
+
* SCOPE: this is a WORDING matcher only. It answers "does this text look like a
|
|
27
|
+
* transport connection drop?" — it deliberately does NOT decide the final
|
|
28
|
+
* error kind. Callers gate the raw predicate on their own classification (only
|
|
29
|
+
* flagging a drop on the transient/unknown/transport families, never on a
|
|
30
|
+
* positively-identified auth/quota/credit wall) so a wrapped auth/quota error
|
|
31
|
+
* whose OUTER text happens to say "fetch failed" is never mislabelled a drop.
|
|
32
|
+
*
|
|
33
|
+
* Pure module: no IPC, no bot, no FS. Trivially unit-testable.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Canonical connection-drop / SSE-drop wordings (lowercase substrings, matched
|
|
38
|
+
* case-insensitively). Provenance for each entry is in the PR description; the
|
|
39
|
+
* list is deliberately CONSERVATIVE — it covers transport-layer connection
|
|
40
|
+
* drops only and must NOT match auth / quota / overload / provider-credit
|
|
41
|
+
* wording (verified by the negative tests). Bare `stream` / `terminated` are
|
|
42
|
+
* intentionally EXCLUDED: `upstream`/`downstream` contain `stream`, and
|
|
43
|
+
* `terminated` appears in unrelated contexts — both would over-match and risk a
|
|
44
|
+
* false auto-resume in the later PRs that gate on this predicate.
|
|
45
|
+
*
|
|
46
|
+
* DNS-resolution failures (`enotfound`, `eai_again`, `getaddrinfo`, "request
|
|
47
|
+
* timed out") are also excluded: a name-resolution failure is a network fault
|
|
48
|
+
* but NOT a connection drop (no connection was ever established). Path A still
|
|
49
|
+
* classifies those as its broader `network` kind, unchanged — they simply do
|
|
50
|
+
* not set the connection-drop discriminator.
|
|
51
|
+
*/
|
|
52
|
+
export const CONNECTION_DROP_SIGNALS: readonly string[] = [
|
|
53
|
+
// ── Node/undici transport error codes (worded + code forms) ──────────────
|
|
54
|
+
'socket hang up', // model-unavailable.ts networkSignals; mcp-credential-failure.ts
|
|
55
|
+
'econnreset', // model-unavailable.ts networkSignals; retry-api-call.ts; ubiquitous fixtures
|
|
56
|
+
'econnrefused', // model-unavailable.ts networkSignals
|
|
57
|
+
'etimedout', // model-unavailable.ts networkSignals; retry-api-call.ts
|
|
58
|
+
'epipe', // broken pipe on a half-dead socket (tests: pending-card-expiry, boot-sweep-gate)
|
|
59
|
+
'fetch failed', // model-unavailable.ts networkSignals; retry-api-call.ts
|
|
60
|
+
'network error', // model-unavailable.ts networkSignals
|
|
61
|
+
// ── Worded connection-lost forms ─────────────────────────────────────────
|
|
62
|
+
'connection refused', // model-unavailable.ts networkSignals
|
|
63
|
+
'connection reset', // worded ECONNRESET (retry-api-call.ts comment)
|
|
64
|
+
'connection closed', // model-unavailable.ts networkSignals; canonical "Connection closed mid-response"
|
|
65
|
+
'connection lost', // plausible SSE-drop wording (precise phrase, no over-match risk)
|
|
66
|
+
// ── Mid-response / SSE-drop markers ──────────────────────────────────────
|
|
67
|
+
'mid-response', // model-unavailable.ts networkSignals; "Connection closed mid-response"
|
|
68
|
+
'premature close', // Node/undici stream "Premature close" — a response body that ended early
|
|
69
|
+
'stream disconnected', // plausible SSE-drop wording (precise two-word phrase)
|
|
70
|
+
'stream closed', // plausible SSE-drop wording (precise two-word phrase)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when `text` carries a canonical connection-drop / SSE-drop wording.
|
|
75
|
+
*
|
|
76
|
+
* WORDING ONLY — see the module header. Never throws; a non-string collapses to
|
|
77
|
+
* `false`.
|
|
78
|
+
*/
|
|
79
|
+
export function isConnectionDropText(text: string): boolean {
|
|
80
|
+
if (typeof text !== 'string' || text.length === 0) return false
|
|
81
|
+
const lower = text.toLowerCase()
|
|
82
|
+
return CONNECTION_DROP_SIGNALS.some(s => lower.includes(s))
|
|
83
|
+
}
|
|
@@ -23205,6 +23205,31 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
|
|
|
23205
23205
|
"proxy-misconfig"
|
|
23206
23206
|
]);
|
|
23207
23207
|
|
|
23208
|
+
// connection-drop.ts
|
|
23209
|
+
var CONNECTION_DROP_SIGNALS = [
|
|
23210
|
+
"socket hang up",
|
|
23211
|
+
"econnreset",
|
|
23212
|
+
"econnrefused",
|
|
23213
|
+
"etimedout",
|
|
23214
|
+
"epipe",
|
|
23215
|
+
"fetch failed",
|
|
23216
|
+
"network error",
|
|
23217
|
+
"connection refused",
|
|
23218
|
+
"connection reset",
|
|
23219
|
+
"connection closed",
|
|
23220
|
+
"connection lost",
|
|
23221
|
+
"mid-response",
|
|
23222
|
+
"premature close",
|
|
23223
|
+
"stream disconnected",
|
|
23224
|
+
"stream closed"
|
|
23225
|
+
];
|
|
23226
|
+
function isConnectionDropText(text) {
|
|
23227
|
+
if (typeof text !== "string" || text.length === 0)
|
|
23228
|
+
return false;
|
|
23229
|
+
const lower = text.toLowerCase();
|
|
23230
|
+
return CONNECTION_DROP_SIGNALS.some((s) => lower.includes(s));
|
|
23231
|
+
}
|
|
23232
|
+
|
|
23208
23233
|
// tool-label-sidecar.ts
|
|
23209
23234
|
import { existsSync as existsSync2, readFileSync, statSync as statSync2 } from "node:fs";
|
|
23210
23235
|
import { join as join2 } from "node:path";
|
|
@@ -23677,6 +23702,10 @@ function extractRetryState(obj) {
|
|
|
23677
23702
|
maxRetries: typeof obj.maxRetries === "number" ? obj.maxRetries : null
|
|
23678
23703
|
};
|
|
23679
23704
|
}
|
|
23705
|
+
var CONNECTION_DROP_ELIGIBLE_KINDS = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
23706
|
+
function isConnectionDrop(kind, scanText) {
|
|
23707
|
+
return CONNECTION_DROP_ELIGIBLE_KINDS.has(kind) && isConnectionDropText(scanText);
|
|
23708
|
+
}
|
|
23680
23709
|
function detectErrorInTranscriptLine(line) {
|
|
23681
23710
|
if (!line || line.length > 2 * 1024 * 1024)
|
|
23682
23711
|
return null;
|
|
@@ -23701,7 +23730,9 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
23701
23730
|
raw: obj,
|
|
23702
23731
|
detail: text || errStr || "api error",
|
|
23703
23732
|
transient: kind2 === "rate-limited",
|
|
23704
|
-
terminal: true
|
|
23733
|
+
terminal: true,
|
|
23734
|
+
connectionDrop: isConnectionDrop(kind2, `${text}
|
|
23735
|
+
${errStr}`)
|
|
23705
23736
|
};
|
|
23706
23737
|
}
|
|
23707
23738
|
const isErrorLine = type === "api_error" || type === "error";
|
|
@@ -23714,7 +23745,15 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
23714
23745
|
const transient = kind === "rate-limited" || kind === "transport-transient";
|
|
23715
23746
|
const retry = extractRetryState(obj);
|
|
23716
23747
|
const terminal = !transient ? true : retry.retryAttempt != null && retry.maxRetries != null ? retry.retryAttempt >= retry.maxRetries : isErrorLine;
|
|
23717
|
-
return {
|
|
23748
|
+
return {
|
|
23749
|
+
kind,
|
|
23750
|
+
raw,
|
|
23751
|
+
detail,
|
|
23752
|
+
transient,
|
|
23753
|
+
terminal,
|
|
23754
|
+
connectionDrop: isConnectionDrop(kind, `${detail}
|
|
23755
|
+
${String(type ?? "")}`)
|
|
23756
|
+
};
|
|
23718
23757
|
}
|
|
23719
23758
|
function extractDetailMessage(obj) {
|
|
23720
23759
|
if (!obj)
|
|
@@ -74529,6 +74529,31 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
|
|
|
74529
74529
|
"proxy-misconfig"
|
|
74530
74530
|
]);
|
|
74531
74531
|
|
|
74532
|
+
// connection-drop.ts
|
|
74533
|
+
var CONNECTION_DROP_SIGNALS = [
|
|
74534
|
+
"socket hang up",
|
|
74535
|
+
"econnreset",
|
|
74536
|
+
"econnrefused",
|
|
74537
|
+
"etimedout",
|
|
74538
|
+
"epipe",
|
|
74539
|
+
"fetch failed",
|
|
74540
|
+
"network error",
|
|
74541
|
+
"connection refused",
|
|
74542
|
+
"connection reset",
|
|
74543
|
+
"connection closed",
|
|
74544
|
+
"connection lost",
|
|
74545
|
+
"mid-response",
|
|
74546
|
+
"premature close",
|
|
74547
|
+
"stream disconnected",
|
|
74548
|
+
"stream closed"
|
|
74549
|
+
];
|
|
74550
|
+
function isConnectionDropText(text4) {
|
|
74551
|
+
if (typeof text4 !== "string" || text4.length === 0)
|
|
74552
|
+
return false;
|
|
74553
|
+
const lower = text4.toLowerCase();
|
|
74554
|
+
return CONNECTION_DROP_SIGNALS.some((s) => lower.includes(s));
|
|
74555
|
+
}
|
|
74556
|
+
|
|
74532
74557
|
// session-tail.ts
|
|
74533
74558
|
function sanitizeCwdToProjectName(cwd) {
|
|
74534
74559
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
@@ -74783,6 +74808,7 @@ function projectTranscriptLine(line) {
|
|
|
74783
74808
|
}
|
|
74784
74809
|
return [];
|
|
74785
74810
|
}
|
|
74811
|
+
var CONNECTION_DROP_ELIGIBLE_KINDS = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
74786
74812
|
|
|
74787
74813
|
// pty-tail.ts
|
|
74788
74814
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
@@ -78601,6 +78627,7 @@ function parseLlmError(raw, retryState) {
|
|
|
78601
78627
|
terminal = true;
|
|
78602
78628
|
}
|
|
78603
78629
|
}
|
|
78630
|
+
const connectionDrop = (kind === "transient" || kind === "unknown") && isConnectionDropText(text4);
|
|
78604
78631
|
return {
|
|
78605
78632
|
kind,
|
|
78606
78633
|
coreText: buildCoreText(kind, source),
|
|
@@ -78610,6 +78637,7 @@ function parseLlmError(raw, retryState) {
|
|
|
78610
78637
|
...requestId != null ? { requestId } : {},
|
|
78611
78638
|
...providerId != null ? { providerId } : {},
|
|
78612
78639
|
source,
|
|
78640
|
+
connectionDrop,
|
|
78613
78641
|
autoRetrying,
|
|
78614
78642
|
terminal
|
|
78615
78643
|
};
|
|
@@ -101454,6 +101482,10 @@ function extractRetryState(obj) {
|
|
|
101454
101482
|
maxRetries: typeof obj.maxRetries === "number" ? obj.maxRetries : null
|
|
101455
101483
|
};
|
|
101456
101484
|
}
|
|
101485
|
+
var CONNECTION_DROP_ELIGIBLE_KINDS2 = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
101486
|
+
function isConnectionDrop(kind, scanText) {
|
|
101487
|
+
return CONNECTION_DROP_ELIGIBLE_KINDS2.has(kind) && isConnectionDropText(scanText);
|
|
101488
|
+
}
|
|
101457
101489
|
function detectErrorInTranscriptLine(line) {
|
|
101458
101490
|
if (!line || line.length > 2 * 1024 * 1024)
|
|
101459
101491
|
return null;
|
|
@@ -101478,7 +101510,9 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
101478
101510
|
raw: obj,
|
|
101479
101511
|
detail: text4 || errStr || "api error",
|
|
101480
101512
|
transient: kind2 === "rate-limited",
|
|
101481
|
-
terminal: true
|
|
101513
|
+
terminal: true,
|
|
101514
|
+
connectionDrop: isConnectionDrop(kind2, `${text4}
|
|
101515
|
+
${errStr}`)
|
|
101482
101516
|
};
|
|
101483
101517
|
}
|
|
101484
101518
|
const isErrorLine = type === "api_error" || type === "error";
|
|
@@ -101491,7 +101525,15 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
101491
101525
|
const transient = kind === "rate-limited" || kind === "transport-transient";
|
|
101492
101526
|
const retry = extractRetryState(obj);
|
|
101493
101527
|
const terminal = !transient ? true : retry.retryAttempt != null && retry.maxRetries != null ? retry.retryAttempt >= retry.maxRetries : isErrorLine;
|
|
101494
|
-
return {
|
|
101528
|
+
return {
|
|
101529
|
+
kind,
|
|
101530
|
+
raw,
|
|
101531
|
+
detail,
|
|
101532
|
+
transient,
|
|
101533
|
+
terminal,
|
|
101534
|
+
connectionDrop: isConnectionDrop(kind, `${detail}
|
|
101535
|
+
${String(type ?? "")}`)
|
|
101536
|
+
};
|
|
101495
101537
|
}
|
|
101496
101538
|
function extractDetailMessage(obj) {
|
|
101497
101539
|
if (!obj)
|
|
@@ -105876,10 +105918,10 @@ function startOutboxSweep(deps) {
|
|
|
105876
105918
|
}
|
|
105877
105919
|
|
|
105878
105920
|
// ../src/build-info.ts
|
|
105879
|
-
var VERSION2 = "0.21.
|
|
105880
|
-
var COMMIT_SHA = "
|
|
105881
|
-
var COMMIT_DATE = "2026-08-
|
|
105882
|
-
var LATEST_PR =
|
|
105921
|
+
var VERSION2 = "0.21.19";
|
|
105922
|
+
var COMMIT_SHA = "7e7061ba";
|
|
105923
|
+
var COMMIT_DATE = "2026-08-18T15:36:04Z";
|
|
105924
|
+
var LATEST_PR = 4777;
|
|
105883
105925
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
105884
105926
|
|
|
105885
105927
|
// gateway/boot-version.ts
|
|
@@ -17278,6 +17278,34 @@ var init_operator_events = __esm(() => {
|
|
|
17278
17278
|
]);
|
|
17279
17279
|
});
|
|
17280
17280
|
|
|
17281
|
+
// connection-drop.ts
|
|
17282
|
+
function isConnectionDropText(text) {
|
|
17283
|
+
if (typeof text !== "string" || text.length === 0)
|
|
17284
|
+
return false;
|
|
17285
|
+
const lower = text.toLowerCase();
|
|
17286
|
+
return CONNECTION_DROP_SIGNALS.some((s) => lower.includes(s));
|
|
17287
|
+
}
|
|
17288
|
+
var CONNECTION_DROP_SIGNALS;
|
|
17289
|
+
var init_connection_drop = __esm(() => {
|
|
17290
|
+
CONNECTION_DROP_SIGNALS = [
|
|
17291
|
+
"socket hang up",
|
|
17292
|
+
"econnreset",
|
|
17293
|
+
"econnrefused",
|
|
17294
|
+
"etimedout",
|
|
17295
|
+
"epipe",
|
|
17296
|
+
"fetch failed",
|
|
17297
|
+
"network error",
|
|
17298
|
+
"connection refused",
|
|
17299
|
+
"connection reset",
|
|
17300
|
+
"connection closed",
|
|
17301
|
+
"connection lost",
|
|
17302
|
+
"mid-response",
|
|
17303
|
+
"premature close",
|
|
17304
|
+
"stream disconnected",
|
|
17305
|
+
"stream closed"
|
|
17306
|
+
];
|
|
17307
|
+
});
|
|
17308
|
+
|
|
17281
17309
|
// tool-label-sidecar.ts
|
|
17282
17310
|
import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
|
17283
17311
|
import { join as join3 } from "node:path";
|
|
@@ -17761,6 +17789,9 @@ function extractRetryState(obj) {
|
|
|
17761
17789
|
maxRetries: typeof obj.maxRetries === "number" ? obj.maxRetries : null
|
|
17762
17790
|
};
|
|
17763
17791
|
}
|
|
17792
|
+
function isConnectionDrop(kind, scanText) {
|
|
17793
|
+
return CONNECTION_DROP_ELIGIBLE_KINDS.has(kind) && isConnectionDropText(scanText);
|
|
17794
|
+
}
|
|
17764
17795
|
function detectErrorInTranscriptLine(line) {
|
|
17765
17796
|
if (!line || line.length > 2 * 1024 * 1024)
|
|
17766
17797
|
return null;
|
|
@@ -17785,7 +17816,9 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
17785
17816
|
raw: obj,
|
|
17786
17817
|
detail: text || errStr || "api error",
|
|
17787
17818
|
transient: kind2 === "rate-limited",
|
|
17788
|
-
terminal: true
|
|
17819
|
+
terminal: true,
|
|
17820
|
+
connectionDrop: isConnectionDrop(kind2, `${text}
|
|
17821
|
+
${errStr}`)
|
|
17789
17822
|
};
|
|
17790
17823
|
}
|
|
17791
17824
|
const isErrorLine = type === "api_error" || type === "error";
|
|
@@ -17798,7 +17831,15 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
17798
17831
|
const transient = kind === "rate-limited" || kind === "transport-transient";
|
|
17799
17832
|
const retry = extractRetryState(obj);
|
|
17800
17833
|
const terminal = !transient ? true : retry.retryAttempt != null && retry.maxRetries != null ? retry.retryAttempt >= retry.maxRetries : isErrorLine;
|
|
17801
|
-
return {
|
|
17834
|
+
return {
|
|
17835
|
+
kind,
|
|
17836
|
+
raw,
|
|
17837
|
+
detail,
|
|
17838
|
+
transient,
|
|
17839
|
+
terminal,
|
|
17840
|
+
connectionDrop: isConnectionDrop(kind, `${detail}
|
|
17841
|
+
${String(type ?? "")}`)
|
|
17842
|
+
};
|
|
17802
17843
|
}
|
|
17803
17844
|
function extractDetailMessage(obj) {
|
|
17804
17845
|
if (!obj)
|
|
@@ -18234,13 +18275,15 @@ function startSessionTail(config2) {
|
|
|
18234
18275
|
}
|
|
18235
18276
|
};
|
|
18236
18277
|
}
|
|
18237
|
-
var MAX_JSONL_LINE_BYTES, MAX_ERROR_TEXT_CHARS = 500;
|
|
18278
|
+
var MAX_JSONL_LINE_BYTES, MAX_ERROR_TEXT_CHARS = 500, CONNECTION_DROP_ELIGIBLE_KINDS;
|
|
18238
18279
|
var init_session_tail = __esm(() => {
|
|
18239
18280
|
init_operator_events();
|
|
18240
18281
|
init_model_unavailable();
|
|
18282
|
+
init_connection_drop();
|
|
18241
18283
|
init_tool_label_sidecar();
|
|
18242
18284
|
init_model_label();
|
|
18243
18285
|
MAX_JSONL_LINE_BYTES = 2 * 1024 * 1024;
|
|
18286
|
+
CONNECTION_DROP_ELIGIBLE_KINDS = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
18244
18287
|
});
|
|
18245
18288
|
|
|
18246
18289
|
// ../node_modules/.bun/@xterm+headless@6.0.0/node_modules/@xterm/headless/lib-headless/xterm-headless.js
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type ProviderCreditEntry,
|
|
42
42
|
} from './provider-credit.js'
|
|
43
43
|
import { classifyClaudeError } from './operator-events.js'
|
|
44
|
+
import { isConnectionDropText } from './connection-drop.js'
|
|
44
45
|
import { stripRawErrorBytes, extractRequestId } from './raw-error-scrub.js'
|
|
45
46
|
import { fmtLocalClock, tzAbbrev } from './shared/local-time.js'
|
|
46
47
|
|
|
@@ -94,6 +95,18 @@ export interface ParsedLlmError {
|
|
|
94
95
|
*/
|
|
95
96
|
providerId?: string
|
|
96
97
|
source: LlmErrorSource
|
|
98
|
+
/**
|
|
99
|
+
* True when this error is a mid-stream connection / SSE drop (a transport
|
|
100
|
+
* connection loss), per the canonical {@link isConnectionDropText} matcher.
|
|
101
|
+
* Set consistently on BOTH classification paths (here and
|
|
102
|
+
* `detectErrorInTranscriptLine`) so a later PR can gate auto-resume on ONE
|
|
103
|
+
* reliable discriminator. Only ever true for the `transient`/`unknown`
|
|
104
|
+
* families — never for a positively-identified auth/quota/overload/credit
|
|
105
|
+
* wall (guarded below), so it can never trigger a wrong auto-resume of a
|
|
106
|
+
* genuinely terminal error. Classification only in this PR; no consumer acts
|
|
107
|
+
* on it yet.
|
|
108
|
+
*/
|
|
109
|
+
connectionDrop: boolean
|
|
97
110
|
/** True when the harness is still retrying this error internally (mid-retry). */
|
|
98
111
|
autoRetrying: boolean
|
|
99
112
|
/** True when the failure is final (NOT an in-flight retry). */
|
|
@@ -169,6 +182,17 @@ export function parseLlmError(
|
|
|
169
182
|
}
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
// Connection-drop discriminator. Gated to the transient/unknown families so a
|
|
186
|
+
// positively-classified auth/quota/overload/credit wall is NEVER flagged as a
|
|
187
|
+
// drop, even if its raw text coincidentally carries a drop wording (e.g. a
|
|
188
|
+
// LiteLLM-wrapped 401 whose outer text says "fetch failed"). The gate mirrors
|
|
189
|
+
// Path B's inclusion set (`transport-transient`/`unknown-*`), keeping the two
|
|
190
|
+
// classifiers in agreement. `source==='network'` is not required: a drop
|
|
191
|
+
// wording that `detectModelUnavailable` does not recognise (e.g. `EPIPE`)
|
|
192
|
+
// lands on `unknown` here, and must still be identifiable as a drop.
|
|
193
|
+
const connectionDrop =
|
|
194
|
+
(kind === 'transient' || kind === 'unknown') && isConnectionDropText(text)
|
|
195
|
+
|
|
172
196
|
return {
|
|
173
197
|
kind,
|
|
174
198
|
coreText: buildCoreText(kind, source),
|
|
@@ -178,6 +202,7 @@ export function parseLlmError(
|
|
|
178
202
|
...(requestId != null ? { requestId } : {}),
|
|
179
203
|
...(providerId != null ? { providerId } : {}),
|
|
180
204
|
source,
|
|
205
|
+
connectionDrop,
|
|
181
206
|
autoRetrying,
|
|
182
207
|
terminal,
|
|
183
208
|
}
|
|
@@ -82,6 +82,12 @@ else
|
|
|
82
82
|
uat/load-env.test.ts
|
|
83
83
|
uat/uat-driver.test.ts
|
|
84
84
|
uat/runners/
|
|
85
|
+
# M3 directive-flip UAT — deterministic (no-model, no-network) gate half.
|
|
86
|
+
# Same rationale as uat/runners/: vitest excludes all of uat/**, and these
|
|
87
|
+
# import `vitest` (bun runs them via its vitest-compat shim) — so they run
|
|
88
|
+
# on the BUN side. Directory match (trailing slash) never widens into
|
|
89
|
+
# uat/scenarios/. Fenced by scripts/check-test-runner-coverage.mjs.
|
|
90
|
+
uat/flip/
|
|
85
91
|
)
|
|
86
92
|
fi
|
|
87
93
|
|
|
@@ -42,6 +42,7 @@ function isMultiAgentEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
42
42
|
}
|
|
43
43
|
import { classifyClaudeError, type OperatorEventKind } from './operator-events.js'
|
|
44
44
|
import { isLitellmProxyLocal429, isTransientUpstreamSignal } from './model-unavailable.js'
|
|
45
|
+
import { isConnectionDropText } from './connection-drop.js'
|
|
45
46
|
import { createToolLabelSidecar, type ToolLabelSidecar, type SidecarOptions } from './tool-label-sidecar.js'
|
|
46
47
|
import { isModelSentinel } from './model-label.js'
|
|
47
48
|
|
|
@@ -960,6 +961,25 @@ function extractRetryState(obj: Record<string, unknown>): {
|
|
|
960
961
|
}
|
|
961
962
|
}
|
|
962
963
|
|
|
964
|
+
/**
|
|
965
|
+
* The OperatorEventKinds on which a connection-drop wording may be flagged as a
|
|
966
|
+
* genuine transport drop. INCLUSION (not exclusion) by design: a positively
|
|
967
|
+
* classified auth/quota/credit/rate-limit/overload wall is NEVER in this set,
|
|
968
|
+
* so a wrapped terminal error whose outer text coincidentally carries a drop
|
|
969
|
+
* wording is never mislabelled a drop. `transport-transient` is the natural
|
|
970
|
+
* mid-stream-abort kind; `unknown-4xx`/`unknown-5xx` is where a drop-worded
|
|
971
|
+
* line lands when `classifyClaudeError` does not recognise its wording (the
|
|
972
|
+
* exact Path-B bug this closes). Mirrors Path A's `transient`/`unknown` gate in
|
|
973
|
+
* `parseLlmError`, keeping the two classifiers in agreement.
|
|
974
|
+
*/
|
|
975
|
+
const CONNECTION_DROP_ELIGIBLE_KINDS: ReadonlySet<OperatorEventKind> =
|
|
976
|
+
new Set<OperatorEventKind>(['transport-transient', 'unknown-5xx', 'unknown-4xx'])
|
|
977
|
+
|
|
978
|
+
/** True when `kind` may carry the connection-drop flag AND the text is a drop wording. */
|
|
979
|
+
function isConnectionDrop(kind: OperatorEventKind, scanText: string): boolean {
|
|
980
|
+
return CONNECTION_DROP_ELIGIBLE_KINDS.has(kind) && isConnectionDropText(scanText)
|
|
981
|
+
}
|
|
982
|
+
|
|
963
983
|
export function detectErrorInTranscriptLine(
|
|
964
984
|
line: string,
|
|
965
985
|
): {
|
|
@@ -972,6 +992,15 @@ export function detectErrorInTranscriptLine(
|
|
|
972
992
|
* error mid-retry is `transient:true, terminal:false`; the caller
|
|
973
993
|
* suppresses it (no operator card until the failure is terminal). */
|
|
974
994
|
terminal: boolean
|
|
995
|
+
/**
|
|
996
|
+
* True when this line is a mid-stream connection / SSE drop (a transport
|
|
997
|
+
* connection loss), per the canonical `isConnectionDropText` matcher, gated
|
|
998
|
+
* to the transport/unknown kinds. Set consistently with `parseLlmError`'s
|
|
999
|
+
* `ParsedLlmError.connectionDrop` so a later PR can gate auto-resume on ONE
|
|
1000
|
+
* reliable discriminator across both classification paths. Classification
|
|
1001
|
+
* only in this PR; no caller acts on it yet.
|
|
1002
|
+
*/
|
|
1003
|
+
connectionDrop: boolean
|
|
975
1004
|
} | null {
|
|
976
1005
|
if (!line || line.length > 2 * 1024 * 1024) return null
|
|
977
1006
|
let obj: Record<string, unknown>
|
|
@@ -1042,6 +1071,7 @@ export function detectErrorInTranscriptLine(
|
|
|
1042
1071
|
detail: text || errStr || 'api error',
|
|
1043
1072
|
transient: kind === 'rate-limited',
|
|
1044
1073
|
terminal: true,
|
|
1074
|
+
connectionDrop: isConnectionDrop(kind, `${text}\n${errStr}`),
|
|
1045
1075
|
}
|
|
1046
1076
|
}
|
|
1047
1077
|
|
|
@@ -1089,7 +1119,14 @@ export function detectErrorInTranscriptLine(
|
|
|
1089
1119
|
? retry.retryAttempt >= retry.maxRetries
|
|
1090
1120
|
: isErrorLine
|
|
1091
1121
|
|
|
1092
|
-
return {
|
|
1122
|
+
return {
|
|
1123
|
+
kind,
|
|
1124
|
+
raw,
|
|
1125
|
+
detail,
|
|
1126
|
+
transient,
|
|
1127
|
+
terminal,
|
|
1128
|
+
connectionDrop: isConnectionDrop(kind, `${detail}\n${String(type ?? '')}`),
|
|
1129
|
+
}
|
|
1093
1130
|
}
|
|
1094
1131
|
|
|
1095
1132
|
function extractDetailMessage(obj: Record<string, unknown> | null): string | null {
|