@mars-sea/dsh-commandcode-provider 0.11.2 → 0.11.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/lib/index.d.ts CHANGED
@@ -327,6 +327,19 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
327
327
  * wins; otherwise a cached (not network-fetched) billing tier of Go is
328
328
  * treated as CLI-only. Unknown accounts default to Provider API and fall
329
329
  * back only after an `upgrade_required` rejection.
330
+ *
331
+ * Models the Provider API serves ONLY through `/provider/v1/messages` (the
332
+ * Claude family — `requiresMessagesEndpoint()`) always take the CLI
333
+ * transport, on any tier and even under a forced `'openai'` preference:
334
+ * that option is documented as "prefer the Provider API, still fall back",
335
+ * and the 400 these models answer with is not a preference to be honoured
336
+ * but a hard refusal. `/alpha/generate` routes every one of them, so this
337
+ * costs nothing.
338
+ *
339
+ * That decision is deliberately NOT written to `protocolCache`. The cache is
340
+ * keyed by API key alone, so remembering it would pin the whole ACCOUNT to
341
+ * the CLI transport and drag every other model — DeepSeek, GLM, Qwen, all of
342
+ * which the Provider API serves correctly — off it until the entry expired.
330
343
  */
331
344
  private resolveProtocol;
332
345
  /**
@@ -1544,6 +1557,18 @@ interface Config {
1544
1557
  requestTimeoutMs?: number;
1545
1558
  /** Milliseconds a stream may stall before being treated as a dead connection; defaults to 300s. */
1546
1559
  streamIdleTimeoutMs?: number;
1560
+ /**
1561
+ * Transport failures one request absorbs before the failure is surfaced;
1562
+ * defaults to 5. The route's retry policy is near-unbounded on purpose (1000
1563
+ * attempts, waits doubling to 15 minutes) because that shape is for the
1564
+ * failures a provider asks to have retried — an exhausted rate-limit window,
1565
+ * a gateway 520. A transport failure is not one of those: the first attempts
1566
+ * recover an ordinary blip (the default 5 retries are scheduled 0.5/1/2/4/8 s
1567
+ * after the failures before them, so ~15.5 s of grace), and after that the
1568
+ * wait is pure stall, so the retries are capped here. Raise it on a genuinely
1569
+ * flaky link; 0 surfaces every transport failure immediately.
1570
+ */
1571
+ transportMaxRetries?: number;
1547
1572
  /**
1548
1573
  * Whether the model picker hides models above the account's subscription
1549
1574
  * tier; defaults to true. The filter fails open (unknown plan, billing
package/lib/index.js CHANGED
@@ -386,6 +386,48 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
386
386
  "inclusionai/ling-3.0-flash-sante:free"
387
387
  ]);
388
388
  /**
389
+ * Catalog models the Provider API serves ONLY through `/provider/v1/messages`
390
+ * (Anthropic Messages shape). Every other model in the catalog answers on
391
+ * `/provider/v1/chat/completions`; these reject it outright with HTTP 400
392
+ * `Model "<id>" must be called via /provider/v1/messages (Anthropic Messages
393
+ * shape)`.
394
+ *
395
+ * Measured 2026-09-16 against the live catalog (command-code@1.54.0): all 69
396
+ * models were posted to `/provider/v1/chat/completions`; exactly these eight
397
+ * — the whole Claude family — refused, and every one of them is routed
398
+ * normally by `/alpha/generate` (a lower-plan key gets the ordinary
399
+ * `MODEL_NOT_IN_PLAN` 403 there, never a routing error). So the CLI transport
400
+ * is a complete fallback and the adapter does not need a Messages transport.
401
+ *
402
+ * Note what this list is NOT: it is not a plan gate. `claude-sonnet-5` is
403
+ * Pro-tier and `claude-opus-4-8` Provider-tier, so the accounts entitled to
404
+ * these models are exactly the non-Go ones, which `resolveProtocol()` sends to
405
+ * the Provider API by default — picking any Claude model there failed every
406
+ * request before this snapshot existed.
407
+ *
408
+ * Keep in sync when models ship (see the dsh-commandcode-upstream skill).
409
+ * `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
410
+ * Messages-only, so a Claude model added upstream is routed correctly by an
411
+ * un-updated plugin instead of hard-failing with the 400 above; the worst case
412
+ * of that rule going stale the other way (upstream teaching
413
+ * `/provider/v1/chat/completions` to serve Claude) is one model riding the CLI
414
+ * transport it already works on.
415
+ */
416
+ const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
417
+ "claude-sonnet-5",
418
+ "claude-sonnet-4-6",
419
+ "claude-fable-5-1",
420
+ "claude-fable-5",
421
+ "claude-opus-5",
422
+ "claude-opus-4-8",
423
+ "claude-opus-4-7",
424
+ "claude-haiku-4-5-20251001"
425
+ ]);
426
+ /** True when the Provider API will only serve `modelId` via `/provider/v1/messages`. */
427
+ function requiresMessagesEndpoint(modelId) {
428
+ return MESSAGES_ONLY_MODELS.has(modelId) || modelId.startsWith("claude-");
429
+ }
430
+ /**
389
431
  * The minimum subscription plan a model is included in, per the official plan
390
432
  * pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`, `/docs/plans/max`
391
433
  * and `/docs/resources/pricing-limits`). Each plan's model list is a superset of
@@ -2361,6 +2403,28 @@ function numberValue(value) {
2361
2403
  function booleanValue(value) {
2362
2404
  return typeof value === "boolean" ? value : void 0;
2363
2405
  }
2406
+ /**
2407
+ * Thinking text carried by an OpenRouter-shaped `reasoning_details` array
2408
+ * (`[{ type: 'reasoning.text', text, format, index }]`), concatenated in the
2409
+ * order the gateway sent it.
2410
+ *
2411
+ * Entries are read by their `text` member alone rather than filtered on
2412
+ * `type === 'reasoning.text'`: the sibling shapes in that protocol carry
2413
+ * `summary` or an encrypted blob instead of `text`, so a plain read already
2414
+ * ignores them, while a type filter would silently drop a text-bearing variant
2415
+ * added later — and losing thinking is the failure this exists to prevent.
2416
+ * Returns undefined when the array yields nothing, so the caller's `??` chain
2417
+ * keeps falling through instead of treating "no text" as a reasoning delta.
2418
+ */
2419
+ function reasoningDetailsText(value) {
2420
+ if (!Array.isArray(value)) return void 0;
2421
+ let text = "";
2422
+ for (const entry of value) {
2423
+ if (!isRecord(entry)) continue;
2424
+ text += stringValue(entry.text) ?? "";
2425
+ }
2426
+ return text === "" ? void 0 : text;
2427
+ }
2364
2428
  /** Parse a billing-period timestamp (ISO string or millis) into millis; 0 when absent/invalid. */
2365
2429
  function periodEndValue(value) {
2366
2430
  const asNumber = numberValue(value);
@@ -2640,6 +2704,44 @@ async function writeModelsCache(cachePath, models) {
2640
2704
  }
2641
2705
  }
2642
2706
  /**
2707
+ * Whether an inbound message is a tool RESULT — a `user`-role message the
2708
+ * producer tagged with `source: { kind: 'tool' }`.
2709
+ *
2710
+ * The tag is read defensively because it is PRODUCER-supplied and nothing
2711
+ * validates it: dsh-llm's `Message` type declares `source` required, but the
2712
+ * runtime never checks it and every adapter shipped before this one ignored the
2713
+ * field outright (`dsh-llm-deepseek` never reads it). A third-party plugin that
2714
+ * assembles its own history can therefore omit it and run correctly everywhere
2715
+ * else — dsh-mneme's memory pipeline did — which made this adapter, the first
2716
+ * to actually read the field, the first to crash on such a message with
2717
+ * `TypeError: Cannot read properties of undefined (reading 'kind')` in the
2718
+ * serialization phase, 0–8 ms in, before any request left the machine and with
2719
+ * nothing in the error naming the message or the producer (issue #47).
2720
+ *
2721
+ * Two rules keep an untagged message from costing the turn:
2722
+ *
2723
+ * - A PRESENT tag is authoritative. `kind: 'user'`, `'model'`, `'plugin'` or
2724
+ * any producer-added kind means "not a tool result", whatever the content
2725
+ * looks like — the tag answers *who produced this*.
2726
+ * - An ABSENT tag falls back to the content shape. A `user` message whose
2727
+ * first block is a `tool-result` is a tool result by the harness's own
2728
+ * definition (`ToolResultMessage` is exactly that shape with a
2729
+ * `ToolMessageSource`), and the fallback is not cosmetic: `pairedToolCalls()`
2730
+ * counts a `tool-result` block from ANY message, so classifying such a
2731
+ * message as a plain user message would drop it at emission while its call
2732
+ * still counted as paired — leaving the assistant's tool call unanswered on
2733
+ * the wire, which the gateway rejects outright. FIRST block, not "any block":
2734
+ * the tool-result branch reads `content[0]` and skips anything else, so
2735
+ * claiming a mixed message as a tool result would discard its other blocks
2736
+ * instead of sending them.
2737
+ */
2738
+ function isToolResultMessage(message) {
2739
+ if (message.role !== "user") return false;
2740
+ const kind = message.source?.kind;
2741
+ if (kind !== void 0) return kind === "tool";
2742
+ return message.content?.[0]?.type === "tool-result";
2743
+ }
2744
+ /**
2643
2745
  * Collect the tool calls that have a paired tool result, plus each call's
2644
2746
  * name. The name map feeds the `toolName` of replayed tool results: some
2645
2747
  * backends (e.g. Google Gemini `functionResponse`) reject a result whose
@@ -2963,7 +3065,7 @@ async function messagesToCC(messages, readImage) {
2963
3065
  };
2964
3066
  for (const message of messages) {
2965
3067
  if (message.role === "system") continue;
2966
- if (message.role === "user" && message.source.kind !== "tool") {
3068
+ if (message.role === "user" && !isToolResultMessage(message)) {
2967
3069
  flushPendingImages();
2968
3070
  const parts = [];
2969
3071
  for (const block of message.content) {
@@ -3006,7 +3108,7 @@ async function messagesToCC(messages, readImage) {
3006
3108
  });
3007
3109
  continue;
3008
3110
  }
3009
- if (message.role === "user" && message.source.kind === "tool") {
3111
+ if (isToolResultMessage(message)) {
3010
3112
  const block = message.content[0];
3011
3113
  if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
3012
3114
  const media = toolResultMedia(block);
@@ -3075,7 +3177,7 @@ async function messagesToOpenAI(messages, readImage) {
3075
3177
  };
3076
3178
  for (const message of messages) {
3077
3179
  if (message.role === "system") continue;
3078
- if (message.role === "user" && message.source.kind !== "tool") {
3180
+ if (message.role === "user" && !isToolResultMessage(message)) {
3079
3181
  flushPendingImages();
3080
3182
  const parts = [];
3081
3183
  for (const block of message.content) {
@@ -3121,7 +3223,7 @@ async function messagesToOpenAI(messages, readImage) {
3121
3223
  out.push(assistant);
3122
3224
  continue;
3123
3225
  }
3124
- if (message.role === "user" && message.source.kind === "tool") {
3226
+ if (isToolResultMessage(message)) {
3125
3227
  const block = message.content[0];
3126
3228
  if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
3127
3229
  const media = toolResultMedia(block);
@@ -3717,10 +3819,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
3717
3819
  * wins; otherwise a cached (not network-fetched) billing tier of Go is
3718
3820
  * treated as CLI-only. Unknown accounts default to Provider API and fall
3719
3821
  * back only after an `upgrade_required` rejection.
3822
+ *
3823
+ * Models the Provider API serves ONLY through `/provider/v1/messages` (the
3824
+ * Claude family — `requiresMessagesEndpoint()`) always take the CLI
3825
+ * transport, on any tier and even under a forced `'openai'` preference:
3826
+ * that option is documented as "prefer the Provider API, still fall back",
3827
+ * and the 400 these models answer with is not a preference to be honoured
3828
+ * but a hard refusal. `/alpha/generate` routes every one of them, so this
3829
+ * costs nothing.
3830
+ *
3831
+ * That decision is deliberately NOT written to `protocolCache`. The cache is
3832
+ * keyed by API key alone, so remembering it would pin the whole ACCOUNT to
3833
+ * the CLI transport and drag every other model — DeepSeek, GLM, Qwen, all of
3834
+ * which the Provider API serves correctly — off it until the entry expired.
3720
3835
  */
3721
- resolveProtocol(apiKey) {
3836
+ resolveProtocol(apiKey, model) {
3722
3837
  const forced = this.deps.options().protocol;
3723
- if (forced === "cli" || forced === "openai") return forced;
3838
+ if (forced === "cli") return forced;
3839
+ if (requiresMessagesEndpoint(model)) return "cli";
3840
+ if (forced === "openai") return forced;
3724
3841
  const cached = this.cachedProtocolUseCli(apiKey);
3725
3842
  if (cached !== void 0) return cached ? "cli" : "openai";
3726
3843
  if (this.cachedBillingTierWeight(apiKey) === GO_TIER_WEIGHT) {
@@ -3835,7 +3952,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
3835
3952
  const supported = KNOWN_EFFORTS[options.model];
3836
3953
  const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
3837
3954
  const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
3838
- let protocol = this.resolveProtocol(apiKey);
3955
+ let protocol = this.resolveProtocol(apiKey, options.model);
3839
3956
  let requestOptions = this.surfaceOffload === void 0 ? withImageBudget(options, REQUEST_IMAGE_BUDGETS[0], this.imagePolicy) : withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
3840
3957
  let imageBudgetIndex = 0;
3841
3958
  const buildBody = async (target) => target === "cli" ? buildCliBody(requestOptions, connection, {
@@ -4167,7 +4284,7 @@ function handleOpenAIEvent(asm, event) {
4167
4284
  }
4168
4285
  const choice = isRecord(choices[0]) ? choices[0] : {};
4169
4286
  const delta = isRecord(choice.delta) ? choice.delta : {};
4170
- const reasoningDelta = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content) ?? "";
4287
+ const reasoningDelta = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
4171
4288
  if (reasoningDelta !== "") {
4172
4289
  chunks.push(...closeText(asm));
4173
4290
  if (asm.reasoningIndex < 0) {
@@ -5961,6 +6078,108 @@ function applyCommandCodeTuiSettings(ctx, deps) {
5961
6078
  return refresh;
5962
6079
  }
5963
6080
  //#endregion
6081
+ //#region src/transport-retry.ts
6082
+ /**
6083
+ * A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
6084
+ *
6085
+ * The route's retry policy (`providerRetryPolicy` in `./adapter.ts`) is
6086
+ * deliberately near-unbounded: 1000 attempts with waits doubling to a 15-minute
6087
+ * cap. That shape exists for the failures a provider ASKS to have retried — an
6088
+ * exhausted 5-hour window, a Cloudflare 520 — where the wait is the point.
6089
+ *
6090
+ * A transport failure is not that. The reporter's second event at ~710k
6091
+ * context was `Connect Timeout Error ... timeout: 10000ms` (undici's own
6092
+ * connect timeout; this plugin's `requestTimeoutMs` is 60s) and, on another
6093
+ * attempt, `read ECONNRESET` against `/alpha/generate`. Because `TRANSPORT` is
6094
+ * in the whitelist, each of those entered the same 1000-attempt loop: with
6095
+ * `initialDelayMs: 500` doubling, the wait before attempt 11 alone is 512 s and
6096
+ * the cumulative wait before it is ~500 s — the "模型请求重试已取消(11/1000)·
6097
+ * 482s" the reporter photographed is that 512 s countdown, 30 s into it — and
6098
+ * those waits are counted BEFORE each retry attempt, not after it.
6099
+ *
6100
+ * A connection that cannot be established is either a sub-second blip (the
6101
+ * first few attempts recover it, invisibly) or an outage that no amount of
6102
+ * waiting inside one request will fix. So transport failures get their own
6103
+ * budget, and when it is exhausted the failure is surfaced with the diagnosis
6104
+ * instead of manufacturing a multi-minute stall. Waiting out a real outage is
6105
+ * the user's call — the next send starts a fresh budget — and
6106
+ * `dsh-llm-retry`'s window for rate limits is untouched.
6107
+ *
6108
+ * Deliberately NOT counted here: the pre-stream recovery the ADAPTER performs
6109
+ * inside one `stream()` call (account rotation on 429/401, the 413 image-budget
6110
+ * step-down, the Provider-API → CLI protocol switch). Those are invisible to
6111
+ * the caller and bounded on their own; this budget caps only the harness's
6112
+ * retry loop.
6113
+ *
6114
+ * Reset rule: the budget clears at each new STEP — one step is exactly one
6115
+ * model request (`step/start` is appended immediately before the call) — so the
6116
+ * cap is per logical request and the next step of the same turn gets its own
6117
+ * grace. `agent/status` → `idle` is NOT the reset signal even though it looks
6118
+ * like the natural one: the agent loop's `setPhase` emits it only on a status
6119
+ * CHANGE, and a turn's steps all run inside one `running` phase, so it never
6120
+ * fires between them — a budget reset only there would be per-turn, and an
6121
+ * outage would fail every later step of that turn immediately instead of
6122
+ * retrying each. `assistant/attempt` is wrong for the opposite reason: the loop
6123
+ * appends it for the FAILED attempt itself, *before* dispatching
6124
+ * `agent/request-error`, so resetting on it would clear the budget on every
6125
+ * failure and restore the very loop this module exists to stop.
6126
+ *
6127
+ * @module
6128
+ */
6129
+ /** Failure code this budget covers. */
6130
+ const TRANSPORT_FAILURE_CODE = "TRANSPORT";
6131
+ /** One agent's live transport-failure count, keyed weakly so a dropped agent cannot leak. */
6132
+ const counts = /* @__PURE__ */ new WeakMap();
6133
+ /**
6134
+ * Count one attempt's outcome against the transport budget.
6135
+ *
6136
+ * @param agent - the identity the budget is scoped to (the agent object).
6137
+ * @param failureCode - the failed attempt's `LlmFailure.code`.
6138
+ * @param maxRetries - transport retries to absorb before the failure surfaces.
6139
+ * @returns `'ignored'` for any non-transport code (the route policy decides
6140
+ * those, unchanged), `'retry'` while the budget has room, `'exhausted'` once
6141
+ * it does not.
6142
+ */
6143
+ function absorbTransportFailure(agent, failureCode, maxRetries) {
6144
+ if (failureCode !== "TRANSPORT") return "ignored";
6145
+ const used = counts.get(agent) ?? 0;
6146
+ if (used >= maxRetries) return "exhausted";
6147
+ counts.set(agent, used + 1);
6148
+ return "retry";
6149
+ }
6150
+ /** Clear one agent's transport budget (a new step starts, or the turn ended). */
6151
+ function resetTransportFailures(agent) {
6152
+ counts.delete(agent);
6153
+ }
6154
+ /**
6155
+ * What one session event means for the transport budget.
6156
+ *
6157
+ * Exported and pure on purpose: the choice of event is the part of this design
6158
+ * that is easy to get plausibly wrong (`agent/status` → `idle` looks like the
6159
+ * natural reset and is not one — see the module comment), so it is stated once,
6160
+ * here, where a test can pin it, instead of being implied by a listener body.
6161
+ *
6162
+ * @param type - the appended session event's type.
6163
+ * @returns `'reset'` for a new step (one model request), `'forget'` when the
6164
+ * turn ended and the session → agent mapping is no longer needed, otherwise
6165
+ * `'none'`.
6166
+ */
6167
+ function transportResetAction(type) {
6168
+ if (type === "step/start") return "reset";
6169
+ if (type === "turn/end") return "forget";
6170
+ return "none";
6171
+ }
6172
+ /**
6173
+ * The diagnosis a capped turn ends with. Bilingual, because the harness renders
6174
+ * a failed turn's message verbatim and this plugin's other user-facing failures
6175
+ * are bilingual too (see the adapter's error builders). It names the retry
6176
+ * budget and the checks that actually help, instead of the bare "fetch failed"
6177
+ * chain the retry chrome would otherwise show.
6178
+ */
6179
+ function transportBudgetMessage(failureMessage, maxRetries) {
6180
+ return `${failureMessage};Command Code API 连接在连续重试后仍失败——已停止重试以免整轮卡死。插件已自动重试 ${maxRetries} 次。通常说明当前网络到 api.commandcode.ai 的连接不通,常见原因是代理未生效:DSH 只读取环境变量 HTTPS_PROXY/HTTP_PROXY(系统代理/PAC 不会被读取),请确认启动 DSH 的终端里已导出代理,或先关闭代理直连后重试。网络恢复后直接重发即可 — Command Code API transport still failed after ${maxRetries} automatic retries, which were stopped so the turn could not stall for minutes. This is a connectivity problem between this machine and api.commandcode.ai, not a context-window or request-size rejection. Common cause: the proxy is not reaching the harness — DSH reads HTTPS_PROXY/HTTP_PROXY from the environment only (a Windows system proxy/PAC setting is not consulted), so export it in the launching shell, or turn the proxy off and retry. Resend when the network recovers; the budget is per request and has been reset`;
6181
+ }
6182
+ //#endregion
5964
6183
  //#region src/index.ts
5965
6184
  /**
5966
6185
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -6005,6 +6224,7 @@ const Config = z.object({
6005
6224
  modelsCachePath: z.string(),
6006
6225
  requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
6007
6226
  streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
6227
+ transportMaxRetries: z.number().min(0).max(50),
6008
6228
  filterModelsByPlan: z.boolean(),
6009
6229
  visibleModels: z.array(z.string()),
6010
6230
  /**
@@ -6146,6 +6366,36 @@ function apply(ctx, config) {
6146
6366
  settingsPath: []
6147
6367
  }]);
6148
6368
  ctx.llm.registerAdapter([PROVIDER], adapter);
6369
+ const transportMaxRetries = () => {
6370
+ const value = current().transportMaxRetries;
6371
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 5;
6372
+ return Math.min(Math.trunc(value), 50);
6373
+ };
6374
+ const sessionAgents = /* @__PURE__ */ new WeakMap();
6375
+ ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
6376
+ if (signal.aborted) return next();
6377
+ const session = Reflect.get(agent, "session");
6378
+ if (typeof session === "object" && session !== null) sessionAgents.set(session, agent);
6379
+ const decision = absorbTransportFailure(agent, failure.code, transportMaxRetries());
6380
+ if (decision === "ignored") return next();
6381
+ if (decision === "retry") return { kind: "retry" };
6382
+ const message = transportBudgetMessage(failure.message, transportMaxRetries());
6383
+ ctx.logger.warn(`llm-commandcode: transport retry budget exhausted for ${PROVIDER} (${TRANSPORT_FAILURE_CODE})`);
6384
+ throw new Error(message);
6385
+ });
6386
+ ctx.on("session/event", (session, event) => {
6387
+ const action = transportResetAction(event.type);
6388
+ if (action === "forget") {
6389
+ sessionAgents.delete(session);
6390
+ return;
6391
+ }
6392
+ if (action !== "reset") return;
6393
+ const agent = sessionAgents.get(session);
6394
+ if (agent !== void 0) resetTransportFailures(agent);
6395
+ });
6396
+ ctx.on("agent/status", ({ agent, status }) => {
6397
+ if (status === "idle") resetTransportFailures(agent);
6398
+ });
6149
6399
  const usageReports = async () => {
6150
6400
  const described = await pool.describeAccounts();
6151
6401
  const byId = new Map(described.map((account) => [account.slot.id, account]));