@mars-sea/dsh-commandcode-provider 0.11.14 → 0.11.17

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.js CHANGED
@@ -2,12 +2,12 @@ import { homedir, tmpdir } from "node:os";
2
2
  import { dirname, join } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
5
- import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, projectOffloadedImages, requiredImageOffload, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
5
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, projectOffloadedImages, requiredImageOffload, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
6
6
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
7
7
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
8
8
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
9
9
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
10
- import { randomBytes, randomUUID } from "node:crypto";
10
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
11
11
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
12
12
  import { createServer } from "node:http";
13
13
  import { createServer as createServer$1 } from "node:net";
@@ -18,18 +18,12 @@ import { WebError } from "@deepseek-ai/dsh-web";
18
18
  * reasoning-effort levels, vision/thinking flags, model → minimum plan tier,
19
19
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
20
20
  *
21
- * Everything in this module is synced from official sources (the command-code
22
- * CLI bundle's model table and the official plan/pricing/model docs — see the
23
- * dsh-commandcode-upstream skill for the exact extraction procedures), and
24
- * changes whenever an upstream CLI release reshuffles models/plans/prices.
25
- * Keeping the snapshot in its own module confines those frequent sync diffs
26
- * here: src/adapter.ts holds only the stable wire/runtime logic and imports
27
- * these tables + read helpers.
28
- *
29
- * Snapshot read helpers (planLabel, dealLabel, formatContext,
30
- * capabilityDescription, peakPricing*, compareByPlan, modelVisibleInPlan,
31
- * subscriptionPlanInfo, isFreeModel) live here too — they exist only to read
32
- * the tables, so a sync never has to touch src/adapter.ts.
21
+ * Everything here is synced from official sources — the command-code CLI
22
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.66.0) and
23
+ * the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
24
+ * for the extraction procedures. Keeping the snapshot in its own module
25
+ * confines those frequent sync diffs here: src/adapter.ts holds only the stable
26
+ * wire/runtime logic. The read helpers live here too, for the same reason.
33
27
  *
34
28
  * Ported from pi-commandcode-provider (MIT); originally part of src/adapter.ts
35
29
  * and split out so upstream syncs stay reviewable.
@@ -313,6 +307,11 @@ const KNOWN_EFFORTS = {
313
307
  "low",
314
308
  "medium",
315
309
  "high"
310
+ ],
311
+ "stealth/pixel-canary": [
312
+ "low",
313
+ "medium",
314
+ "xhigh"
316
315
  ]
317
316
  };
318
317
  /**
@@ -378,6 +377,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
378
377
  "moonshotai/Kimi-K2.7-Code-Highspeed",
379
378
  "moonshotai/Kimi-K3",
380
379
  "sakana/fugu-ultra",
380
+ "stealth/pixel-canary",
381
381
  "stealth/space-bunny-alpha",
382
382
  "stepfun/Step-3.7-Flash",
383
383
  "stepfun/Step-5-Preview",
@@ -395,19 +395,22 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
395
395
  ]);
396
396
  /**
397
397
  * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
398
- * registry (`command-code@1.65.0` `dist/cli.mjs`: `modelSupportsZdr(id)` is
398
+ * registry (`command-code@1.66.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
399
399
  * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
400
- * carries the same membership in the sibling route table — the union is this
401
- * set). The official docs (commandcode.ai/docs/resources/zdr) put it in prose
402
- * — "99% of our models have ZDR-capable upstreams … only a small handful of
403
- * models are affected" — so the CLI's exclusion list is the only per-model
404
- * evidence there is; a ZDR request naming one of these fails with HTTP 422
405
- * `cmd_zdr_no_providers` instead of routing through a provider that retains.
400
+ * carries the same membership in the sibling route table — the UNION of both
401
+ * is this set. Reading only the sibling route table would drop `meituan/
402
+ * LongCat-2.0` and `stealth/pixel-canary`, which each appear in
403
+ * `modelSupportsZdr` alone. The official docs (commandcode.ai/docs/resources/
404
+ * zdr) put it in prose — "99% of our models have ZDR-capable upstreams … only
405
+ * a small handful of models are affected" — so the CLI's exclusion list is
406
+ * the only per-model evidence there is; a ZDR request naming one of these
407
+ * fails with HTTP 422 `cmd_zdr_no_providers` instead of routing through a
408
+ * provider that retains.
406
409
  *
407
410
  * Why a NEGATIVE set, and why "not listed" answers TRUE: 99% of the catalog is
408
411
  * covered, so the maintained difference is the exception list. This helper is
409
412
  * informational; the adapter sends the ZDR header for EVERY request when the
410
- * switch is on. The provider remains the routing authority and refuses an
413
+ * switch is on — the provider remains the routing authority and refuses an
411
414
  * unsupported model rather than silently dropping the privacy guarantee.
412
415
  *
413
416
  * `minimax/minimax-m3-free` is the one entry the public catalog
@@ -418,16 +421,9 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
418
421
  * Keep in sync via the dsh-commandcode-upstream skill: the CLI's registry data
419
422
  * (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
420
423
  * flags) is upstream-internal routing, not a per-model contract, so this table
421
- * is the snapshot of the exclusion set and nothing more. The 1.62.0 → 1.64.0
422
- * diff of that union is EMPTY: both anchors were extracted from both bundles
423
- * during the 2026-09-23 check and each carries the same 20 members — which is
424
- * how rare a change here is expected to be. The 1.64.0 → 1.65.0 diff (2026-09-24)
425
- * is the counterexample that proves the check still runs: exactly one member
426
- * joins (`stealth/space-bunny-alpha`), and it is in both anchors. `meituan/
427
- * LongCat-2.0` is the one member the two anchors disagree about (it is in
428
- * `modelSupportsZdr`'s set in both releases and in neither `knownModelSupportsZdr`
429
- * set), so the union is
430
- * what this table follows; reading only the sibling route table would drop it.
424
+ * is the snapshot of the exclusion set and nothing more. It is a rare change:
425
+ * 20 members held across 1.62.0 → 1.64.0, and 1.65.0 and 1.66.0 each added
426
+ * exactly one (the two stealth-preview models below).
431
427
  */
432
428
  const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
433
429
  "MiniMaxAI/MiniMax-M3",
@@ -442,6 +438,7 @@ const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
442
438
  "poolside/laguna-s-2.1-free",
443
439
  "sakana/fugu-ultra",
444
440
  "stealth/space-bunny-alpha",
441
+ "stealth/pixel-canary",
445
442
  "stepfun/Step-3.7-Flash",
446
443
  "stepfun/Step-5-Preview",
447
444
  "xai/grok-4.5",
@@ -461,36 +458,18 @@ function supportsZeroDataRetention(modelId) {
461
458
  return !KNOWN_NON_ZDR_MODELS.has(modelId);
462
459
  }
463
460
  /**
464
- * Models the official CLI's model table (command-code@1.53.0) marks
465
- * `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
466
- * think automatically, with Command Code driving the depth. This is the
467
- * authoritative "thinks, effort not adjustable" set: `KNOWN_EFFORTS` (which
468
- * mirrors the CLI's effort map exactly) stays the sole source for selectable
469
- * effort levels, and this snapshot is not surfaced in the picker's compact
470
- * description — it exists for programmatic consumers.
461
+ * Models the official CLI's model table marks `reasoning:!0` but defines no
462
+ * selectable `reasoning_effort` levels — they think automatically, with
463
+ * Command Code driving the depth. `KNOWN_EFFORTS` (which mirrors the CLI's
464
+ * effort map exactly) stays the sole source for selectable effort levels, and
465
+ * this snapshot is not surfaced in the picker's compact description — it exists
466
+ * for programmatic consumers.
471
467
  *
472
- * Source: the command-code@1.53.0 bundled model table (dist/cli.mjs),
473
- * cross-checked with https://commandcode.ai/docs/reference/cli/models.
474
- * (`stealth/ox-alpha` left this set in command-code@1.32.1, which gave it
475
- * selectable `['low', 'high', 'max']` efforts; the preview then ended in
476
- * 1.34.0, removing the model from the catalog entirely. `tencent/hy4-preview`
477
- * joined this set in command-code@1.37.0 — reasoning:!0, no efforts, 1M
478
- * context, routed through OpenRouter — then gained selectable
479
- * `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
480
- * `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
481
- * command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
482
- * to `KNOWN_EFFORTS`. command-code@1.42.0 added LongCat 2.0 (then
483
- * `meituan/LongCat-2.0:free`, now the paid `meituan/LongCat-2.0`; reasoning:!0,
484
- * no efforts). command-code@1.45.0 gave the Muse Spark family
485
- * (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
486
- * `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
487
- * command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
488
- * `['low', 'medium', 'high']` efforts — it moved to `KNOWN_EFFORTS` too.
489
- * command-code@1.52.0 added `inclusionai/ling-3.0-flash-sante:free`
490
- * (reasoning:!0, no efforts).)
491
- * command-code@1.53.0 added `deepseek/deepseek-v4.1-flash` with selectable
492
- * ['low', 'high', 'max'] efforts, so it lives in `KNOWN_EFFORTS`, not here.)
493
- * Keep in sync via the dsh-commandcode-upstream skill.
468
+ * Source: the bundled model table (dist/cli.mjs), cross-checked with
469
+ * https://commandcode.ai/docs/reference/cli/models. Keep in sync via the
470
+ * dsh-commandcode-upstream skill; a model that GAINS selectable efforts leaves
471
+ * this set for `KNOWN_EFFORTS` (Tencent Hy4 Preview, Kimi K3, the Muse Spark
472
+ * family and MiniMax M3 all took that path).
494
473
  */
495
474
  const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
496
475
  "Qwen/Qwen3.6-Max-Preview",
@@ -519,7 +498,7 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
519
498
  *
520
499
  * Measured 2026-09-16 against the live catalog (command-code@1.54.0): all 69
521
500
  * models were posted to `/provider/v1/chat/completions`; exactly these eight
522
- * — the whole Claude family — refused, and every one of them is routed
501
+ * — the whole Claude family then — refused, and every one of them is routed
523
502
  * normally by `/alpha/generate` (a lower-plan key gets the ordinary
524
503
  * `MODEL_NOT_IN_PLAN` 403 there, never a routing error). So the CLI transport
525
504
  * is a complete fallback and the adapter does not need a Messages transport.
@@ -530,13 +509,15 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
530
509
  * the Provider API by default — picking any Claude model there failed every
531
510
  * request before this snapshot existed.
532
511
  *
533
- * Keep in sync when models ship (see the dsh-commandcode-upstream skill).
534
512
  * `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
535
- * Messages-only, so a Claude model added upstream is routed correctly by an
536
- * un-updated plugin instead of hard-failing with the 400 above; the worst case
537
- * of that rule going stale the other way (upstream teaching
538
- * `/provider/v1/chat/completions` to serve Claude) is one model riding the CLI
539
- * transport it already works on.
513
+ * Messages-only, so a Claude model added upstream (`claude-opus-5-5` in
514
+ * command-code@1.64.0 was the first this snapshot missed) is still routed
515
+ * correctly by an un-updated plugin instead of hard-failing with the 400
516
+ * above. The worst case of that rule going stale the other way (upstream
517
+ * teaching `/provider/v1/chat/completions` to serve Claude) is one model
518
+ * riding the CLI transport it already works on.
519
+ *
520
+ * Keep in sync when models ship (see the dsh-commandcode-upstream skill).
540
521
  */
541
522
  const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
542
523
  "claude-sonnet-5",
@@ -554,31 +535,14 @@ function requiresMessagesEndpoint(modelId) {
554
535
  }
555
536
  /**
556
537
  * The minimum subscription plan a model is included in, per the official plan
557
- * pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`, `/docs/plans/max`
558
- * and `/docs/resources/pricing-limits`). Each plan's model list is a superset of
559
- * the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max. Models absent from every
560
- * plan list (Claude Opus/Fable, Fugu Ultra) are Provider-tier.
561
- * `claude-fable-5-1` (Claude Fable 5.1, added in command-code@1.40.0) is
562
- * Provider/Max-tier exactly like `claude-fable-5` — its availability matrix on
563
- * the official plan/pricing pages grants individual-provider/max/ultra and
564
- * teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
565
- * command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
566
- * LongCat 2.0 (Go — a free promo until 2026-09-19, when the backend renamed
567
- * `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0`); 1.43.0 added
568
- * `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
569
- * (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
570
- * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
571
- * added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
572
- * `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
573
- * `z-ai/glm-5.3-flashx` (Go); command-code@1.59.0 added `xai/grok-4.7` (GOAT)
574
- * and 1.60.0 added `stepfun/Step-5-Preview` (Go); command-code@1.62.0 added the
575
- * MiMo V2.6 family — `xiaomi/mimo-v2.6-flash` + `xiaomi/mimo-v2.6-pro` (Go) and
576
- * `xiaomi/mimo-v2.6-pro-ultraspeed` (GOAT) — so the 1.58.0 -> 1.62.0 window's
577
- * only tier changes are additions and the superset chain still holds. The
578
- * 1.62.0 -> 1.65.0 window continues the pattern: 1.64.0 added
579
- * `claude-opus-5-5` (Provider/Max), `gpt-6-sol` (Pro) and `gpt-6-luna` (Go),
580
- * and 1.65.0 added `stealth/space-bunny-alpha` (Go, every tier) — additions
581
- * only, no tier moves.
538
+ * pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
539
+ * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
540
+ * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
541
+ * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
542
+ * Provider-tier. Re-verified at command-code@1.66.0 (2026-09-27): 82 catalog
543
+ * ids at 52/60/74/82 cumulative, a strict superset chain — every release since
544
+ * 1.49.0 has been additive with no tier move, and per-entry tags below name the
545
+ * release that added each row.
582
546
  *
583
547
  * The Provider API exposes no plan metadata, so this snapshot is the source of
584
548
  * truth for the picker's plan annotation — it answers "which plan do I need to
@@ -623,6 +587,7 @@ const KNOWN_PLANS = {
623
587
  "stepfun/Step-3.5-Flash": "go",
624
588
  "stepfun/Step-3.7-Flash": "go",
625
589
  "stepfun/Step-5-Preview": "go",
590
+ "stealth/pixel-canary": "go",
626
591
  "stealth/space-bunny-alpha": "go",
627
592
  "tencent/hy3-paid": "go",
628
593
  "tencent/hy4-preview": "go",
@@ -716,11 +681,11 @@ function compareByPlan(a, b) {
716
681
  }
717
682
  /**
718
683
  * Subscription plan table, synced from the official CLI bundle's plan maps
719
- * (located by the `"individual-go"` key in command-code@1.53.0 `dist/cli.mjs`,
720
- * re-verified unchanged through 1.53.0): subscription `planId`
721
- * prefix → display name and the plan's monthly credit total. This is the
722
- * account's own subscription (from `/alpha/billing/subscriptions`) — distinct
723
- * from {@link KNOWN_PLANS}, which maps catalog models to their minimum tier.
684
+ * (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
685
+ * through command-code@1.66.0): subscription `planId` prefix → display name and
686
+ * the plan's monthly credit total. This is the account's own subscription
687
+ * (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
688
+ * which maps catalog models to their minimum tier.
724
689
  *
725
690
  * `tierWeight` is plugin-added (not from the CLI maps): the plan's rank on
726
691
  * the {@link PLAN_ORDER} scale, used by the picker's plan filter
@@ -835,6 +800,10 @@ const KNOWN_DEALS = {
835
800
  "stealth/space-bunny-alpha": {
836
801
  label: "FREE",
837
802
  free: true
803
+ },
804
+ "stealth/pixel-canary": {
805
+ label: "FREE",
806
+ free: true
838
807
  }
839
808
  };
840
809
  /**
@@ -843,24 +812,11 @@ const KNOWN_DEALS = {
843
812
  * charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
844
813
  * weekday, full price) **Monday to Friday only**; the other 17 hours of a
845
814
  * weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
846
- * The V4 Flash Vision (exp) variant (command-code@1.32.0) shares the V4 Flash
847
- * rates exactly — $0.15/$0.60 off-peak and $0.30/$1.20 peak, per the page's own
848
- * `timeOfDay` block, not merely 2× its own off-peak figures: a rate that is
849
- * internally consistent can still be the wrong row, which is why the vendored
850
- * price table (`./model-prices.ts`) is synced from the page and not hand-kept.
851
- * The picker shows the
852
- * *current* state as a compact
853
- * label (`Peak`/`Half`) matching the English noun style of the other markers
854
- * (`Image`, `FREE`), so a developer can tell at a glance whether calling the
855
- * model right now is cheap or expensive.
856
- *
857
- * Authoritative extraction: the pricing page embeds a model JSON array whose
858
- * hourly-priced entries carry a `timeOfDay` block
859
- * (`{ windows: "01–04 & 06–10 UTC, Mon–Fri", peakHoursPerDay: 7,
860
- * offPeakHoursPerDay: 17, peak: {...}, offPeak: {...} }`). Exactly four models
861
- * carry it: V4 Pro, V4 Flash, V4 Flash Vision (exp), and V4.1 Flash (added in
862
- * command-code@1.53.0 at $0.15/$0.60 off-peak, $0.30/$1.20 peak — the same
863
- * schedule as the other three).
815
+ * Exactly four models carry the page's `timeOfDay` block (the four rows below).
816
+ * The picker shows the *current* state as a compact label (`Peak`/`Half`)
817
+ * matching the English noun style of the other markers (`Image`, `FREE`), so a
818
+ * developer can tell at a glance whether calling the model right now is cheap
819
+ * or expensive.
864
820
  *
865
821
  * Extraction caution: the rendered HTML rows are a trap. Each annotation div
866
822
  * sits inside its OWN row's container, immediately before the NEXT row starts,
@@ -868,7 +824,11 @@ const KNOWN_DEALS = {
868
824
  * to the model printed after it — that is how `deepseek/deepseek-v4-flash-fast`
869
825
  * was wrongly added here (its row is flat-priced at $0.28/$0.56/$0.07 and has
870
826
  * no `timeOfDay` block). Trust the embedded JSON's `timeOfDay` membership and
871
- * the 2× price relation, never the flat-text neighbor.
827
+ * the 2× price relation, never the flat-text neighbor. A rate that is
828
+ * internally consistent can still be the wrong row (the V4 Flash Vision (exp)
829
+ * variant is priced from the page's own `timeOfDay` block, not from 2× its own
830
+ * off-peak figures), which is why the vendored price table
831
+ * (`./model-prices.ts`) is synced from the page and not hand-kept.
872
832
  *
873
833
  * Keep in sync with the official pricing page when the model set, the peak
874
834
  * windows, or the weekday rule change (see the dsh-commandcode-upstream skill).
@@ -907,11 +867,8 @@ function isPeakPricingHour(now = Date.now()) {
907
867
  return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
908
868
  }
909
869
  /**
910
- * Whether `now` (defaults to `Date.now()`) falls in a peak-pricing window for
911
- * time-of-day-priced models. Peak rates apply Monday–Friday (UTC) only: the
912
- * official rule charges Saturday and Sunday completely off-peak for all 24
913
- * hours, so a weekend timestamp is off-peak even inside `PEAK_HOUR_RANGES`.
914
- * `undefined` for models outside the snapshot.
870
+ * As {@link isPeakPricingHour}, plus the {@link KNOWN_PEAK_PRICING} membership
871
+ * test; `undefined` for models outside the snapshot.
915
872
  */
916
873
  function peakPricingState(modelId, now = Date.now()) {
917
874
  if (!KNOWN_PEAK_PRICING.has(modelId)) return void 0;
@@ -1801,7 +1758,7 @@ const MODEL_PRICE_ROWS = [
1801
1758
  {
1802
1759
  id: "step-3.5-flash",
1803
1760
  rates: [
1804
- .1,
1761
+ .09,
1805
1762
  .3,
1806
1763
  .02
1807
1764
  ]
@@ -1837,6 +1794,14 @@ const MODEL_PRICE_ROWS = [
1837
1794
  2.501,
1838
1795
  .042
1839
1796
  ]
1797
+ },
1798
+ {
1799
+ id: "typesafe/jev",
1800
+ rates: [
1801
+ .042,
1802
+ 0,
1803
+ 0
1804
+ ]
1840
1805
  }
1841
1806
  ];
1842
1807
  /**
@@ -1853,8 +1818,8 @@ const PRICE_SLUG_OVERRIDES = { "nvidia/nemotron-3-ultra-550b-a55b": "nemotron-3-
1853
1818
  * not consistently: `tencent/hy4-preview` keeps its prefix while
1854
1819
  * `Qwen/Qwen3.8-Max-0902` becomes `qwen-3.8-max-0902`, with a hyphen the
1855
1820
  * catalog id does not have. Generating candidates and taking the first that
1856
- * exists in the vendored table absorbs that drift without a hand-maintained
1857
- * map of seventy ids.
1821
+ * exists in the vendored table absorbs that drift without a hand-maintained map
1822
+ * of eighty ids.
1858
1823
  */
1859
1824
  function priceSlugCandidates(modelId) {
1860
1825
  const lower = modelId.toLowerCase();
@@ -1909,13 +1874,10 @@ function wireRow(id, slug, row) {
1909
1874
  * Rows are keyed by CATALOG id wherever the two namespaces reconcile, because
1910
1875
  * that is what a session reports, and every row also carries its page slug as a
1911
1876
  * second lookup key. Price rows no catalog model claims are served under the
1912
- * slug alone, so drift in either direction still prices: a page rename the
1913
- * catalog has not followed, or a model the catalog snapshot has not learned
1914
- * yet. Free models are served explicitly at zero so the composer can say so
1915
- * instead of showing nothing.
1916
- *
1917
- * The peak windows travel with the table, so the browser applies the very
1918
- * schedule this snapshot knows instead of restating it.
1877
+ * slug alone, so drift in either direction still prices. Free models are served
1878
+ * explicitly at zero so the composer can say so instead of showing nothing; the
1879
+ * peak windows travel with the table so the browser applies the very schedule
1880
+ * this snapshot knows instead of restating it.
1919
1881
  */
1920
1882
  function modelPriceTable() {
1921
1883
  const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
@@ -1969,13 +1931,13 @@ function peakHour(at, windows) {
1969
1931
  return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
1970
1932
  }
1971
1933
  /**
1972
- * All prompt billing buckets determine a request's band, never cumulative session input.
1934
+ * All prompt billing buckets determine a request's band, never cumulative
1935
+ * session input.
1973
1936
  *
1974
1937
  * A tiered model returns the matching band and does NOT then apply `peak`: the
1975
- * page publishes those two dimensions independently, no row carries both today,
1976
- * and the generator would report a tiered row on every sync — so the combination
1977
- * becoming real is visible before it reaches a user. This ordering is a latent
1978
- * choice, not a verified upstream rule.
1938
+ * page publishes those two dimensions independently and no row carries both
1939
+ * today, so the combination becoming real is visible before it reaches a user.
1940
+ * This ordering is a latent choice, not a verified upstream rule.
1979
1941
  */
1980
1942
  function requestRates(price, at, contextTokens, table) {
1981
1943
  const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
@@ -2019,11 +1981,11 @@ function costGroupKey(group, table) {
2019
1981
  //#endregion
2020
1982
  //#region src/cost-projection.ts
2021
1983
  const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
2022
- const finite$1 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
1984
+ const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
2023
1985
  function tokensOf(value) {
2024
1986
  const u = record$2(value);
2025
- if (!u || !finite$1(u.inputTokens) || !finite$1(u.outputTokens)) return void 0;
2026
- if (u.cacheReadTokens !== void 0 && !finite$1(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite$1(u.cacheWriteTokens)) return void 0;
1987
+ if (!u || !finite(u.inputTokens) || !finite(u.outputTokens)) return void 0;
1988
+ if (u.cacheReadTokens !== void 0 && !finite(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite(u.cacheWriteTokens)) return void 0;
2027
1989
  return {
2028
1990
  uncachedInputTokens: u.inputTokens,
2029
1991
  outputTokens: u.outputTokens,
@@ -2049,13 +2011,13 @@ function usageOf(type, data) {
2049
2011
  }
2050
2012
  function parseState(value) {
2051
2013
  const s = record$2(value), facts = record$2(s?.facts);
2052
- if (!s || !facts || !finite$1(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
2014
+ if (!s || !facts || !finite(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
2053
2015
  const group = (v) => {
2054
2016
  const g = record$2(v), t = record$2(g?.tokens);
2055
- return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite$1(g.at)) && finite$1(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite$1(t[k]));
2017
+ return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite(g.at)) && finite(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite(t[k]));
2056
2018
  };
2057
2019
  const sel = record$2(s.selection), last = record$2(s.last);
2058
- if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite$1(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite$1(last.turn) && finite$1(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
2020
+ if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite(last.turn) && finite(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
2059
2021
  return value;
2060
2022
  }
2061
2023
  /** Optional registry uses only synchronous parse/view; no new runtime module is required. */
@@ -2089,11 +2051,11 @@ function createCostProjection(table = modelPriceTable()) {
2089
2051
  }
2090
2052
  if (event.type === "step/start" || event.type === "llm/retry-started") return {
2091
2053
  ...state,
2092
- at: finite$1(event.time) ? event.time : null,
2054
+ at: finite(event.time) ? event.time : null,
2093
2055
  last: null
2094
2056
  };
2095
2057
  const tokens = usageOf(event.type, data);
2096
- if (tokens === void 0 || !finite$1(data.turn) || !finite$1(data.step)) return state;
2058
+ if (tokens === void 0 || !finite(data.turn) || !finite(data.step)) return state;
2097
2059
  const previous = state.last?.turn === data.turn && state.last.step === data.step ? state.last.group : void 0;
2098
2060
  const group = {
2099
2061
  provider: state.selection?.provider ?? "",
@@ -2165,34 +2127,18 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2165
2127
  /**
2166
2128
  * Multi-account pool for the Command Code provider (host side).
2167
2129
  *
2168
- * One Command Code subscription (e.g. the Go plan's 5-hour window) is
2169
- * metered; a user with several subscriptions wants a request that hits one
2170
- * account's limit to continue on the next account without a visible failure.
2171
- * This module owns that rotation:
2130
+ * One subscription's 5-hour window is metered, and a user with several wants a
2131
+ * request that hits one account's limit to continue on the next without a
2132
+ * visible failure. Rotation is passive: a key is marked only on a real
2133
+ * pre-stream rejection, and when nothing can serve, marked keys are revived by
2134
+ * probing their real usage windows.
2172
2135
  *
2173
- * - {@link CommandCodeAccountPool.resolveKey} hands out the first account
2174
- * whose key is not currently marked exhausted — or, when the request's
2175
- * model matches a {@link CommandCodeModelAccountRule} and that account is
2176
- * usable, the routed account — resolving each slot's key lazily (literal
2177
- * config key → credential seam → launch environment → the official CLI
2178
- * auth file for the default slot only).
2179
- * - {@link CommandCodeAccountPool.markRejected} records a 429 or 401 against
2180
- * the exact API key, so several slots sharing one key share one state. A
2181
- * 429 that NAMED a usage window marks the window exhausted; a 429 that
2182
- * named none is recorded as a plain throttle, which rotates the same way
2183
- * but is never reported as an exhausted window (issue #54).
2184
- * - When every account is marked, the pool probes each key's
2185
- * `/alpha/billing/credits` window limits (through the injected
2186
- * {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
2187
- * no longer reports `exceeded` is revived, otherwise the pool throws a
2188
- * `RATE_LIMIT` error naming the earliest reset time.
2189
- * - An account the user explicitly asked for — the manual pin
2190
- * ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
2191
- * routed account — is probed the same way when a mark would demote it, so
2192
- * a fallback is never permanent (issue #51).
2136
+ * The non-obvious rules — which rejection may be REPORTED as a spent window, why
2137
+ * a fallback is never permanent, what an explicitly selected account's probe
2138
+ * costs — are stated at the members below, and recorded in full in AGENTS.md.
2193
2139
  *
2194
- * The pool is deliberately cordis-free (like the adapter): every host fact
2195
- * arrives through injected thunks, so node tests can drive it directly.
2140
+ * Cordis-free like the adapter: every host fact arrives through injected thunks,
2141
+ * so node tests can drive the pool directly.
2196
2142
  *
2197
2143
  * @module dsh-commandcode-provider/accounts
2198
2144
  */
@@ -2200,22 +2146,21 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2200
2146
  * Upper bound on the retry wait this pool attaches to the all-exhausted
2201
2147
  * `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
2202
2148
  * `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
2203
- * provider-specified wait verbatim only at or below that cap — in normal mode
2204
- * a LONGER attached wait makes the executor abandon the retry entirely
2205
- * instead of falling back to local backoff, which would turn "poll until the
2206
- * window opens" into "fail now".
2149
+ * provider-specified wait verbatim only at or below that cap — a LONGER one
2150
+ * makes the executor abandon the retry instead of falling back to local backoff
2151
+ * (in normal mode), turning "poll until the window opens" into "fail now".
2207
2152
  */
2208
2153
  const RETRY_MAX_DELAY_MS = 9e5;
2209
2154
  /**
2210
2155
  * How long an explicitly selected account's rate-limit mark may stand before
2211
- * its window is probed again. A 429 whose reset was never learned marks the
2212
- * key `unknown`, which {@link accountUsable} refuses forever — deliberately,
2213
- * because the all-marked revival pass is the only thing meant to clear it and
2214
- * it costs one billing call per key. That is fine for a key nobody asked for,
2215
- * and wrong for the one the user pinned: a single 429 would otherwise demote
2216
- * the user's own selection until the process restarts (issue #51). The probe
2217
- * is therefore allowed once per interval per key, which bounds an endpoint
2218
- * that keeps failing while a successful probe resolves the mark for good.
2156
+ * its window is probed again.
2157
+ *
2158
+ * A 429 whose reset was never learned marks the key `unknown`, which
2159
+ * {@link accountUsable} refuses — for a key nobody asked for that is correct
2160
+ * (only the all-marked pass clears it, and it costs one billing call), but for
2161
+ * the user's own pinned account a single 429 would demote it until the process
2162
+ * restarts (issue #51). Once per interval per key bounds a probe endpoint that
2163
+ * keeps failing; a successful probe resolves the mark for good.
2219
2164
  */
2220
2165
  const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
2221
2166
  /** A labeled, human-readable clock reading for error messages. */
@@ -2234,10 +2179,10 @@ function accountUsable(state) {
2234
2179
  return false;
2235
2180
  }
2236
2181
  /**
2237
- * Pick the account that should serve now: the manually preferred slot when it
2238
- * is usable, otherwise the first usable account in rotation order; undefined
2239
- * when no account is usable. Shared by the pool (request path) and the plugin
2240
- * entry (the usage view's active badge) so both always agree.
2182
+ * Pick the account that should serve now: the manually preferred slot when it is
2183
+ * usable, otherwise the first usable account in rotation order; undefined when
2184
+ * no account is usable. Shared by the pool (request path) and the plugin entry
2185
+ * (the usage view's active badge) so both always agree.
2241
2186
  */
2242
2187
  function selectActiveAccount(accounts, preferredId) {
2243
2188
  const usable = accounts.filter((account) => accountUsable(account.state));
@@ -2320,27 +2265,26 @@ var CommandCodeAccountPool = class {
2320
2265
  return out;
2321
2266
  }
2322
2267
  /**
2323
- * Hand out the key for a request: the model-routed account when the
2324
- * request's model matches a rule (and that account is usable), else the
2325
- * manually preferred account when usable, else the first usable account in
2326
- * rotation order. Returns `undefined` when no account resolves any key at
2327
- * all (the caller then reports the missing credential). Throws
2328
- * `RATE_LIMIT` — naming the earliest window reset — or
2329
- * `INVALID_CREDENTIAL` when accounts exist but none can serve.
2268
+ * Hand out the key for a request: the model-routed account when the request's
2269
+ * model matches a rule (and that account is usable), else the manually
2270
+ * preferred account when usable, else the first usable account in rotation
2271
+ * order. Returns `undefined` when no account resolves any key at all, or when
2272
+ * an account this request already used is still UNMARKED — that rejection is
2273
+ * the honest answer, so the caller's own rejection surfaces as itself. Throws
2274
+ * `RATE_LIMIT` (naming the earliest window reset) or `INVALID_CREDENTIAL`
2275
+ * when accounts exist but every one of them is marked.
2330
2276
  *
2331
2277
  * `options.model` is the request's model id; routing rules re-read per
2332
- * resolution, so a settings change applies live.
2333
- *
2334
- * `options.tried` lists the keys this request has already used — the
2335
- * just-rejected one included. They are removed from the resolution entirely,
2336
- * which is what lets one request walk a four-account pool: an account-scoped
2337
- * rejection that does not mark the key (no credits, a model outside the
2338
- * account's plan) would otherwise be offered again on every attempt, and the
2339
- * accounts behind it would never be reached.
2278
+ * resolution, so a settings change applies live. `options.tried` lists the
2279
+ * keys this request has already used — the just-rejected one included — and
2280
+ * they are removed from the resolution entirely, which is what lets one
2281
+ * request walk a four-account pool: a rejection that does not mark the key
2282
+ * (no credits, a model outside the account's plan) would otherwise be offered
2283
+ * again on every attempt and the accounts behind it never reached.
2340
2284
  *
2341
- * An explicit selection (the pin or a model rule) that a rate-limit mark
2342
- * would demote is probed before the fallback serves, so "falls back while
2343
- * exhausted" never becomes "stays demoted until the process restarts".
2285
+ * An explicit selection (the pin or a model rule) that a rate-limit mark would
2286
+ * demote is probed before the fallback serves, so "falls back while exhausted"
2287
+ * never becomes "stays demoted until the process restarts" (issue #51).
2344
2288
  */
2345
2289
  async resolveKey(options) {
2346
2290
  const accounts = await this.resolvedAccounts();
@@ -2396,21 +2340,18 @@ var CommandCodeAccountPool = class {
2396
2340
  throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
2397
2341
  }
2398
2342
  /**
2399
- * Record a rejection against one key. `rate-limit` marks the key's usage
2400
- * WINDOW exhausted — as a `cooldown` until `resetAtMs` when the rejection
2401
- * body named the provider's own reset time, otherwise as an `unknown` mark
2402
- * whose window is probed lazily. `throttled` (the plain 429 that named no
2403
- * window) marks the key with the `throttle` cause instead: it leaves rotation
2404
- * exactly like a window mark does, but the pool's own diagnosis keeps saying
2405
- * "rate limited" rather than inventing an exhausted window. A 401
2406
- * (`invalid-credential`) disables the key until the stored credential
2343
+ * Record a rejection against one key. A `throttled` rejection (the plain 429
2344
+ * that named no window) marks the key with the `throttle` cause, leaving
2345
+ * rotation exactly like a window mark but keeping the pool's own diagnosis at
2346
+ * "rate limited" rather than inventing an exhausted window. `rate-limit`
2347
+ * marks a `cooldown` until `resetAtMs` when the body named the provider's own
2348
+ * reset, otherwise an `unknown` mark whose window is probed lazily.
2349
+ * `invalid-credential` (401) disables the key until the stored credential
2407
2350
  * changes.
2408
2351
  *
2409
- * `resetAtMs` is seconds-to-millis converted by the adapter from the
2410
- * provider's `error.rateLimit.reset`: knowing the real reset immediately is
2411
- * what keeps an exhausted account out of rotation for exactly as long as the
2412
- * provider said. It only applies to a window mark; a plain throttle keeps the
2413
- * window unknown on purpose, so a probe can still find out.
2352
+ * `resetAtMs` is seconds-to-millis converted by the adapter from
2353
+ * `error.rateLimit.reset`; it applies to a window mark only. A plain throttle
2354
+ * keeps its window unknown on purpose, so a probe can still find out.
2414
2355
  */
2415
2356
  markRejected(apiKey, rejection, resetAtMs) {
2416
2357
  if (rejection === "invalid-credential") this.states.set(apiKey, {
@@ -2442,15 +2383,12 @@ var CommandCodeAccountPool = class {
2442
2383
  * One account's key: literal → credential seam → auth file (default slot).
2443
2384
  *
2444
2385
  * Every source is normalized here, at the single point where a slot's key
2445
- * enters the pool. The adapter sends the key through the harness's
2446
- * `assertUsableApiKey()`, which trims it — a stored key from the credentials
2447
- * seam, a `.env` line, or a shell export all pick up surrounding whitespace
2448
- * — and reports that trimmed form back to `markRejected()`. Returning the
2449
- * raw value would file every 429/401 mark under a key no later lookup can
2450
- * find: rotation would re-offer the same account, the account card would show
2451
- * no mark, and the usage endpoints would 401 while chat kept working.
2452
- * Normalizing once makes resolution, probing, marking, and the request path
2453
- * agree on one string.
2386
+ * enters the pool. The adapter sends the key through
2387
+ * `assertUsableApiKey()`, which trims it, and reports that trimmed form back
2388
+ * to {@link markRejected}: returning the raw value would file every 429/401
2389
+ * mark under a key no later lookup can find — rotation would re-offer the same
2390
+ * account, the account card would show no mark, and the usage endpoints would
2391
+ * 401 while chat kept working.
2454
2392
  */
2455
2393
  async resolveSlotKey(slot) {
2456
2394
  if (slot.literal !== void 0) {
@@ -2473,12 +2411,11 @@ var CommandCodeAccountPool = class {
2473
2411
  }
2474
2412
  }
2475
2413
  /**
2476
- * The account the user explicitly asked for: the slot a model rule routes
2477
- * the request to when that id exists among the resolved slots, else the
2478
- * manually pinned slot. Consulted only on the fallback path — a usable
2479
- * routed account already returned above — so unlike
2480
- * {@link selectAccountForModel} it does NOT require the account to be
2481
- * usable, which is exactly what {@link resolveKey} re-probes.
2414
+ * The account the user explicitly asked for: the slot a model rule routes the
2415
+ * request to when that id exists among the resolved slots, else the manually
2416
+ * pinned slot. Consulted only on the fallback path, so — unlike
2417
+ * {@link selectAccountForModel} — it does NOT require the account to be usable,
2418
+ * which is exactly what {@link resolveKey} re-probes.
2482
2419
  */
2483
2420
  explicitAccount(accounts, model, preferredId) {
2484
2421
  const rule = matchModelRule(model, this.deps.modelAccountRules?.());
@@ -2488,9 +2425,9 @@ var CommandCodeAccountPool = class {
2488
2425
  /**
2489
2426
  * Whether an explicitly selected account's mark is due for a window probe.
2490
2427
  * Only an `unknown` mark (a 429 whose reset was never learned) is worth
2491
- * re-probing: a `cooldown` already carries its reset time and expires by
2492
- * itself, and a `disabled` (401) key stays out until the stored credential
2493
- * changes. The interval bounds a probe endpoint that keeps failing.
2428
+ * re-probing: a `cooldown` carries its reset and expires by itself, and a
2429
+ * `disabled` (401) key stays out until the stored credential changes. The
2430
+ * interval bounds a probe endpoint that keeps failing.
2494
2431
  */
2495
2432
  canProbeExplicit(account) {
2496
2433
  if (account.state?.kind !== "unknown") return false;
@@ -2504,8 +2441,8 @@ var CommandCodeAccountPool = class {
2504
2441
  /**
2505
2442
  * Probe one explicitly selected account's window and apply the answer. A
2506
2443
  * window that is no longer exceeded drops the mark, so the user's own
2507
- * selection serves again on this very request. An exceeded one is stamped
2508
- * as a cooldown carrying the provider's reset time, after which
2444
+ * selection serves again on this very request; an exceeded one is stamped as
2445
+ * a cooldown carrying the provider's reset time, after which
2509
2446
  * {@link accountUsable} lets the account back in with no further probe. A
2510
2447
  * probe that fails changes nothing: the mark stays `unknown` and the next
2511
2448
  * attempt waits out the interval.
@@ -2557,22 +2494,20 @@ var CommandCodeAccountPool = class {
2557
2494
  * read a window as exceeded) → the window diagnosis, naming the earliest
2558
2495
  * known reset when the provider published one;
2559
2496
  * - every mark a plain throttle (a 429 that said nothing about a window, and
2560
- * no probe could confirm one) → a throttle diagnosis that says so. Claiming
2561
- * "all accounts have exhausted their usage window" here is issue #54: the
2562
- * report's account panel showed the five-hour window at 2% and the weekly
2563
- * one at 10% while the turn failed with exactly that sentence.
2497
+ * no probe could confirm one) → a throttle diagnosis that says so.
2498
+ * Claiming "all accounts have exhausted their usage window" here is issue
2499
+ * #54: a bare 429 was reported that way while the account card showed both
2500
+ * windows barely used.
2564
2501
  *
2565
- * Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
2566
- * and by its already-tried diagnosis, so the two can never drift apart.
2502
+ * The attached `providerRetryAfterMs` is the exact wait until the earliest known
2503
+ * window reset, so dsh-llm-retry sleeps through the window instead of polling at
2504
+ * its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS} — see there for
2505
+ * why a longer attached wait is worse than a capped one; longer resets simply
2506
+ * ride the capped local backoff and the probe revival. A throttle diagnosis
2507
+ * attaches none: its marks carry no reset.
2567
2508
  *
2568
- * The attached `providerRetryAfterMs` is the exact wait until the earliest
2569
- * known window reset, so dsh-llm-retry sleeps through the window instead of
2570
- * polling at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}:
2571
- * the executor honors a provider wait verbatim only at or below the policy's
2572
- * `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
2573
- * (normal mode), which would turn "poll until the window opens" into "fail
2574
- * now". Longer resets simply ride the capped local backoff and the probe
2575
- * revival. A throttle diagnosis attaches none: its marks carry no reset.
2509
+ * Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey} and
2510
+ * by its already-tried diagnosis, so the two can never drift apart.
2576
2511
  */
2577
2512
  function allAccountsUnusable(accounts) {
2578
2513
  if (accounts.filter((account) => account.state?.kind === "disabled").length === accounts.length) return new LlmError(`llm-commandcode: every configured Command Code account (${accounts.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${accounts.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
@@ -2615,14 +2550,14 @@ const REQUEST_IMAGE_MAX_LONG_EDGE = 1568;
2615
2550
  const REQUEST_IMAGE_MAX_ENCODED_BYTES = 1048576;
2616
2551
  /**
2617
2552
  * Aspect-preserving projection onto a long-edge budget; never enlarges, and
2618
- * never returns a zero dimension (a 1-px degenerate side is what the round of
2619
- * an extreme aspect ratio can produce, and a 0 would make the encoder refuse).
2553
+ * never returns a zero dimension.
2620
2554
  *
2621
2555
  * `dsh-attachment` exports an equivalent `longEdgeDimensions`, but it does not
2622
2556
  * guard its inputs: a reference whose declared size is 0 or NaN — a malformed
2623
2557
  * or fabricated attachment record — would project to a zero-sized target and
2624
- * make the encoder refuse the whole request. The guard is the reason this stays
2625
- * local; `tests/image-tokens.test.ts` pins the degenerate cases.
2558
+ * make the encoder refuse the whole request. This local guard is the ONLY
2559
+ * reason the function is not imported; do not replace it with the upstream one.
2560
+ * `tests/image-tokens.test.ts` pins the degenerate cases.
2626
2561
  */
2627
2562
  function longEdgeDimensions(width, height, longEdge) {
2628
2563
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return {
@@ -2666,16 +2601,13 @@ function requestImageTarget(ref) {
2666
2601
  * that the heuristic prices at ~0 tokens each, so context pressure is
2667
2602
  * under-reported and the session walks into its own context window.
2668
2603
  *
2669
- * This route is a gateway in front of many upstreams — one catalog id per model,
2670
- * but the visual-token rule belongs to whoever serves it. So the estimate is per
2671
- * FAMILY, using each vendor's published rule, and the families are recognised by
2672
- * the catalog id (which names the vendor). Every formula is stated here rather
2673
- * than imported: the only in-tree implementations live inside the official
2674
- * adapters, and this plugin deliberately does not depend on those protocols.
2675
- *
2676
- * These are ESTIMATES for context accounting, not invoices — the provider's own
2677
- * usage remains authoritative for a completed request, exactly as the published
2678
- * per-token rates the session-cost readout uses are.
2604
+ * This route is a gateway in front of many upstreams, so the estimate is per
2605
+ * FAMILY, using each vendor's published rule, and the families are recognised
2606
+ * by the catalog id (which names the vendor). Every formula is stated here
2607
+ * rather than imported: the only in-tree implementations live inside the
2608
+ * official adapters, and this plugin deliberately does not depend on those
2609
+ * protocols. These are ESTIMATES for context accounting, not invoices — the
2610
+ * provider's own usage remains authoritative for a completed request.
2679
2611
  *
2680
2612
  * @module dsh-commandcode-provider/image-tokens
2681
2613
  */
@@ -2825,14 +2757,11 @@ function scaleBy(size, factor) {
2825
2757
  * Opt-in raw-stream trace for the Command Code transports.
2826
2758
  *
2827
2759
  * Why this exists: this route's failure mode of record is a response that ENDS
2828
- * EARLY. The gateway, an intermediary proxy, or the network closes the socket
2829
- * mid-generation, so the adapter's read loop sees `done` without ever seeing
2830
- * the transport's terminal event. From inside the adapter those two endings are
2831
- * indistinguishable — the provider cut the stream, a proxy timed out, or the
2832
- * socket was dropped — and the only artifact that separates them is the raw
2833
- * wire: which events actually arrived, when the last one landed, how long the
2834
- * socket then sat silent, and how it ended. Without that, a truncated answer is
2835
- * a report with no evidence attached.
2760
+ * EARLY — the gateway, a proxy or the network closes the socket mid-generation,
2761
+ * so the adapter's read loop sees `done` without the transport's terminal event.
2762
+ * A provider cut, a proxy timeout and a dropped socket are indistinguishable
2763
+ * from inside the adapter; only the raw wire separates them — which events
2764
+ * arrived, when the last one landed, how long the socket then sat silent.
2836
2765
  *
2837
2766
  * Enable it per process:
2838
2767
  *
@@ -2842,24 +2771,25 @@ function scaleBy(size, factor) {
2842
2771
  * ```
2843
2772
  *
2844
2773
  * Any value that is not a flag is a path, and the explicit off-spellings
2845
- * (`0`, `false`, `no`, `off`) are recognized as "off" rather than being taken
2846
- * for a file name.
2774
+ * (`0`, `false`, `no`, `off`) are read as "off" rather than as a file name.
2847
2775
  *
2848
2776
  * Every model request appends one JSONL record per raw chunk plus lifecycle
2849
- * records (`response`, `eof`, `idle-timeout`, `read-error`, `end`,
2850
- * `stream-close`), each stamped with `at` — milliseconds since that request's
2851
- * first byte. The chunk records carry the provider's own payload verbatim
2852
- * (bounded per chunk), because a truncated answer is exactly what is being
2853
- * diagnosed: treat the file as conversation content, not as a log safe to
2854
- * share. It never carries the request body, an API key, or any request header.
2855
- *
2856
- * Deliberate trade-offs:
2857
- * - OFF costs one `process.env` read per request.
2858
- * - ON appends synchronously, one file open per chunk. The trace must survive
2859
- * a host killed mid-diagnosis, and the microseconds it costs are irrelevant
2860
- * beside the seconds-scale silences it is there to measure.
2861
- * - A trace that cannot be written turns itself off instead of failing the
2862
- * request it is observing: a diagnostic must never become the outage.
2777
+ * records (`request`, `response`, `eof`, `idle-timeout`, `read-error`, `end`,
2778
+ * `stream-close`), each stamped with `streamId` and `at` — milliseconds since
2779
+ * the trace opened, before connecting. `stream-open.startedAt` is Unix time in
2780
+ * milliseconds, so interleaved calls and session logs can be correlated.
2781
+ *
2782
+ * PRIVACY: the chunk records carry the provider's own payload verbatim, because
2783
+ * a truncated answer is exactly what is being diagnosed — treat the file as
2784
+ * conversation content, not as a log safe to share. The request-side record
2785
+ * records structural counts and SHA-256 fingerprints only; it never carries the
2786
+ * raw request body, an API key, or any request header. Keep it that way.
2787
+ *
2788
+ * Trade-offs: OFF costs one `process.env` read per request; ON appends
2789
+ * synchronously, one file open per chunk, because the trace must survive a host
2790
+ * killed mid-diagnosis. A trace that cannot be written turns itself off rather
2791
+ * than failing the request it observes — a diagnostic must never become the
2792
+ * outage.
2863
2793
  *
2864
2794
  * @module
2865
2795
  */
@@ -2882,7 +2812,7 @@ const FLAG_VALUES = /* @__PURE__ */ new Set([
2882
2812
  * A user turning the trace off writes `false` as readily as they write `1` to
2883
2813
  * turn it on, and without this set every one of these spellings fell through to
2884
2814
  * "…so it must be a path" — the trace switched ON and wrote the conversation to
2885
- * a file NAMED `false` (or `0`, or `off`) in the process's working directory.
2815
+ * a file NAMED `false` in the process's working directory.
2886
2816
  */
2887
2817
  const OFF_VALUES = /* @__PURE__ */ new Set([
2888
2818
  "0",
@@ -2934,6 +2864,7 @@ function openStreamTrace(scope, options = {}) {
2934
2864
  if (path === void 0) return OFF;
2935
2865
  const now = options.now ?? Date.now;
2936
2866
  const startedAt = now();
2867
+ const streamId = randomUUID();
2937
2868
  let written = 0;
2938
2869
  let stopped = false;
2939
2870
  let capped = false;
@@ -2953,6 +2884,7 @@ function openStreamTrace(scope, options = {}) {
2953
2884
  line = `${JSON.stringify({
2954
2885
  at: now() - startedAt,
2955
2886
  scope,
2887
+ streamId,
2956
2888
  ...record
2957
2889
  })}\n`;
2958
2890
  } catch {
@@ -2966,6 +2898,7 @@ function openStreamTrace(scope, options = {}) {
2966
2898
  line = `${JSON.stringify({
2967
2899
  at: now() - startedAt,
2968
2900
  scope,
2901
+ streamId,
2969
2902
  event: "trace-capped",
2970
2903
  bytes: written
2971
2904
  })}\n`;
@@ -2977,9 +2910,14 @@ function openStreamTrace(scope, options = {}) {
2977
2910
  stopped = true;
2978
2911
  }
2979
2912
  };
2980
- write({ event: "stream-open" });
2913
+ write({
2914
+ event: "stream-open",
2915
+ startedAt
2916
+ });
2981
2917
  return {
2982
- enabled: true,
2918
+ get enabled() {
2919
+ return !stopped && !capped;
2920
+ },
2983
2921
  record(event, fields) {
2984
2922
  write(fields === void 0 ? { event } : {
2985
2923
  event,
@@ -3003,21 +2941,17 @@ function openStreamTrace(scope, options = {}) {
3003
2941
  * community-maintained integration; you need your own Command Code account
3004
2942
  * and API key or subscription, and Command Code's terms apply.
3005
2943
  *
3006
- * Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
3007
- * re-verified against command-code@1.62.0 — endpoints, request shape, and
3008
- * stream events unchanged):
3009
- * POST {apiBase}/alpha/generate
3010
- * body: { config, memory, taste, skills, params: { model, messages, tools,
3011
- * system, max_tokens, temperature, stream, reasoning_effort? }, threadId }
3012
- * SSE-ish JSONL events: text-delta | reasoning-start/delta/end | tool-call
3013
- * | tool-result | finish | error
3014
- * Model catalog: GET {apiBase}/provider/v1/models -> { object: 'list', data: [...] }
2944
+ * Owns the two chat transports (`/alpha/generate`, `/provider/v1/chat/completions`),
2945
+ * message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
2946
+ * the pre-stream account rotation loop. The wire protocol is
2947
+ * reverse-engineered (command-code@1.28.4, re-verified through 1.66.0);
2948
+ * AGENTS.md holds the full protocol record.
3015
2949
  *
3016
2950
  * The adapter is deliberately free of cordis/schemastery: it receives a
3017
2951
  * per-request options thunk and an API-key resolver from the plugin entry
3018
2952
  * (src/index.ts), so a settings change reaches the very next request.
3019
2953
  */
3020
- const COMMAND_CODE_CLI_VERSION = "1.65.0";
2954
+ const COMMAND_CODE_CLI_VERSION = "1.66.0";
3021
2955
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
3022
2956
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
3023
2957
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -3051,6 +2985,97 @@ function stringValue(value) {
3051
2985
  function numberValue(value) {
3052
2986
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3053
2987
  }
2988
+ /** Hash one request value without retaining or logging its contents. */
2989
+ function fingerprintValue(value) {
2990
+ let serialized;
2991
+ try {
2992
+ serialized = JSON.stringify(value) ?? String(value);
2993
+ } catch {
2994
+ serialized = String(value);
2995
+ }
2996
+ return {
2997
+ bytes: Buffer.byteLength(serialized),
2998
+ sha256: createHash("sha256").update(serialized).digest("hex")
2999
+ };
3000
+ }
3001
+ /**
3002
+ * Structural request facts for issue #64's cache diagnosis.
3003
+ *
3004
+ * This deliberately records hashes and counts, never the prompt, tool
3005
+ * descriptions, image bytes, headers, or credentials. `body` catches changes
3006
+ * in any wire field; the named sections make a changed system/tools/messages
3007
+ * prefix visible without making the trace a copy of the conversation.
3008
+ */
3009
+ function requestFingerprint(protocol, body) {
3010
+ const params = protocol === "cli" ? recordOrEmpty(body.params) : body;
3011
+ const messages = Array.isArray(params.messages) ? params.messages : [];
3012
+ const tools = Array.isArray(params.tools) ? params.tools : [];
3013
+ return {
3014
+ protocol,
3015
+ model: typeof params.model === "string" ? params.model : void 0,
3016
+ threadId: typeof body.threadId === "string" ? body.threadId : void 0,
3017
+ body: fingerprintValue(body),
3018
+ config: fingerprintValue(body.config),
3019
+ system: fingerprintValue(protocol === "cli" ? params.system : messages.filter((m) => isRecord(m) && m.role === "system")),
3020
+ tools: {
3021
+ count: tools.length,
3022
+ ...fingerprintValue(tools)
3023
+ },
3024
+ messages: {
3025
+ count: messages.length,
3026
+ roles: messages.map((message) => isRecord(message) && typeof message.role === "string" ? message.role : "unknown"),
3027
+ ...fingerprintValue(messages),
3028
+ items: messages.map((message) => fingerprintValue(message))
3029
+ }
3030
+ };
3031
+ }
3032
+ /** Only provider correlation headers, never a wholesale response-header dump. */
3033
+ const RESPONSE_ID_HEADERS = [
3034
+ "x-request-id",
3035
+ "request-id",
3036
+ "x-trace-id",
3037
+ "x-generation-id",
3038
+ "traceparent"
3039
+ ];
3040
+ function responseIdentifiers(headers) {
3041
+ const result = {};
3042
+ for (const name of RESPONSE_ID_HEADERS) {
3043
+ const value = diagnosticId(headers.get(name));
3044
+ if (value !== void 0) result[name] = value;
3045
+ }
3046
+ return result;
3047
+ }
3048
+ function diagnosticId(value) {
3049
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 512 ? value : void 0;
3050
+ }
3051
+ function captureResponseIdentifiers(metadata, event) {
3052
+ if (!isRecord(event)) return;
3053
+ if (metadata.protocol === "openai" && Array.isArray(event.choices) || event.type === "response-metadata") {
3054
+ const id = diagnosticId(event.id);
3055
+ if (id !== void 0) metadata.responseId = id;
3056
+ }
3057
+ for (const name of [
3058
+ "generationId",
3059
+ "providerRequestId",
3060
+ "traceId"
3061
+ ]) {
3062
+ const id = diagnosticId(event[name]);
3063
+ if (id !== void 0) metadata[name] = id;
3064
+ }
3065
+ }
3066
+ function hasResponseIdentifiers(metadata) {
3067
+ return Object.keys(metadata.headers).length > 0 || metadata.responseId !== void 0 || metadata.generationId !== void 0 || metadata.providerRequestId !== void 0 || metadata.traceId !== void 0;
3068
+ }
3069
+ /** Keep the provider's request id on DSH's durable failure, when it supplied one. */
3070
+ function failureWithRequestId(error, metadata) {
3071
+ const id = metadata.providerRequestId ?? metadata.headers["x-request-id"] ?? metadata.headers["request-id"];
3072
+ if (!(error instanceof LlmError) || error.failure.requestId !== void 0 || id === void 0) return error;
3073
+ return new LlmError(error.message, error.code, {
3074
+ ...error.failure,
3075
+ requestId: ProviderRequestId(id),
3076
+ ...error.cause === void 0 ? {} : { cause: error.cause }
3077
+ });
3078
+ }
3054
3079
  function booleanValue(value) {
3055
3080
  return typeof value === "boolean" ? value : void 0;
3056
3081
  }
@@ -3060,16 +3085,13 @@ function booleanValue(value) {
3060
3085
  *
3061
3086
  * Entries are read by their own text members rather than filtered on `type`:
3062
3087
  * DeepSeek sends `{ type: 'reasoning.text', text }` while the OpenAI family
3063
- * sends `{ type: 'reasoning.summary', summary, format: 'openai-responses-v1' }`
3064
- * — the same summary text the scalar `delta.reasoning` carries, in the
3065
- * Responses protocol's vocabulary. A `type` filter would drop one family or the
3066
- * other, and losing thinking is the failure this exists to prevent.
3067
- *
3068
- * `text` wins per entry, but an EMPTY `text` must not shadow a populated
3069
- * `summary` (the Responses wire emits `text: ''` placeholders), so the fallback
3070
- * tests for non-empty rather than merely present. Entries carrying neither
3071
- * member — the encrypted blobs — contribute nothing, and no entry contributes
3072
- * both, so a summary is never counted twice.
3088
+ * sends `{ type: 'reasoning.summary', summary }` — the same summary text the
3089
+ * scalar `delta.reasoning` carries. A `type` filter would drop one family, and
3090
+ * losing thinking is the failure this exists to prevent. `text` wins per entry,
3091
+ * but an EMPTY `text` must not shadow a populated `summary` (the Responses wire
3092
+ * emits `text: ''` placeholders), so the fallback tests for non-empty rather
3093
+ * than merely present. Encrypted-blob entries carry neither member and
3094
+ * contribute nothing; no entry carries both, so nothing is counted twice.
3073
3095
  *
3074
3096
  * Returns undefined when the array yields nothing, so the caller's `??` chain
3075
3097
  * keeps falling through instead of treating "no text" as a reasoning delta.
@@ -3090,9 +3112,7 @@ function reasoningDetailsText(value) {
3090
3112
  * none" — the same rule {@link reasoningDetailsText} applies inside the array.
3091
3113
  * The scalar and the array describe the same thinking, so an empty spelling of
3092
3114
  * one must not shadow populated text in the other (the Responses wire pads with
3093
- * `text: ''`). `??` alone cannot express this: an empty string is not nullish,
3094
- * so `reasoning: ''` beside a populated `reasoning_details` used to drop the
3095
- * whole chunk's thinking.
3115
+ * `text: ''`), and `??` alone cannot express that.
3096
3116
  */
3097
3117
  function nonEmptyText(value) {
3098
3118
  return value === void 0 || value === "" ? void 0 : value;
@@ -3156,30 +3176,21 @@ function isZdrNoProviders(providerCode, providerDetail, errText) {
3156
3176
  /**
3157
3177
  * Classify one in-band stream `error` payload into the failure the caller
3158
3178
  * throws. Shared by both transports — the CLI transport delivers it as an
3159
- * `error` event (`{ type: 'error', error }`), the Provider API transport as a
3160
- * top-level `error` member of an SSE chunk — so the two cannot drift apart.
3179
+ * `error` event, the Provider API transport as a top-level `error` member of
3180
+ * an SSE chunk — so the two cannot drift apart.
3161
3181
  *
3162
3182
  * The wording decides before the status does, mirroring the official CLI's
3163
- * `classifyKind` (which reads the message first) and the harness helpers
3164
- * (which exist so thrown and in-band delivery share one classifier). Order is
3165
- * load-bearing:
3183
+ * `classifyKind` (which reads the message first). Order is load-bearing:
3166
3184
  *
3167
3185
  * - A context-window rejection is terminal for THIS request but recoverable by
3168
3186
  * the harness: `dsh-compaction-basic` listens on `agent/request-error` for
3169
3187
  * `CONTEXT_WINDOW_EXCEEDED` and answers it with a compaction + retry of the
3170
- * reduced surface. That is what lets a long session survive a request that
3171
- * outgrew the model's window, and it is what the official CLI does (its
3172
- * non-retryable `truncated` kind). Reported as retryable `SERVER` — where
3173
- * this wording landed before — the adapter resent the byte-identical
3174
- * oversized request up to `maxRetries` times and the session could never
3175
- * recover, which is exactly the endless-retry-at-long-context report
3176
- * (issue #39).
3188
+ * reduced surface. Reported as retryable `SERVER` instead, the adapter resent
3189
+ * the byte-identical oversized request up to `maxRetries` times and the
3190
+ * session could never recover (issue #39).
3177
3191
  * - Credits/plan wording is terminal, so an exhausted balance or a model
3178
3192
  * outside the plan is not retried as if it were a transient rate limit.
3179
- * - Only then does the status/`isRetryable` pair decide — and an explicit
3180
- * refusal (`isRetryable: false`) or a terminal marker OUTRANKS a status, so a
3181
- * 5xx carrying "insufficient credits" stays terminal instead of entering the
3182
- * retry cadence as `SERVER`.
3193
+ * - Only then does the status/`isRetryable` pair decide.
3183
3194
  */
3184
3195
  function streamErrorToLlmError(value, fallbackMessage) {
3185
3196
  const record = isRecord(value) ? value : void 0;
@@ -3283,6 +3294,8 @@ function mergeCombinatorBranches(node, depth) {
3283
3294
  * resolved/merged; anything else (an array/scalar root, or no schema at all)
3284
3295
  * degrades to a permissive free-form object, because a request the provider
3285
3296
  * refuses helps no one and the tool's own description is still in the prompt.
3297
+ * Only the root is touched and every path returns a copy: the harness may
3298
+ * deep-freeze the caller's schema.
3286
3299
  */
3287
3300
  function toolParametersSchema(parameters, depth = 0) {
3288
3301
  if (!isRecord(parameters)) return {
@@ -3401,11 +3414,10 @@ async function writeModelsCache(cachePath, models) {
3401
3414
  * The tool result one message answers, or undefined when it answers none.
3402
3415
  *
3403
3416
  * The harness models a tool result as its own `role: 'tool'` message carrying
3404
- * `toolCallId`/`isError` beside raw content blocks (a closed role map:
3405
- * `MessageRoleMap.tool` is the only role a tool result can arrive on). Pairing
3406
- * and emission MUST agree on this same view: otherwise a dropped result either
3407
- * drops its call too (making the model forget completed work), or leaves an
3408
- * unanswered call on the wire.
3417
+ * `toolCallId`/`isError` beside raw content blocks (`MessageRoleMap.tool` is
3418
+ * the only role a tool result can arrive on). Pairing and emission MUST agree
3419
+ * on this same view, or a dropped result either drops its call too (making the
3420
+ * model forget completed work) or leaves an unanswered call on the wire.
3409
3421
  */
3410
3422
  function toolResultOf(message) {
3411
3423
  if (message.role !== "tool") return void 0;
@@ -3448,8 +3460,8 @@ function pairedToolCalls(messages) {
3448
3460
  * another provider issued the call — so overlong paired ids are remapped to
3449
3461
  * short per-request aliases. Correlation only needs to hold within one
3450
3462
  * request (each call travels with its result in the same body), so a
3451
- * sequential alias is enough: no durable state, no cross-request stability,
3452
- * and the harness log keeps the original ids.
3463
+ * sequential alias is enough: no durable state, and the harness log keeps the
3464
+ * original ids.
3453
3465
  */
3454
3466
  const MAX_WIRE_TOOL_CALL_ID_LENGTH = 64;
3455
3467
  /**
@@ -3481,7 +3493,7 @@ function blockText(block) {
3481
3493
  /**
3482
3494
  * Collect text and nested images of one tool result. The harness `read_image`
3483
3495
  * tool returns both, so dropping the image half is what made a tool-read image
3484
- * invisible to a Vision model (issue #30). Deduplication keeps a result that
3496
+ * invisible to a Vision model (issue #30); deduplication keeps a result that
3485
3497
  * repeats one attachment from paying for the same pixels twice.
3486
3498
  */
3487
3499
  function toolResultMedia(block) {
@@ -3521,17 +3533,14 @@ const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result";
3521
3533
  * tool it just ran rather than to the user; it also leads the part list,
3522
3534
  * because an image-first content array is what some gateways reject.
3523
3535
  *
3524
- * The note names the tool call the image came out of, because a turn's
3525
- * carriers are emitted as a group after the whole tool group (issue #33):
3526
- * position alone would have to carry the association, and every `read_image`
3527
- * result renders the same envelope text, so two parallel calls would produce
3528
- * two identical notes. The id is the WIRE id — the one the model saw on the
3529
- * `tool-call` it issued and on the tool message just above — so an overlong
3530
- * cross-provider id is named by the alias that replaced it, not by the
3531
- * harness-side id the model never saw.
3532
- *
3533
- * Count and pixel dimensions are appended when the tool's own text does not
3534
- * already state them (a result that returns the image and nothing else).
3536
+ * The note names the tool call the image came out of: a turn's carriers are
3537
+ * emitted as a group after the whole tool group (issue #33), so position
3538
+ * cannot carry the association — and every `read_image` result renders the
3539
+ * same envelope text, so two parallel calls would produce two identical
3540
+ * notes. The id is the WIRE id, the one the model saw on the `tool-call` and
3541
+ * on the tool message just above, so an overlong cross-provider id is named
3542
+ * by the alias that replaced it. Count and pixel dimensions are appended
3543
+ * when the tool's own text does not already state them.
3535
3544
  */
3536
3545
  function toolResultImageNote(media, toolCallId) {
3537
3546
  const lead = `${TOOL_RESULT_IMAGE_TEXT} (${toolCallId}):`;
@@ -3562,10 +3571,10 @@ const ENGINE_IMAGE_POLICY = {
3562
3571
  requiredImageOffload
3563
3572
  };
3564
3573
  /**
3565
- * The failure a route raises instead of evicting on its own. The
3566
- * count is the ONE thing the harness needs: `dsh-compaction-image-offload`
3567
- * records it as an `image/offload` event, marks that many oldest retained
3568
- * occurrences, and retries the step.
3574
+ * The failure a route raises instead of evicting on its own. The count is the
3575
+ * ONE thing the harness needs: `dsh-compaction-image-offload` records it as an
3576
+ * `image/offload` event, marks that many oldest retained occurrences, and
3577
+ * retries the step.
3569
3578
  *
3570
3579
  * Deliberately outside `providerRetryPolicy()`'s whitelist: a byte-identical
3571
3580
  * resend cannot succeed, so the retry belongs to the surface mutation, not to
@@ -3627,8 +3636,9 @@ async function readRequestImage(attachments, ref) {
3627
3636
  * Price one request occurrence.
3628
3637
  *
3629
3638
  * Two cases cost no vision tokens at all, and both are priced as their
3630
- * model-visible TEXT so the caller's estimator can charge that instead: a model
3631
- * that accepts text only (the harness projects those images to placeholder text
3639
+ * model-visible TEXT so the caller's estimator can charge that instead (which
3640
+ * is what stops the meter counting an evicted image as context): a model that
3641
+ * accepts text only (the harness projects those images to placeholder text
3632
3642
  * before the request), and an occurrence the session surface has already
3633
3643
  * offloaded (the same placeholder text rides in every later request). A retained
3634
3644
  * occurrence on this route contributes no text at all — the pixels are inlined —
@@ -3883,7 +3893,6 @@ function parseUsageTotals(usage) {
3883
3893
  periodBasis: stringValue(usage.periodBasis) ?? "billing-period"
3884
3894
  };
3885
3895
  }
3886
- /** Parse one window-limit block (`fiveHour` / `weekly`). */
3887
3896
  /**
3888
3897
  * Parse one window block into a window limit, or undefined when the endpoint
3889
3898
  * reported no such window.
@@ -3951,6 +3960,46 @@ function classifyTotalFailure(failures, failed) {
3951
3960
  if (failed.every((item) => item.kind === "invalid-response")) return "invalid-response";
3952
3961
  if (failed.every((item) => item.kind === "transport")) return "network";
3953
3962
  }
3963
+ /** UUID shape accepted by the official CLI's `toWireThreadId` helper. */
3964
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3965
+ const THREAD_ID_NAMESPACE = Buffer.from("6ba7b8119dad11d180b400c04fd430c8", "hex");
3966
+ /**
3967
+ * Map the harness session identity to the CLI transport's UUID thread ID.
3968
+ *
3969
+ * The official CLI creates one thread ID per agent run and reuses it for the
3970
+ * tool-loop requests that follow. DSH stamps its loop-owned session ID onto
3971
+ * GenerateOptions, but ordinary IDs are `session-<UUID>`, not bare UUIDs.
3972
+ * UUIDv5 gives every non-UUID session ID a stable, wire-compatible mapping.
3973
+ * An already valid UUID remains unchanged; a one-shot call without a session
3974
+ * ID still gets a fresh thread ID.
3975
+ * This alone does not establish cache affinity (issue #64's A/B found none).
3976
+ */
3977
+ function cliThreadId(sessionId) {
3978
+ if (!sessionId) return randomUUID();
3979
+ if (UUID_PATTERN.test(sessionId)) return sessionId;
3980
+ const digest = createHash("sha1").update(THREAD_ID_NAMESPACE).update("dsh-commandcode-provider/session/").update(sessionId).digest();
3981
+ digest[6] = digest[6] & 15 | 80;
3982
+ digest[8] = digest[8] & 63 | 128;
3983
+ const hex = digest.toString("hex", 0, 16);
3984
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3985
+ }
3986
+ /**
3987
+ * The CLI wire's explicit system-section form.
3988
+ *
3989
+ * The official CLI's current transport sends system text as an array and marks
3990
+ * stable sections with Anthropic-compatible ephemeral cache control. DSH's
3991
+ * adapter has one already-folded system string rather than the CLI's internal
3992
+ * section list, so preserve that text as one cacheable section. This does not
3993
+ * recover the CLI's separate static/dynamic boundaries or guarantee a hit.
3994
+ */
3995
+ function cliSystem(systemText) {
3996
+ if (!systemText) return "";
3997
+ return [{
3998
+ type: "text",
3999
+ text: systemText,
4000
+ cache_control: { type: "ephemeral" }
4001
+ }];
4002
+ }
3954
4003
  /** Build the legacy CLI (`/alpha/generate`) request body for one call. */
3955
4004
  async function buildCliBody(options, connection, facts) {
3956
4005
  return {
@@ -3977,13 +4026,13 @@ async function buildCliBody(options, connection, facts) {
3977
4026
  description: tool.description,
3978
4027
  input_schema: toolParametersSchema(tool.parameters)
3979
4028
  })),
3980
- system: facts.systemText,
4029
+ system: cliSystem(facts.systemText),
3981
4030
  max_tokens: facts.maxTokens,
3982
4031
  temperature: options.temperature ?? .3,
3983
4032
  stream: true,
3984
4033
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
3985
4034
  },
3986
- threadId: randomUUID()
4035
+ threadId: cliThreadId(options.sessionId)
3987
4036
  };
3988
4037
  }
3989
4038
  /** Build the documented Provider Chat Completions request body for one call. */
@@ -4012,12 +4061,11 @@ async function buildOpenAIBody(options, facts) {
4012
4061
  }
4013
4062
  /**
4014
4063
  * One pre-stream connect attempt: POST the body and wait for response
4015
- * headers only (`requestTimeoutMs` must never bound the body stream — see
4016
- * the caller). Returns the live response plus its cleanup, or the rejection
4017
- * facts for the rotation loop to classify. Every failure path cleans up
4018
- * before returning or throwing; on success the caller-abort listener
4019
- * outlives the connect phase (it aborts a stalled body read), so the
4020
- * streaming tail calls cleanup.
4064
+ * headers only (`requestTimeoutMs` must never bound the body stream). Returns
4065
+ * the live response plus its cleanup, or the rejection facts for the rotation
4066
+ * loop to classify. Every failure path cleans up before returning or throwing;
4067
+ * on success the caller-abort listener outlives the connect phase (it aborts a
4068
+ * stalled body read), so the streaming tail calls cleanup.
4021
4069
  */
4022
4070
  async function connectGenerate(deps, key, protocol, body) {
4023
4071
  const { options, connection, fetchImpl } = deps;
@@ -4074,6 +4122,7 @@ async function connectGenerate(deps, key, protocol, body) {
4074
4122
  if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)};Command Code API 请求在 ${connection.requestTimeoutMs} 毫秒内未收到响应——通常是网络或代理问题,请检查后重试`, "TIMEOUT", { cause: error });
4075
4123
  throw new LlmError(`Command Code API request to ${endpoint} failed: ${errorChain(error)};Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试`, "TRANSPORT", { cause: error });
4076
4124
  }
4125
+ deps.onResponse(response);
4077
4126
  if (!response.ok) {
4078
4127
  const errText = await response.text().catch(() => "");
4079
4128
  cleanup();
@@ -4102,6 +4151,7 @@ function createBlockAssembler() {
4102
4151
  reasoningContent: "",
4103
4152
  sawContent: false,
4104
4153
  finishReason: void 0,
4154
+ cliCacheWriteTokens: void 0,
4105
4155
  openAiToolCalls: []
4106
4156
  };
4107
4157
  }
@@ -4202,13 +4252,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
4202
4252
  * unbounded loop): `RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`/
4203
4253
  * `EMPTY_RESPONSE` retry up to 1000 times with waits doubling from 500 ms
4204
4254
  * and capping at 15 minutes (±10% jitter), so an exhausted 5-hour window
4205
- * recovers in-session instead of failing after two tries. Permanent
4206
- * failures (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`,
4207
- * plan rejections) are absent from the whitelist and surface immediately
4208
- * instead of looping. Waits the pool/adapter attach as
4209
- * `providerRetryAfterMs` are honored verbatim at or below the 15-minute
4210
- * cap and never attached above it (in normal mode a longer attached wait
4211
- * makes the executor abandon the retry outright — see RETRY_MAX_DELAY_MS).
4255
+ * recovers in-session instead of failing after two tries. Permanent failures
4256
+ * (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
4257
+ * rejections) are absent from the whitelist and surface immediately instead
4258
+ * of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
4259
+ * honored verbatim at or below the 15-minute cap and never attached above it
4260
+ * (in normal mode a longer attached wait makes the executor abandon the
4261
+ * retry outright — see RETRY_MAX_DELAY_MS).
4212
4262
  *
4213
4263
  * Captured once at route registration (dsh-llm snapshots this value), so a
4214
4264
  * future config knob for it would apply on profile restart, not per request.
@@ -4239,12 +4289,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4239
4289
  * session reports far less context than it is actually carrying. The price is
4240
4290
  * computed at the dimensions this route would send (the same request target
4241
4291
  * `readImageRequest` encodes to), so the estimate tracks the wire rather than
4242
- * the stored original.
4243
- *
4244
- * Both payload generations are handled, because they disagree: ≤0.1.5 hands
4245
- * over bare `ImageAttachmentRef`s, ≥0.1.6 hands over `ImageBlock`s that also
4246
- * carry the surface's `offloaded` mark. Returning a price per occurrence, in
4247
- * order, is a hard requirement — the meter throws when the counts differ.
4292
+ * the stored original. One price per occurrence, in order, is a hard
4293
+ * requirement — the meter throws when the counts differ.
4248
4294
  */
4249
4295
  imageRequestPricing(_provider, model) {
4250
4296
  const imageCapable = KNOWN_IMAGE_MODELS.has(model);
@@ -4304,6 +4350,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4304
4350
  name: entry ? `${entry.name} (CC)` : model,
4305
4351
  description: capabilityDescription(model, entry?.contextWindow),
4306
4352
  inputModalities: vision ? ["text", "image"] : ["text"],
4353
+ toolUpdate: "addition-only",
4307
4354
  ...entry ? {
4308
4355
  context: { contextWindow: entry.contextWindow },
4309
4356
  defaultMaxTokens: Math.min(entry.maxTokens, DEFAULT_GENERATE_MAX_TOKENS)
@@ -4329,10 +4376,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
4329
4376
  }
4330
4377
  /**
4331
4378
  * Fetch one account endpoint and parse its JSON body. Returns the HTTP
4332
- * status alongside the parsed record so each caller applies its own
4333
- * failure accounting: the billing probe fails open silently, the usage
4334
- * report books failures per endpoint. Non-2xx and invalid JSON bodies come
4335
- * back without a record; only a fetch failure propagates to the caller.
4379
+ * status alongside the parsed record so each caller applies its own failure
4380
+ * accounting: the billing probe fails open silently, the usage report books
4381
+ * failures per endpoint. Non-2xx and invalid JSON bodies come back without a
4382
+ * record; only a fetch failure propagates to the caller.
4336
4383
  *
4337
4384
  * `timeoutMs` is the caller's budget for this one request. The default is the
4338
4385
  * CATALOG probe's short cap, which fits the picker's fail-open reads; the
@@ -4369,9 +4416,6 @@ var CommandCodeAdapter = class extends LlmAdapter {
4369
4416
  * per picker load). `undefined` means the filter cannot be evaluated at all
4370
4417
  * — no key resolved — which {@link modelVisibleForAnyAccount} reads as
4371
4418
  * "show everything".
4372
- *
4373
- * The serving account is only the first entry: with several accounts the
4374
- * model list must not depend on which of them rotation happens to be using.
4375
4419
  */
4376
4420
  async loadPoolBillingAccess() {
4377
4421
  const keys = await this.poolAccountKeys();
@@ -4484,10 +4528,9 @@ var CommandCodeAdapter = class extends LlmAdapter {
4484
4528
  * but a hard refusal. `/alpha/generate` routes every one of them, so this
4485
4529
  * costs nothing.
4486
4530
  *
4487
- * That decision is deliberately NOT written to `protocolCache`. The cache is
4488
- * keyed by API key alone, so remembering it would pin the whole ACCOUNT to
4489
- * the CLI transport and drag every other model — DeepSeek, GLM, Qwen, all of
4490
- * which the Provider API serves correctly — off it until the entry expired.
4531
+ * That decision is deliberately NOT written to `protocolCache`, which is
4532
+ * keyed by API key alone: remembering it would pin the whole ACCOUNT to the
4533
+ * CLI transport and drag every other model off it until the entry expired.
4491
4534
  */
4492
4535
  resolveProtocol(apiKey, model) {
4493
4536
  const forced = this.deps.options().protocol;
@@ -4513,18 +4556,14 @@ var CommandCodeAdapter = class extends LlmAdapter {
4513
4556
  * the default resolves the currently active account.
4514
4557
  *
4515
4558
  * Two failure modes are NOT "the network is down", and the report must say
4516
- * so rather than let {@link classifyTotalFailure} blame the connection:
4517
- *
4518
- * - A key that no HTTP header can carry (a stray newline from a paste, a
4519
- * full-width character) makes `fetch` throw a `TypeError` **before any
4520
- * I/O**, for every endpoint at once. The chat path refuses such a key
4521
- * through `assertUsableApiKey`; this path must too, and it answers
4522
- * `blocked: 'invalid-key'` instead of four phantom transport failures.
4523
- * - The four endpoints get the SAME per-request budget as a chat call
4524
- * (`connection.requestTimeoutMs`, default 60 s), not the catalog's 10 s
4525
- * probe budget: on a slow link a 10 s cap made every account query time
4526
- * out while chat kept working, which is exactly a "check your network"
4527
- * banner that never clears.
4559
+ * so rather than let {@link classifyTotalFailure} blame the connection: a
4560
+ * key that no HTTP header can carry (a stray newline from a paste, a
4561
+ * full-width character) makes `fetch` throw a `TypeError` before any I/O,
4562
+ * for every endpoint at once, so this path runs `assertUsableApiKey` too and
4563
+ * answers `blocked: 'invalid-key'`; and the four endpoints get the SAME
4564
+ * per-request budget as a chat call (`connection.requestTimeoutMs`, default
4565
+ * 60 s), not the catalog's 10 s probe budget, because on a slow link a 10 s
4566
+ * cap made every account query time out while chat kept working.
4528
4567
  */
4529
4568
  async getUsage(apiKey) {
4530
4569
  const connection = this.deps.options();
@@ -4602,17 +4641,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
4602
4641
  * the same time (the two are metered separately), and reading only the
4603
4642
  * shorter one revived such an account as "usable" — the pool then handed it
4604
4643
  * out, the provider rejected the request again, and the next all-marked pass
4605
- * revived it once more, so a used-up account kept coming back (issue #51's
4606
- * follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
4607
- * published window is exceeded, and `resetAt` is the LATEST reset among the
4608
- * exceeded ones — the binding constraint, because a clear five-hour window
4609
- * buys nothing while the weekly quota is spent.
4644
+ * revived it once more (issue #51's follow-up: "切到一个不可用账号").
4645
+ * `exceeded` is therefore true when ANY published window is exceeded, and
4646
+ * `resetAt` is the LATEST reset among the exceeded ones — the binding
4647
+ * constraint, because a clear five-hour window buys nothing while the weekly
4648
+ * quota is spent.
4610
4649
  *
4611
4650
  * Returns `undefined` when the probe itself failed (transport, non-200, or a
4612
4651
  * payload with no window limits at all) — a failed probe never changes pool
4613
- * state. The monthly/purchased/free credit balances are deliberately NOT part
4614
- * of this answer: they are balances rather than windows, they carry no reset
4615
- * time, and the server remains the final gate on them.
4652
+ * state. The credit balances are deliberately NOT part of this answer: they
4653
+ * carry no reset time, and the server remains the final gate on them.
4616
4654
  */
4617
4655
  async probeWindowLimits(apiKey) {
4618
4656
  try {
@@ -4678,73 +4716,110 @@ var CommandCodeAdapter = class extends LlmAdapter {
4678
4716
  readImage
4679
4717
  });
4680
4718
  let body = await buildBody(protocol);
4719
+ const trace = openStreamTrace(`${options.provider}/${options.model}`);
4681
4720
  const tried = /* @__PURE__ */ new Set();
4721
+ let attemptNumber = 0;
4722
+ let responseMetadata = {
4723
+ protocol,
4724
+ headers: {}
4725
+ };
4682
4726
  const connectDeps = {
4683
4727
  options,
4684
4728
  connection,
4685
- fetchImpl: this.fetchImpl
4729
+ fetchImpl: this.fetchImpl,
4730
+ onResponse: (response) => {
4731
+ responseMetadata = {
4732
+ protocol,
4733
+ headers: responseIdentifiers(response.headers)
4734
+ };
4735
+ trace.record("response", {
4736
+ protocol,
4737
+ endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
4738
+ status: response.status,
4739
+ attempt: attemptNumber,
4740
+ accountsTried: tried.size,
4741
+ maxTokens,
4742
+ ...reasoningEffort === void 0 ? {} : { reasoningEffort },
4743
+ headers: responseMetadata.headers
4744
+ });
4745
+ }
4686
4746
  };
4687
4747
  let connected;
4688
- for (;;) {
4689
- tried.add(apiKey);
4690
- const attempt = await connectGenerate(connectDeps, apiKey, protocol, body);
4691
- if ("response" in attempt) {
4692
- connected = attempt;
4693
- break;
4694
- }
4695
- if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText)) {
4696
- protocol = "cli";
4697
- this.rememberProtocol(apiKey, true);
4698
- body = await buildBody("cli");
4699
- continue;
4700
- }
4701
- if (attempt.status === 413 && REQUEST_IMAGE_BUDGETS.length > 1) {
4702
- const missing = this.surfaceOffload.requiredImageOffload(requestOptions.messages, coreBudget(REQUEST_IMAGE_BUDGETS[1]), imageVersionBytes);
4703
- if (missing > 0) throw imageOffloadRequired(missing);
4704
- }
4705
- const rotate = this.deps.rotateApiKey;
4706
- const rejection = classifyAccountRejection(attempt.status, attempt.errText);
4707
- if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4708
- const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4709
- tried: [...tried],
4710
- ...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
4748
+ try {
4749
+ for (;;) {
4750
+ tried.add(apiKey);
4751
+ attemptNumber++;
4752
+ responseMetadata = {
4753
+ protocol,
4754
+ headers: {}
4755
+ };
4756
+ if (trace.enabled) trace.record("request", {
4757
+ attempt: attemptNumber,
4758
+ sessionId: options.sessionId,
4759
+ ...requestFingerprint(protocol, body)
4711
4760
  });
4712
- if (next !== void 0 && !tried.has(next)) {
4713
- apiKey = next;
4761
+ const attempt = await connectGenerate(connectDeps, apiKey, protocol, body);
4762
+ if ("response" in attempt) {
4763
+ connected = attempt;
4764
+ break;
4765
+ }
4766
+ if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText)) {
4767
+ protocol = "cli";
4768
+ this.rememberProtocol(apiKey, true);
4769
+ body = await buildBody("cli");
4714
4770
  continue;
4715
4771
  }
4772
+ if (attempt.status === 413 && REQUEST_IMAGE_BUDGETS.length > 1) {
4773
+ const missing = this.surfaceOffload.requiredImageOffload(requestOptions.messages, coreBudget(REQUEST_IMAGE_BUDGETS[1]), imageVersionBytes);
4774
+ if (missing > 0) throw imageOffloadRequired(missing);
4775
+ }
4776
+ const rotate = this.deps.rotateApiKey;
4777
+ const rejection = classifyAccountRejection(attempt.status, attempt.errText);
4778
+ if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4779
+ const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4780
+ tried: [...tried],
4781
+ ...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
4782
+ });
4783
+ if (next !== void 0 && !tried.has(next)) {
4784
+ apiKey = next;
4785
+ continue;
4786
+ }
4787
+ }
4788
+ if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4789
+ const reset = rejection.resetAtMs;
4790
+ const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4791
+ const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4792
+ const window = rejection.reason === "rate-limit";
4793
+ throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
4794
+ }
4795
+ throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
4716
4796
  }
4717
- if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4718
- const reset = rejection.resetAtMs;
4719
- const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4720
- const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4721
- const window = rejection.reason === "rate-limit";
4722
- throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
4723
- }
4724
- throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
4797
+ } catch (error) {
4798
+ trace.record("connect-error", {
4799
+ message: boundTraceText(errorChain(error)),
4800
+ responseMetadata
4801
+ });
4802
+ trace.close();
4803
+ throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
4725
4804
  }
4726
4805
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
4727
4806
  const { response, cleanup } = connected;
4728
4807
  if (!response.body) {
4729
4808
  cleanup();
4730
- throw new LlmError("Command Code API returned no response body", "PROVIDER_PROTOCOL_ERROR");
4809
+ trace.record("end", {
4810
+ outcome: "empty",
4811
+ code: "PROVIDER_PROTOCOL_ERROR"
4812
+ });
4813
+ trace.close();
4814
+ throw failureWithRequestId(new LlmError("Command Code API returned no response body", "PROVIDER_PROTOCOL_ERROR"), responseMetadata);
4731
4815
  }
4732
4816
  const reader = response.body.getReader();
4733
4817
  const decoder = new TextDecoder();
4734
4818
  let buffer = "";
4735
- const trace = openStreamTrace(`${options.provider}/${options.model}`);
4736
4819
  const streamStartedAt = Date.now();
4737
4820
  let chunkCount = 0;
4738
4821
  let totalBytes = 0;
4739
4822
  let lastChunkAt = streamStartedAt;
4740
- trace.record("response", {
4741
- protocol,
4742
- endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
4743
- status: response.status,
4744
- attempts: tried.size,
4745
- maxTokens,
4746
- ...reasoningEffort === void 0 ? {} : { reasoningEffort }
4747
- });
4748
4823
  let idleTimer;
4749
4824
  let idleFired = false;
4750
4825
  const armIdle = () => {
@@ -4766,6 +4841,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4766
4841
  let usageEmitted = false;
4767
4842
  let endRecorded = false;
4768
4843
  function* handle(event) {
4844
+ captureResponseIdentifiers(responseMetadata, event);
4769
4845
  for (const chunk of handleEvent(asm, protocol, event)) if (chunk.type === "finish") finish = chunk;
4770
4846
  else if (chunk.type === "usage") usage = chunk.usage;
4771
4847
  else yield chunk;
@@ -4836,11 +4912,15 @@ var CommandCodeAdapter = class extends LlmAdapter {
4836
4912
  chunks: chunkCount,
4837
4913
  bytes: totalBytes,
4838
4914
  totalMs: Date.now() - streamStartedAt,
4839
- sawContent: asm.sawContent
4915
+ sawContent: asm.sawContent,
4916
+ responseMetadata
4840
4917
  });
4841
4918
  endRecorded = true;
4842
4919
  if (failure !== void 0) throw failure;
4843
- yield finish;
4920
+ yield hasResponseIdentifiers(responseMetadata) ? {
4921
+ ...finish,
4922
+ replayState: { response: responseMetadata }
4923
+ } : finish;
4844
4924
  } else {
4845
4925
  const elapsed = Date.now() - streamStartedAt;
4846
4926
  const detail = `${chunkCount} chunk(s), ${totalBytes} bytes, ${elapsed}ms`;
@@ -4849,7 +4929,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4849
4929
  chunks: chunkCount,
4850
4930
  bytes: totalBytes,
4851
4931
  totalMs: elapsed,
4852
- sawContent: asm.sawContent
4932
+ sawContent: asm.sawContent,
4933
+ responseMetadata
4853
4934
  });
4854
4935
  endRecorded = true;
4855
4936
  if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
@@ -4869,9 +4950,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
4869
4950
  sawContent: asm.sawContent,
4870
4951
  chunks: chunkCount,
4871
4952
  bytes: totalBytes,
4872
- totalMs: Date.now() - streamStartedAt
4953
+ totalMs: Date.now() - streamStartedAt,
4954
+ responseMetadata
4873
4955
  });
4874
- throw error;
4956
+ throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
4875
4957
  } finally {
4876
4958
  clearIdle();
4877
4959
  cleanup();
@@ -4888,16 +4970,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
4888
4970
  * status alone cannot distinguish them. The status also decides the failure
4889
4971
  * CODE via {@link httpErrorCode}, which is what the retry policy routes on —
4890
4972
  * a transient 5xx must not be reported as a permanent provider rejection. A
4891
- * 429's `Retry-After` header rides along as
4892
- * `providerRetryAfterMs` so dsh-llm-retry can wait exactly that long instead
4893
- * of guessing at the backoff cadence — capped at RETRY_MAX_DELAY_MS, because
4894
- * in normal mode a longer attached wait makes the executor abandon the retry
4895
- * outright instead of falling back to local backoff.
4973
+ * 429's `Retry-After` header rides along as `providerRetryAfterMs` so
4974
+ * dsh-llm-retry can wait exactly that long instead of guessing at the backoff
4975
+ * cadence — capped at RETRY_MAX_DELAY_MS, because in normal mode a longer
4976
+ * attached wait makes the executor abandon the retry outright.
4896
4977
  *
4897
4978
  * One body is read, not just its status: a client-side rejection whose
4898
4979
  * `error.code`/`type`/`message` names the model context window is reported as
4899
- * `CONTEXT_WINDOW_EXCEEDED` (see the branch below), because that failure has a
4900
- * recovery the harness performs and a generic provider error does not.
4980
+ * `CONTEXT_WINDOW_EXCEEDED`, because that failure has a recovery the harness
4981
+ * performs and a generic provider error does not. Every message built here is
4982
+ * bilingual — the harness renders it verbatim in its retry chrome.
4901
4983
  */
4902
4984
  function generateHttpError(status, errText, retryAfterMs) {
4903
4985
  let providerCode;
@@ -4930,16 +5012,11 @@ function generateHttpError(status, errText, retryAfterMs) {
4930
5012
  * status, never the provider's `error.type` — so a status that arrives here as
4931
5013
  * a permanent code is never retried, whatever it says about itself.
4932
5014
  *
4933
- * 5xx is the provider's own "we are temporarily unavailable" class (Cloudflare's
4934
- * 520-527 included; a 520 body says exactly that in words) and the only sane
4935
- * answer to it is the byte-identical resend the retry policy exists to make.
4936
- * Keeping it on `PROVIDER_HTTP_ERROR` — absent from the whitelist by design, so
4937
- * that a 403 plan rejection or a 400 shape error fails fast — made the one
4938
- * failure that asks to be retried the only one that never was, while the same
4939
- * status arriving as an in-band stream `error` event was classified retryable
4940
- * `SERVER` (see the `error` case of {@link handleCliEvent}). 408 is the same
4941
- * story for the request itself. Everything else stays permanent: a 400/403/404/
4942
- * 409/422 rejection repeats identically on every attempt.
5015
+ * 5xx is the provider's own "temporarily unavailable" class (Cloudflare's
5016
+ * 520-527 included) and the only sane answer to it is the byte-identical
5017
+ * resend the retry policy exists to make; 408 is the same story for the
5018
+ * request itself. Everything else stays permanent: a 400/403/404/409/422
5019
+ * rejection repeats identically on every attempt.
4943
5020
  */
4944
5021
  function httpErrorCode(status) {
4945
5022
  if (status === 429) return "RATE_LIMIT";
@@ -5029,22 +5106,29 @@ function handleCliEvent(asm, event) {
5029
5106
  });
5030
5107
  break;
5031
5108
  }
5109
+ case "cache-write-tokens": {
5110
+ const cacheWrite = numberValue(event.cacheWriteTokens);
5111
+ if (cacheWrite !== void 0 && cacheWrite >= 0) asm.cliCacheWriteTokens = cacheWrite;
5112
+ break;
5113
+ }
5032
5114
  case "finish": {
5033
5115
  asm.finishReason = stringValue(event.finishReason);
5034
5116
  chunks.push(...closeText(asm), ...closeReasoning(asm));
5035
5117
  const usage = isRecord(event.totalUsage) ? event.totalUsage : void 0;
5036
- if (usage) {
5037
- const details = isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : void 0;
5038
- const totalInput = numberValue(usage.inputTokens) ?? 0;
5118
+ const capturedCacheWrite = asm.cliCacheWriteTokens;
5119
+ if (usage || capturedCacheWrite !== void 0) {
5120
+ const details = isRecord(usage?.inputTokenDetails) ? usage.inputTokenDetails : void 0;
5121
+ const totalInput = numberValue(usage?.inputTokens) ?? 0;
5039
5122
  const cacheRead = numberValue(details?.cacheReadTokens) ?? 0;
5040
- const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0;
5123
+ const reportedCacheWrite = numberValue(details?.cacheWriteTokens) ?? 0;
5124
+ const cacheWrite = reportedCacheWrite === 0 && capturedCacheWrite !== void 0 ? capturedCacheWrite : reportedCacheWrite;
5041
5125
  const tokenUsage = {
5042
5126
  inputTokens: numberValue(details?.noCacheTokens) ?? Math.max(0, totalInput - cacheRead - cacheWrite),
5043
- outputTokens: numberValue(usage.outputTokens) ?? 0,
5127
+ outputTokens: numberValue(usage?.outputTokens) ?? 0,
5044
5128
  cacheReadTokens: cacheRead,
5045
5129
  cacheWriteTokens: cacheWrite
5046
5130
  };
5047
- const reasoningTokens = numberValue((isRecord(usage.outputTokenDetails) ? usage.outputTokenDetails : void 0)?.reasoningTokens) ?? numberValue(usage.reasoningTokens);
5131
+ const reasoningTokens = numberValue((isRecord(usage?.outputTokenDetails) ? usage.outputTokenDetails : void 0)?.reasoningTokens) ?? numberValue(usage?.reasoningTokens);
5048
5132
  if (reasoningTokens !== void 0) tokenUsage.reasoningTokens = reasoningTokens;
5049
5133
  chunks.push({
5050
5134
  type: "usage",
@@ -5197,12 +5281,6 @@ function emptyCompletionError(reason, maxTokens, usage) {
5197
5281
  return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
5198
5282
  }
5199
5283
  /**
5200
- * True when a Provider API pre-stream rejection is Command Code's Go-plan
5201
- * gate (`upgrade_required`). Only this exact class of rejection should fall
5202
- * back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
5203
- * ordinary errors so real account/model problems are not masked.
5204
- */
5205
- /**
5206
5284
  * Classify a pre-stream rejection that is about the ACCOUNT rather than about
5207
5285
  * the request, using the official CLI's own rules (command-code@1.56.0:
5208
5286
  * `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
@@ -5212,46 +5290,31 @@ function emptyCompletionError(reason, maxTokens, usage) {
5212
5290
  * oversized body or a context overflow must fail fast instead of walking the
5213
5291
  * whole account pool.
5214
5292
  *
5215
- * Issue #51's follow-up is why this exists. Only 429/401 used to rotate, so
5216
- * every other account-scoped rejection was terminal AND invisible to the pool:
5217
- * an account that could not pay, or could not run the model, stayed "usable",
5218
- * was handed out again on the next request, and produced the same error again
5219
- * ("切到一个不可用账号后就报 400,还挺频繁"). The three reasons differ in what
5220
- * the host should do with them, which is the caller's decision, not this
5221
- * function's:
5293
+ * It must recognize the whole account-scoped class, not just 429/401: when it
5294
+ * did not, an account that could not pay, or could not run the model, stayed
5295
+ * "usable" in the pool and was handed out again on the next request (issue
5296
+ * #51's follow-up). The four reasons are defined on
5297
+ * {@link AccountRotationReason}; what this function owes them is the evidence
5298
+ * split:
5222
5299
  *
5223
- * - `rate-limit`: a usage WINDOW is spent. The code `RATE_LIMITED` is the
5224
- * authoritative signal — the CLI accepts the code OR a 429 status — and the
5225
- * body's `error.rateLimit` names the window and its `reset` (in seconds),
5226
- * so the host can hold the account out until the provider's own reset time.
5227
- * - `throttled`: the provider refused the request (429 or a bare
5228
- * `RATE_LIMITED` code) without naming a usage window. It is account-scoped
5229
- * like the above, so it rotates and marks the key the same way, but it is
5230
- * NOT evidence that a metered window is spent: the CLI's own
5231
- * `parseWindowLimitError` returns a window limit only when
5232
- * `resolveWindowLabel` can name one, and a burst/model/spend limiter the
5233
- * billing endpoint cannot see is exactly this shape (issue #54's report: a
5234
- * 429 whose probe read five-hour 2% and weekly 10% while the error claimed
5235
- * both were exhausted).
5236
- * - `invalid-credential`: 401; the key itself is bad.
5237
- * - `unavailable`: the key is fine but this account cannot serve: no credits
5238
- * (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
5239
- * `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
5240
- * or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
5241
- * the account: the fact is about the model or the balance, it can change
5242
- * without the key changing, and a `:free` model can still be served by an
5243
- * account with an empty balance. These codes are recognized on EVERY
5244
- * status — a 5xx that proxies the provider's own credits/plan body is
5245
- * still an account fact, and only the prose scan is confined to 4xx.
5300
+ * - `rate-limit` / `throttled`: the code `RATE_LIMITED` is the authoritative
5301
+ * signal — the CLI accepts the code OR a 429 status — and only a body that
5302
+ * NAMES a window (`error.rateLimit.window`, see {@link readWindowLimitEvidence})
5303
+ * may be called a spent one. A bare 429 is a burst/model/spend limiter the
5304
+ * billing endpoint cannot see, so it stays `throttled` and the host claims
5305
+ * no window (issue #54).
5306
+ * - `unavailable`: no credits, or a model outside this account's plan, so it
5307
+ * rotates but must NOT mark the key — the fact is about the model or the
5308
+ * balance, and either can change without the key changing.
5246
5309
  */
5247
5310
  /**
5248
5311
  * The provider's structured "this account cannot serve this request" codes.
5249
5312
  *
5250
5313
  * Status-independent on purpose, mirroring the CLI's classifier, which reads
5251
- * the code before any status guard. A 5xx that carries one of these must not
5252
- * be retried as a transient provider failure: the same request against the
5253
- * same account cannot succeed, so the caller rotates past it (without marking
5254
- * the key) instead of replaying it on the harness's retry cadence.
5314
+ * the code before any status guard: a 5xx carrying one of these must rotate
5315
+ * past the account instead of being retried as a transient provider failure,
5316
+ * because the same request against the same account cannot succeed. Only the
5317
+ * prose scan below is confined to 4xx.
5255
5318
  */
5256
5319
  const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
5257
5320
  "USAGE_EXCEEDED",
@@ -5301,12 +5364,12 @@ const MAX_TRUSTED_RESET_MS = 2592e6;
5301
5364
  * `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
5302
5365
  *
5303
5366
  * `window` is present only when the body actually names one: a
5304
- * `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the
5305
- * "usage limit for your plan" wording (read as `weekly` when it says so, else
5306
- * `fiveHour`). That is the CLI's own bar for calling a rejection a WINDOW
5307
- * limit, and it is what keeps a bare 429 — a burst limiter, a model-level
5308
- * limit, a spend cap `windowLimits` cannot show — from being reported as an
5309
- * exhausted window (issue #54).
5367
+ * `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the "usage limit
5368
+ * for your plan" wording (read as `weekly` when it says so, else `fiveHour`).
5369
+ * That is the CLI's own bar for calling a rejection a WINDOW limit, and it is
5370
+ * what keeps a bare 429 — a burst limiter, a model-level limit, a spend cap
5371
+ * `windowLimits` cannot show — from being reported as an exhausted window
5372
+ * (issue #54).
5310
5373
  *
5311
5374
  * `resetAtMs` is the reset the body publishes: `error.rateLimit.reset` is
5312
5375
  * SECONDS (`1e3 * reset`, like the CLI), and a `resets at <ISO>` stamp in the
@@ -5337,6 +5400,13 @@ function readWindowLimitEvidence(errText) {
5337
5400
  ...resetAtMs === void 0 ? {} : { resetAtMs }
5338
5401
  };
5339
5402
  }
5403
+ /**
5404
+ * True when a pre-stream rejection is Command Code's Go-plan gate
5405
+ * (`upgrade_required`) — the only plan without Provider API access. Only this
5406
+ * class of rejection should fall back to the CLI `/alpha/generate` transport;
5407
+ * other 4xx/5xx must surface as ordinary errors so real account/model
5408
+ * problems are not masked.
5409
+ */
5340
5410
  function isUpgradeRequiredError(status, errText) {
5341
5411
  if (status !== 403) return false;
5342
5412
  const lower = errText.toLowerCase();
@@ -5352,7 +5422,9 @@ function isUpgradeRequiredError(status, errText) {
5352
5422
  const message = stringValue(error.message) ?? "";
5353
5423
  if (message.toLowerCase().includes("go plan") && message.toLowerCase().includes("api access")) return true;
5354
5424
  }
5355
- } catch {}
5425
+ } catch {
5426
+ return false;
5427
+ }
5356
5428
  return false;
5357
5429
  }
5358
5430
  /** Map an OpenAI Chat Completions usage payload to harness disjoint counts. */
@@ -5405,7 +5477,6 @@ const commandcodeCommand = {
5405
5477
  weeklyLine: " 📅 每周 {used} / {cap}{warn}",
5406
5478
  windowBarLine: " └ {bar} 重置 {when}",
5407
5479
  exceededWarning: " ⚠️ 超限!",
5408
- resetSuffix: "重置 {when}",
5409
5480
  partialFailures: "⚠️ 部分端点失败: {list}",
5410
5481
  noData: "(无数据 — 请检查 API 密钥)",
5411
5482
  errorText: "获取 Command Code 用量失败:{message}"
@@ -5437,25 +5508,19 @@ const commandcodeCommand = {
5437
5508
  weeklyLine: " 📅 Weekly {used} / {cap}{warn}",
5438
5509
  windowBarLine: " └ {bar} resets {when}",
5439
5510
  exceededWarning: " ⚠️ exceeded!",
5440
- resetSuffix: "resets {when}",
5441
5511
  partialFailures: "⚠️ some endpoints failed: {list}",
5442
5512
  noData: "(no data — check your API key)",
5443
5513
  errorText: "Could not fetch Command Code usage: {message}"
5444
5514
  }
5445
5515
  };
5446
5516
  /**
5447
- * Resolve the active locale for a Host-side command run.
5448
- *
5449
- * Priority: explicit `override` (from `Config.lang`) → `LC_ALL` → `LANG` →
5450
- * the conventional fallback (`'zh'`, matching the existing single-language
5451
- * behavior so unconfigured deployments keep their current output).
5517
+ * Resolve the active locale for a Host-side command run: explicit `override`
5518
+ * (from `Config.lang`) → `LC_ALL` → `LANG` → the `'zh'` fallback that keeps
5519
+ * unconfigured deployments on their current output.
5452
5520
  *
5453
- * The values are matched on the leading tag only — `zh_CN.UTF-8`,
5454
- * `zh-Hans`, `zh` all map to `'zh'`; everything starting with `en` maps to
5455
- * `'en'`; anything else falls back to `'zh'` (a non-`en` shell that
5456
- * already has Chinese in the terminal is the closest sensible default;
5457
- * a Western shell that happens to be neither keeps the existing Chinese
5458
- * output rather than swapping to half-translated English).
5521
+ * Matched on the leading tag only — `zh_CN.UTF-8`, `zh-Hans` and `zh` all
5522
+ * map to `'zh'`, anything starting with `en` maps to `'en'`, everything else
5523
+ * falls back to `'zh'` rather than to half-translated English.
5459
5524
  */
5460
5525
  function pickCommandLocale(override, env = process.env) {
5461
5526
  if (override === "zh" || override === "en") return override;
@@ -5468,11 +5533,10 @@ function commandCopy(locale, key) {
5468
5533
  }
5469
5534
  //#endregion
5470
5535
  //#region src/commands.ts
5471
- /** Format a dollar amount. */
5472
5536
  function money(value) {
5473
5537
  return `$${value.toFixed(4)}`;
5474
5538
  }
5475
- /** Format a dollar amount compactly (2 decimals). */
5539
+ /** Compact dollar amount (2 decimals), for the summary and window rows. */
5476
5540
  function moneyShort(value) {
5477
5541
  return `$${value.toFixed(2)}`;
5478
5542
  }
@@ -5484,14 +5548,14 @@ function tokensCompact(value) {
5484
5548
  return String(value);
5485
5549
  }
5486
5550
  /**
5487
- * Format a success-rate percentage (already in percent units): at most two
5488
- * decimals, trailing zeros trimmed — mirrors `formatSuccessRate` in
5551
+ * Success-rate percentage (already in percent units): at most two decimals,
5552
+ * trailing zeros trimmed — mirrors `formatSuccessRate` in
5489
5553
  * `./client/usage.ts`, which the settings card uses.
5490
5554
  */
5491
5555
  function successRateText(value) {
5492
5556
  return String(Number(value.toFixed(2)));
5493
5557
  }
5494
- /** Format a millis timestamp as a local date; `n/a` when unset. */
5558
+ /** A local date, or `n/a` when the provider published no reset time. */
5495
5559
  function resetLabel(ms) {
5496
5560
  if (ms <= 0) return "n/a";
5497
5561
  return new Date(ms).toLocaleString();
@@ -5595,16 +5659,16 @@ const REMOTE_SERVICE = "commandcodeUsage";
5595
5659
  /** The wire namespace all Command Code endpoints share. */
5596
5660
  const REMOTE_NAMESPACE = "commandcode";
5597
5661
  /**
5598
- * Build the validator helpers one endpoint uses. `prefix` names the
5599
- * endpoint in the rejection message (e.g. `commandcode/report result:`), so
5600
- * each wire file keeps its own diagnostic phrasing while sharing the helper
5601
- * bodies.
5662
+ * Build the validator helpers one endpoint uses. `prefix` names the endpoint
5663
+ * in the rejection message (e.g. `commandcode/report result:`), so each wire
5664
+ * file keeps its own diagnostic phrasing while sharing the helper bodies.
5602
5665
  *
5603
5666
  * The helpers return the reject call directly in the failure branch: since
5604
5667
  * `reject` is typed `never`, the ternary's union collapses to the success type
5605
5668
  * without relying on TypeScript's control-flow analysis of a never-returning
5606
5669
  * call (which only recognizes function declarations, not the destructured
5607
- * arrow `reject` callers receive from this factory).
5670
+ * arrow `reject` callers receive from this factory). Callers that need the
5671
+ * narrowing in an `if` must therefore assign inside the positive branch.
5608
5672
  */
5609
5673
  function makeBoundaryValidator(prefix) {
5610
5674
  const reject = (field) => {
@@ -5632,10 +5696,8 @@ function makeStrictCodec(typeSymbol, schema) {
5632
5696
  }
5633
5697
  /**
5634
5698
  * Build one strict invocation descriptor. Every Command Code Remote shares the
5635
- * `commandcode` namespace, the `commandcodeUsage` service, and a strict
5636
- * `mode: 'strict'` result — only the endpoint, method, result type symbol, and
5637
- * schema differ — so the boilerplate lives here once and each endpoint supplies
5638
- * only its own facts.
5699
+ * `commandcode` namespace, the `commandcodeUsage` service, and a strict result
5700
+ * — only the endpoint, method, result type symbol, and schema differ.
5639
5701
  */
5640
5702
  function makeRemoteDescriptor(endpoint, method, typeSymbol, schema) {
5641
5703
  return {
@@ -5659,7 +5721,6 @@ const USAGE_REPORT_ENDPOINT = "commandcode/report";
5659
5721
  * so rejection messages name the offending boundary.
5660
5722
  */
5661
5723
  const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
5662
- /** Validate one window-limit block (`fiveHour` / `weekly`). */
5663
5724
  /**
5664
5725
  * Parse one quota window, or undefined when the frame carries no such window.
5665
5726
  *
@@ -5960,12 +6021,10 @@ var CommandCodeUsageService = class extends TypertRemoteService {
5960
6021
  this.deps = deps;
5961
6022
  }
5962
6023
  /**
5963
- * Account, usage, and credit state for the settings page's account card —
5964
- * one entry per pool account when the plugin entry wired `reports`, a
5965
- * single default-account entry otherwise. Degrades per endpoint like the
5966
- * `/commandcode` command (failures land in `report.failures`); throws
5967
- * `MISSING_CREDENTIAL` when no key resolves, which the Gateway folds into
5968
- * the failure branch the page renders as a hint.
6024
+ * Account, usage, and credit state for the settings page's account card.
6025
+ * Degrades per endpoint like the `/commandcode` command (failures land in
6026
+ * `report.failures`); throws `MISSING_CREDENTIAL` when no key resolves, which
6027
+ * the Gateway folds into the failure branch the page renders as a hint.
5969
6028
  */
5970
6029
  async report() {
5971
6030
  if (this.deps.reports !== void 0) return this.deps.reports();
@@ -5980,20 +6039,19 @@ var CommandCodeUsageService = class extends TypertRemoteService {
5980
6039
  }] };
5981
6040
  }
5982
6041
  /**
5983
- * The full model catalog for the settings page's model editors (the
5984
- * routing-rule editor and the visible-models filter). The browser never
5985
- * calls the Command Code API directly — the Host serves the catalog
5986
- * (already fetched/cached by the adapter) so models can be picked from
5987
- * the live list instead of typed by hand.
6042
+ * The full model catalog for the settings page's model editors. The browser
6043
+ * never calls the Command Code API directly — the Host serves the catalog
6044
+ * (already fetched/cached by the adapter) so models can be picked from the
6045
+ * live list instead of typed by hand.
5988
6046
  */
5989
6047
  async models() {
5990
6048
  return this.deps.listModels?.() ?? { models: [] };
5991
6049
  }
5992
6050
  /**
5993
6051
  * The model price table the composer prices an in-progress session with.
5994
- * Static vendored data (the official pricing page's rates), served Host-side
5995
- * so the browser bundle never carries a copy that could drift from the
5996
- * snapshot, and so a price update reaches an open page without a rebuild.
6052
+ * Static vendored data, served Host-side so the browser bundle never carries
6053
+ * a copy that could drift from the snapshot, and so a price update reaches an
6054
+ * open page without a rebuild.
5997
6055
  */
5998
6056
  async prices() {
5999
6057
  return (this.deps.prices ?? modelPriceTable)();
@@ -6001,8 +6059,7 @@ var CommandCodeUsageService = class extends TypertRemoteService {
6001
6059
  /**
6002
6060
  * Start (or rejoin) a browser-login attempt and return its fresh status —
6003
6061
  * `waiting` carrying the Studio URL. Rejects when the flow cannot start
6004
- * (no free loopback port, disposed plugin); the Gateway folds the throw
6005
- * into the failure branch the page renders.
6062
+ * (no free loopback port, disposed plugin).
6006
6063
  */
6007
6064
  async loginBegin(targetRef) {
6008
6065
  return this.requireLogin().begin(targetRef);
@@ -6045,11 +6102,8 @@ function applyUsageRemote(ctx, deps) {
6045
6102
  //#endregion
6046
6103
  //#region src/login.ts
6047
6104
  /**
6048
- * Host half of the Command Code browser login (the loopback flow).
6049
- *
6050
- * Mirrors what the official `command-code login` CLI command performs
6051
- * (reverse-engineered from `command-code@1.32.1`, `createAuthFlowController`
6052
- * + `createAuthServer` in its bundle):
6105
+ * Host half of the Command Code browser login (the loopback flow), mirroring
6106
+ * what the official `command-code login` CLI command performs.
6053
6107
  *
6054
6108
  * 1. Bind a temporary HTTP server on `127.0.0.1`, first available port from
6055
6109
  * 5959 upward (10 attempts).
@@ -6061,12 +6115,12 @@ function applyUsageRemote(ctx, deps) {
6061
6115
  * 4. The delivered key is validated against `GET {apiBase}/alpha/whoami`
6062
6116
  * before anything is stored.
6063
6117
  *
6064
- * Server behaviour is mirrored exactly: POST-only `/callback`, a 10 KB body
6065
- * cap, JSON responses (`{success:true}` / `{success:false,error}`), CORS for
6066
- * the Studio origins only, and state-token equality as the anti-forgery
6067
- * check. One deliberate hardening over the CLI build: the CORS origin is
6068
- * echoed only when it is allowlisted (the CLI falls back to the first
6069
- * origin), which browsers treat identically.
6118
+ * The server contract is CLI-mirrored: POST-only `/callback`, a 10 KB body
6119
+ * cap, JSON responses (`{success:true}` / `{success:false,error}`), and
6120
+ * state-token equality as the anti-forgery check. Three deliberate hardenings
6121
+ * over the CLI build: the CORS origin is echoed only when allowlisted (the
6122
+ * CLI falls back to the first origin), `Connection: close`, and the denial
6123
+ * branch checks the state token before it ends the attempt.
6070
6124
  *
6071
6125
  * Storage stays out of this module: the plugin entry supplies
6072
6126
  * {@link CommandCodeLoginFlowDeps.storeKey}, which writes through the dsh
@@ -6176,16 +6230,15 @@ var CommandCodeLoginFlow = class {
6176
6230
  * asynchronously (`complete()`), and that window is open to a cancel or a
6177
6231
  * fresh `begin()`; the generation lets a late completion recognize that it
6178
6232
  * no longer owns the status face and stop instead of storing a credential
6179
- * the user cancelled and flipping the page back to success.
6233
+ * the user cancelled.
6180
6234
  */
6181
6235
  attemptSeq = 0;
6182
6236
  /**
6183
- * The start currently binding a port, if any. Every `begin()` that arrives
6184
- * before it settles joins it: two independent starts would each bind their
6185
- * own loopback server, and only the LAST one is reachable by
6186
- * {@link CommandCodeLoginFlow.teardown} — the orphan keeps listening and
6187
- * answering `/callback` for the process's lifetime, and ten of them exhaust
6188
- * the port window so browser login dies until the Host restarts.
6237
+ * The start currently binding a port, if any. Every `begin()` arriving before
6238
+ * it settles joins it: two independent starts would each bind a loopback
6239
+ * server, and only the LAST one is reachable by `teardown()` — the orphan
6240
+ * keeps answering `/callback` for the process's lifetime, and ten of them
6241
+ * exhaust the port window so browser login dies until the Host restarts.
6189
6242
  */
6190
6243
  starting;
6191
6244
  /** A live attempt's destination; different rows may not rejoin it. */
@@ -6539,20 +6592,15 @@ function corsOrigin(origin) {
6539
6592
  //#endregion
6540
6593
  //#region src/web-search.ts
6541
6594
  /**
6542
- * dsh-commandcode-provider — Command Code web search provider over `ctx.web`.
6595
+ * Command Code web-search provider over `ctx.web`.
6543
6596
  *
6544
- * The official Command Code CLI ships a built-in `web_search` tool that POSTs
6597
+ * The official CLI's built-in `web_search` tool POSTs
6545
6598
  * `{ query, numResults, allowedDomains?, blockedDomains? }` to
6546
6599
  * `{apiBase}/alpha/web-search` and reads `{ results: [{ title, url, snippet }] }`
6547
- * back. It authenticates with the SAME `Authorization: Bearer <key>` header and
6548
- * `x-command-code-version` the model adapter uses, so this provider reuses the
6549
- * plugin's existing credential chain (`COMMANDCODE_API_KEY` → credentials seam →
6550
- * `~/.commandcode/auth.json`) — no separate DeepSeek key, no extra endpoint.
6551
- *
6552
- * This mirrors the host-side `@deepseek-ai/dsh-web-search-deepseek` provider in
6553
- * shape: a cordis-free class registered into the web seam, resolving its key per
6554
- * search, mapping each server-side result to the harness's normalized
6555
- * `WebSearchSource`. The web seam owns `maxResults` truncation.
6600
+ * back, authenticating with the SAME `Authorization: Bearer <key>` and
6601
+ * `x-command-code-version` headers the model adapter uses. This provider
6602
+ * therefore reuses the plugin's existing credential chain and `apiBase`, so
6603
+ * the model-facing `web_search` tool needs no separate configuration.
6556
6604
  *
6557
6605
  * @module dsh-commandcode-provider/web-search
6558
6606
  */
@@ -6560,12 +6608,9 @@ function corsOrigin(origin) {
6560
6608
  const COMMANDCODE_SEARCH_PROVIDER_ID = "commandcode";
6561
6609
  /**
6562
6610
  * The factory-declared search provider id dsh ships by default (from
6563
- * `dsh-base`'s cordis patch `web.config.searchProvider`). Kept as a
6564
- * documented reference only: disabling this plugin's `webSearch` toggle
6565
- * restores the previously selected backend (see
6566
- * {@link applyCommandCodeSearchSelection}) — it never forces this default,
6567
- * because forcing it is what used to silence sibling search plugins such as
6568
- * modsearch even with Command Code search turned off (issue #26).
6611
+ * `dsh-base`'s cordis patch `web.config.searchProvider`). A documented
6612
+ * reference only: disabling this plugin's `webSearch` toggle restores the
6613
+ * PREVIOUSLY selected backend, never this default (issue #26).
6569
6614
  */
6570
6615
  const DEFAULT_WEB_SEARCH_PROVIDER_ID = "deepseek-official";
6571
6616
  /** Fresh selection state: the plugin starts out not owning the selection. */
@@ -6581,24 +6626,15 @@ function commandCodeSearchSelection() {
6581
6626
  * providers (issue #26).
6582
6627
  *
6583
6628
  * - Enabling writes `commandcode` and remembers whatever it displaced. When
6584
- * the plugin already owns the selection (e.g. a settings save while still
6585
- * on), the original `displaced` value is kept — the field currently holds
6586
- * our own id, which must never be mistaken for the user's backend.
6587
- * - Disabling hands the selection back to the remembered backend. When the
6588
- * state holds no memory (a fresh boot straight into `webSearch: false`),
6589
- * the field is left alone: the runtime's current value — a sibling's
6590
- * cordis pin such as `searchProvider: modsearch`, or unset for
6591
- * auto-select — already says what the user wants.
6592
- * - When the field already reads `commandcode` at first touch (e.g. a
6593
- * surviving runtime the plugin did not set, or a manual
6594
- * `searchProvider: commandcode` pin), `displaced` stays undefined and
6595
- * `preexisting` is set, so the later disable is a no-op rather than a guess
6596
- * at the factory default. A "no-op" means the field is left ALONE: writing
6597
- * that `undefined` back would destroy the user's own pin, and dsh-web reads
6598
- * a cleared `searchProviderId` as auto-select — where a second usable
6599
- * provider (the shipped `deepseek-official` is usable whenever a DeepSeek
6600
- * key resolves) makes EVERY later search throw
6601
- * `WEB_PROVIDER_AMBIGUOUS`.
6629
+ * the plugin already owns the selection, the original `displaced` is kept —
6630
+ * the field currently holds our own id, which must never be mistaken for the
6631
+ * user's backend.
6632
+ * - Disabling hands the selection back to the remembered backend only when
6633
+ * this plugin actually took it over. A fresh boot straight into
6634
+ * `webSearch: false`, or a `preexisting` field, leaves it ALONE: writing the
6635
+ * empty memory back would clear the user's own `searchProvider: commandcode`
6636
+ * pin and hand the selection to dsh-web's auto-select, where a second usable
6637
+ * provider makes every later search throw `WEB_PROVIDER_AMBIGUOUS`.
6602
6638
  *
6603
6639
  * Never throws: like the low-level rewrite, a hardened runtime shape degrades
6604
6640
  * to registered-but-unselected.
@@ -6652,7 +6688,7 @@ function toSource(result) {
6652
6688
  ...snippet !== void 0 && snippet.length > 0 ? { snippet } : {}
6653
6689
  };
6654
6690
  }
6655
- function isAbortError$1(error) {
6691
+ function isAbortError(error) {
6656
6692
  return error instanceof DOMException && error.name === "AbortError";
6657
6693
  }
6658
6694
  /** Build the provider's stable cancellation error while retaining the caller's reason. */
@@ -6663,11 +6699,11 @@ function throwIfAborted(signal) {
6663
6699
  if (signal?.aborted === true) throw searchAborted(signal, void 0);
6664
6700
  }
6665
6701
  /**
6666
- * A `ctx.web` search provider backed by the Command Code Provider API. Reuses
6667
- * the plugin's credential chain and `apiBase`, so search "just works" with the
6668
- * existing key — the model-facing `web_search` tool needs no separate
6669
- * configuration. Selection between multiple search providers is the web seam's
6670
- * job (pin `searchProvider: commandcode` if ambiguous).
6702
+ * A `ctx.web` search provider backed by the Command Code Provider API, reusing
6703
+ * the plugin's credential chain and `apiBase` so the model-facing `web_search`
6704
+ * tool needs no separate configuration. Selection between multiple search
6705
+ * providers is the web seam's job (pin `searchProvider: commandcode` if
6706
+ * ambiguous).
6671
6707
  */
6672
6708
  var CommandCodeSearchProvider = class {
6673
6709
  deps;
@@ -6707,7 +6743,7 @@ var CommandCodeSearchProvider = class {
6707
6743
  ...signal !== void 0 ? { signal } : {}
6708
6744
  });
6709
6745
  } catch (error) {
6710
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6746
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6711
6747
  throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
6712
6748
  }
6713
6749
  if (!response.ok) {
@@ -6722,7 +6758,7 @@ var CommandCodeSearchProvider = class {
6722
6758
  if (typeof code === "string" || typeof inner === "string") message += `: ${typeof code === "string" ? code : ""}${typeof code === "string" && typeof inner === "string" ? " — " : ""}${typeof inner === "string" ? inner : ""}`;
6723
6759
  }
6724
6760
  } catch (error) {
6725
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6761
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6726
6762
  const slice = (await response.text().catch(() => "")).trim().slice(0, 200);
6727
6763
  if (slice !== "") message += `: ${slice}`;
6728
6764
  }
@@ -6732,7 +6768,7 @@ var CommandCodeSearchProvider = class {
6732
6768
  try {
6733
6769
  payload = await response.json();
6734
6770
  } catch (error) {
6735
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6771
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6736
6772
  throw new WebError("Command Code web search returned an unparseable response body", "WEB_PROVIDER_ERROR", { cause: error });
6737
6773
  }
6738
6774
  const results = payload?.results;
@@ -6756,7 +6792,7 @@ var CommandCodeSearchProvider = class {
6756
6792
  try {
6757
6793
  key = await this.deps.resolveKey();
6758
6794
  } catch (error) {
6759
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6795
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6760
6796
  if (error instanceof Error && typeof error.code === "string") {
6761
6797
  const code = error.code;
6762
6798
  if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
@@ -6797,11 +6833,10 @@ const OTHER_GROUP_ID = "models-other";
6797
6833
  * free before paid inside a tier, then by id.
6798
6834
  *
6799
6835
  * The list is the static capability snapshot rather than a live catalog read
6800
- * on purpose — a settings page must draw synchronously, and the snapshot is
6801
- * synced from the same upstream table the picker's tier headings come from.
6802
- * A model added upstream after this build still reaches the user: an empty
6803
- * allowlist shows everything, and a model named in the allowlist but absent
6804
- * here is rendered by the "Other" group instead of disappearing.
6836
+ * on purpose — a settings page must draw synchronously. A model added
6837
+ * upstream after this build still reaches the user: an empty allowlist shows
6838
+ * everything, and a model named in the allowlist but absent here is rendered
6839
+ * by the "Other" group instead of disappearing.
6805
6840
  */
6806
6841
  function commandCodeTuiModelChoices() {
6807
6842
  return Object.keys(KNOWN_PLANS).map((id) => ({
@@ -6829,14 +6864,13 @@ function storedIds(value) {
6829
6864
  * Build the section descriptor. Pure, so tests can pin the exact fields
6830
6865
  * without a dsh-TUI host.
6831
6866
  *
6832
- * Field choices worth keeping: the two option-bearing fields (`activeAccount`,
6833
- * `lang`) are `text` + `options` rather than `select`, because a `select`
6834
- * cannot express "unset" — cycling only ever lands on a declared option, so a
6835
- * `select` would strand the user on a pinned value with no way back to
6836
- * automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
6837
- * unset state reachable. `filterModelsByPlan` formats its EFFECTIVE default
6838
- * (unset means true at the adapter), so a fresh install reads true instead of
6839
- * the screen's "(empty)".
6867
+ * The two option-bearing fields (`activeAccount`, `lang`) are `text` +
6868
+ * `options` rather than `select`, because a `select` cannot express "unset" —
6869
+ * cycling only ever lands on a declared option, so it would strand the user on
6870
+ * a pinned value with no way back to automatic. The `auto` sentinel plus a
6871
+ * `parse` that clears the path keeps unset reachable. `filterModelsByPlan`
6872
+ * formats its EFFECTIVE default (unset means true at the adapter) so a fresh
6873
+ * install reads true instead of the screen's "(empty)".
6840
6874
  */
6841
6875
  function buildCommandCodeTuiSection(deps) {
6842
6876
  const ref = deps.apiKeyRef();
@@ -6866,12 +6900,9 @@ function buildCommandCodeTuiSection(deps) {
6866
6900
  * ONE draft: every one of them then parses that same draft on save, all N
6867
6901
  * write ops address the same path, and only the LAST field's op survives —
6868
6902
  * which silently rewrote the allowlist from the last catalog model instead
6869
- * of the one that was toggled. A per-model key is what makes a checkbox
6870
- * express one model's state.
6871
- *
6872
- * An override equal to what the array already says is written as a CLEAR, so
6873
- * toggling a model back to its inherited state leaves no residue and the
6874
- * document stays minimal.
6903
+ * of the one that was toggled. An override equal to what the array already
6904
+ * says is written as a CLEAR, so toggling a model back to its inherited
6905
+ * state leaves no residue.
6875
6906
  */
6876
6907
  const modelField = (id, hint, group) => ({
6877
6908
  path: ["modelVisibility", id],
@@ -6944,54 +6975,6 @@ function buildCommandCodeTuiSection(deps) {
6944
6975
  };
6945
6976
  }
6946
6977
  },
6947
- {
6948
- path: ["commandGuard"],
6949
- group: "advanced",
6950
- kind: "boolean",
6951
- label: "AI command guard",
6952
- descriptions: { zh: "AI 命令安全预判" },
6953
- hint: "typesafe/jev judges whether a shell command is safe; sandbox escalations also need separate scope and necessity verdicts. All must be confident enough; everything else asks as usual. Off by default.",
6954
- hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全;沙箱提权还要分别判断范围与必要性,三项都足够确定才自动放行,其余照常弹窗。默认关闭。" },
6955
- format: (value) => value === true ? "true" : "false",
6956
- parse: (text) => ({
6957
- kind: "set",
6958
- value: text.trim() === "true"
6959
- })
6960
- },
6961
- {
6962
- path: ["commandGuardLevel"],
6963
- group: "advanced",
6964
- kind: "select",
6965
- label: "Auto-approve threshold",
6966
- descriptions: { zh: "自动放行阈值" },
6967
- hint: "How confident typesafe/jev must be before a command runs without asking. Default: medium.",
6968
- hintDescriptions: { zh: "typesafe/jev 需要多确定才免弹窗放行;默认「中」。" },
6969
- options: [
6970
- {
6971
- value: "high",
6972
- label: "High (0.95, approves least)",
6973
- descriptions: { zh: "高(0.95,放行最少)" }
6974
- },
6975
- {
6976
- value: "medium",
6977
- label: "Medium (0.9)",
6978
- descriptions: { zh: "中(0.9)" }
6979
- },
6980
- {
6981
- value: "low",
6982
- label: "Low (0.8, approves most)",
6983
- descriptions: { zh: "低(0.8,放行最多)" }
6984
- }
6985
- ],
6986
- format: (value) => value === "high" || value === "low" ? value : "medium",
6987
- parse: (text) => {
6988
- const trimmed = text.trim();
6989
- return trimmed === "high" || trimmed === "medium" || trimmed === "low" ? {
6990
- kind: "set",
6991
- value: trimmed
6992
- } : void 0;
6993
- }
6994
- },
6995
6978
  {
6996
6979
  path: ["zdr"],
6997
6980
  group: "advanced",
@@ -7126,12 +7109,10 @@ function sectionSignature(section) {
7126
7109
  /**
7127
7110
  * Register the Command Code section on a dsh-TUI host.
7128
7111
  *
7129
- * @param ctx - the context of an activated `tuiSettingsSections` injection.
7130
- * @param deps - plugin-owned facts the section reads.
7131
7112
  * @returns a refresh function that re-registers the section when a fact it
7132
- * renders changed (the plugin entry calls it from its settings `onChange`
7133
- * hook), or `undefined` when the seam is unusable. The returned function is
7134
- * inert after the fiber is torn down.
7113
+ * renders changed — the plugin entry calls it from its
7114
+ * `loader/volatile-update` listener — or `undefined` when the seam is
7115
+ * unusable. The returned function is inert after the fiber is torn down.
7135
7116
  */
7136
7117
  function applyCommandCodeTuiSettings(ctx, deps) {
7137
7118
  const service = tuiSettingsService(ctx);
@@ -7167,45 +7148,26 @@ function applyCommandCodeTuiSettings(ctx, deps) {
7167
7148
  /**
7168
7149
  * A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
7169
7150
  *
7170
- * The route's retry policy (`providerRetryPolicy` in `./adapter.ts`) is
7171
- * deliberately near-unbounded: 1000 attempts with waits doubling to a 15-minute
7172
- * cap. That shape exists for the failures a provider ASKS to have retried — an
7173
- * exhausted 5-hour window, a Cloudflare 520 — where the wait is the point.
7174
- *
7175
- * A transport failure is not that. The reporter's second event at ~710k
7176
- * context was `Connect Timeout Error ... timeout: 10000ms` (undici's own
7177
- * connect timeout; this plugin's `requestTimeoutMs` is 60s) and, on another
7178
- * attempt, `read ECONNRESET` against `/alpha/generate`. Because `TRANSPORT` is
7179
- * in the whitelist, each of those entered the same 1000-attempt loop: with
7180
- * `initialDelayMs: 500` doubling, the wait before attempt 11 alone is 512 s and
7181
- * the cumulative wait before it is ~500 s — the "模型请求重试已取消(11/1000)·
7182
- * 482s" the reporter photographed is that 512 s countdown, 30 s into it — and
7183
- * those waits are counted BEFORE each retry attempt, not after it.
7184
- *
7185
- * A connection that cannot be established is either a sub-second blip (the
7186
- * first few attempts recover it, invisibly) or an outage that no amount of
7187
- * waiting inside one request will fix. So transport failures get their own
7188
- * budget, and when it is exhausted the failure is surfaced with the diagnosis
7189
- * instead of manufacturing a multi-minute stall. Waiting out a real outage is
7190
- * the user's call — the next send starts a fresh budget — and
7191
- * `dsh-llm-retry`'s window for rate limits is untouched.
7192
- *
7193
- * Deliberately NOT counted here: the pre-stream recovery the ADAPTER performs
7194
- * inside one `stream()` call (account rotation on 429/401, the 413 image-budget
7195
- * step-down, the Provider-API → CLI protocol switch). Those are invisible to
7196
- * the caller and bounded on their own; this budget caps only the harness's
7197
- * retry loop.
7198
- *
7199
- * Reset rule: the budget clears at each new STEP — one step is exactly one
7200
- * model request (`step/start` is appended immediately before the call) — so the
7201
- * cap is per logical request and the next step of the same turn gets its own
7202
- * grace. `agent/status` → `idle` is NOT the reset signal even though it looks
7203
- * like the natural one: the agent loop's `setPhase` emits it only on a status
7204
- * CHANGE, and a turn's steps all run inside one `running` phase, so it never
7205
- * fires between them — a budget reset only there would be per-turn, and an
7206
- * outage would fail every later step of that turn immediately instead of
7207
- * retrying each. `assistant/attempt` is wrong for the opposite reason: the loop
7208
- * appends it for the FAILED attempt itself, *before* dispatching
7151
+ * The route policy (`providerRetryPolicy` in `./adapter.ts`) is near-unbounded
7152
+ * on purpose — 1000 attempts doubling to a 15-minute cap — because it exists
7153
+ * for failures the provider ASKS to have retried, where the wait is the point.
7154
+ * A connection that cannot be established is not that: either a blip the first
7155
+ * few attempts absorb, or an outage no in-request waiting fixes (and its waits
7156
+ * count BEFORE each attempt, so attempt 11 alone already waits 512 s). So
7157
+ * transport failures get their own budget; waiting out a real outage is the
7158
+ * user's call, and the next send starts a fresh budget.
7159
+ *
7160
+ * Not counted here: the pre-stream recovery the ADAPTER performs inside one
7161
+ * `stream()` call (account rotation, the 413 image-budget step-down, the
7162
+ * Provider-API → CLI switch) — invisible to the caller and bounded on its own.
7163
+ * This budget caps only the harness's retry loop.
7164
+ *
7165
+ * Reset rule: the budget clears at each new STEP (one step = one model request).
7166
+ * `agent/status` → `idle` is NOT it, though it looks natural: the loop's
7167
+ * `setPhase` emits it only on a status CHANGE, and a turn's steps all run inside
7168
+ * one `running` phase, so a reset there would fail every later step of an outage
7169
+ * turn at once instead of retrying each. `assistant/attempt` is wrong the other
7170
+ * way — the loop appends it for the FAILED attempt itself, before dispatching
7209
7171
  * `agent/request-error`, so resetting on it would clear the budget on every
7210
7172
  * failure and restore the very loop this module exists to stop.
7211
7173
  *
@@ -7239,15 +7201,13 @@ function resetTransportFailures(agent) {
7239
7201
  /**
7240
7202
  * What one session event means for the transport budget.
7241
7203
  *
7242
- * Exported and pure on purpose: the choice of event is the part of this design
7243
- * that is easy to get plausibly wrong (`agent/status` → `idle` looks like the
7244
- * natural reset and is not one — see the module comment), so it is stated once,
7245
- * here, where a test can pin it, instead of being implied by a listener body.
7204
+ * Pure and exported on purpose: the choice of event is the part of this design
7205
+ * that is easy to get plausibly wrong (see the module comment), so it is stated
7206
+ * once, where a test can pin it.
7246
7207
  *
7247
7208
  * @param type - the appended session event's type.
7248
- * @returns `'reset'` for a new step (one model request), `'forget'` when the
7249
- * turn ended and the session → agent mapping is no longer needed, otherwise
7250
- * `'none'`.
7209
+ * @returns `'reset'` for a new step, `'forget'` when the turn ended (the
7210
+ * session → agent mapping is no longer needed), otherwise `'none'`.
7251
7211
  */
7252
7212
  function transportResetAction(type) {
7253
7213
  if (type === "step/start") return "reset";
@@ -7257,880 +7217,44 @@ function transportResetAction(type) {
7257
7217
  /**
7258
7218
  * The diagnosis a capped turn ends with. Bilingual, because the harness renders
7259
7219
  * a failed turn's message verbatim and this plugin's other user-facing failures
7260
- * are bilingual too (see the adapter's error builders). It names the retry
7261
- * budget and the checks that actually help, instead of the bare "fetch failed"
7262
- * chain the retry chrome would otherwise show.
7220
+ * are too; it names the retry budget and the checks that actually help instead
7221
+ * of the bare "fetch failed" chain the retry chrome would otherwise show.
7263
7222
  */
7264
7223
  function transportBudgetMessage(failureMessage, maxRetries) {
7265
7224
  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`;
7266
7225
  }
7267
7226
  //#endregion
7268
- //#region src/systemone.ts
7269
- /**
7270
- * Host-side client for Command Code's System One decision endpoint
7271
- * (`POST {apiBase}/provider/v1/systemone`).
7272
- *
7273
- * `typesafe/jev` is **not a chat model**: it takes a `state` plus typed
7274
- * questions and answers with probabilities, never text, and it never streams.
7275
- * It is therefore absent from `/provider/v1/models` (so the picker can never
7276
- * offer it) and must never be registered as a model route — this plugin uses it
7277
- * as an internal advisor instead: the command guard in `./command-guard.ts`
7278
- * asks it whether a shell command is safe to auto-approve.
7279
- *
7280
- * The three question types are the whole vocabulary (command-code@1.65.0
7281
- * `systemOneRequestSchema`, unchanged since 1.64.0):
7282
- *
7283
- * - `noul` — "is this true?", optional `criteria: { true, false }` describing
7284
- * each verdict. Answers with `noul`, the probability of *yes*.
7285
- * - `choice` — "which one?", `criteria` maps each option key to its
7286
- * description. Answers with `choice`, plus optional `confidence` and
7287
- * `probabilities`.
7288
- * - `score` — "where on this rubric?", `criteria` is an array of at least two
7289
- * levels, lowest first. Answers with `score`, `legend` and `probabilities`.
7290
- *
7291
- * A request carries at most 20 questions and every answer is keyed by the
7292
- * question id it belongs to. `usage` is reported per request, in tokens.
7293
- *
7294
- * Billing: free through 2026-09-24T23:59:59Z, then $0.042 per 1M input tokens
7295
- * with output free. The model has **no ZDR-capable upstream**, so a request
7296
- * carrying `x-cmd-zdr: 1` is refused with HTTP 422 `cmd_zdr_no_providers` —
7297
- * this client never sends that header, and it must stay that way even though
7298
- * the plugin now ships a ZDR switch for chat (`Config.zdr` → the `x-cmd-zdr: 1`
7299
- * header per model, gated on `KNOWN_NON_ZDR_MODELS`): the decision endpoint is
7300
- * reached through THIS module's own request path, never through the adapter's
7301
- * chat connection, so the two cannot drift — and a deployment that later widens
7302
- * ZDR must exclude this endpoint rather than route it through the switch.
7303
- *
7304
- * Auth is the plugin's ordinary chain (the multi-account pool, the credentials
7305
- * seam, the CLI auth file) through the same `Authorization: Bearer <key>` and
7306
- * `x-command-code-version` headers every other endpoint uses.
7307
- *
7308
- * @module dsh-commandcode-provider/systemone
7309
- */
7310
- /** Path of the decision endpoint, relative to `apiBase`. */
7311
- const SYSTEMONE_ROUTE = "/provider/v1/systemone";
7312
- /** The one error type this client throws. Callers fail closed on it. */
7313
- var SystemOneError = class extends Error {
7314
- kind;
7315
- status;
7316
- constructor(message, kind, status, options) {
7317
- super(message, options);
7318
- this.kind = kind;
7319
- this.status = status;
7320
- this.name = "SystemOneError";
7321
- }
7322
- };
7323
- /**
7324
- * Combine the caller's signal with a timeout, keeping the two distinguishable:
7325
- * a timeout is this client's own budget expiring (the guard delegates to the
7326
- * user) while a caller abort means the approval was already withdrawn (answer
7327
- * nothing) — both fail closed, but the diagnosis must not lie about which.
7328
- *
7329
- * The timer is a plain `setTimeout` rather than `AbortSignal.timeout()`, whose
7330
- * timer Node unrefs: with nothing else pending (a test double, a socket that
7331
- * never opened) an unref'd timer lets the process exit while the promise is
7332
- * still pending, which reads as a hang rather than as the timeout it is.
7333
- */
7334
- function deadlineOf(signal, timeoutMs) {
7335
- const controller = new AbortController();
7336
- let timedOut = false;
7337
- const timer = setTimeout(() => {
7338
- timedOut = true;
7339
- controller.abort();
7340
- }, timeoutMs);
7341
- const onCallerAbort = () => controller.abort();
7342
- if (signal !== void 0) {
7343
- if (signal.aborted) controller.abort();
7344
- else signal.addEventListener("abort", onCallerAbort, { once: true });
7345
- }
7346
- return {
7347
- signal: controller.signal,
7348
- callerAborted: () => signal?.aborted === true,
7349
- timedOut: () => timedOut,
7350
- dispose: () => {
7351
- clearTimeout(timer);
7352
- signal?.removeEventListener("abort", onCallerAbort);
7353
- }
7354
- };
7355
- }
7356
- /** Whether a caught value is an abort-shaped failure. */
7357
- function isAbortError(error) {
7358
- return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
7359
- }
7360
- /** Stop waiting for an operation that does not accept an AbortSignal. */
7361
- function withinDeadline(operation, deadline, timeoutMs) {
7362
- return new Promise((resolve, reject) => {
7363
- const onAbort = () => {
7364
- reject(deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout"));
7365
- };
7366
- deadline.signal.addEventListener("abort", onAbort, { once: true });
7367
- operation.then((value) => {
7368
- deadline.signal.removeEventListener("abort", onAbort);
7369
- resolve(value);
7370
- }, (error) => {
7371
- deadline.signal.removeEventListener("abort", onAbort);
7372
- reject(error);
7373
- });
7374
- if (deadline.signal.aborted) onAbort();
7375
- });
7376
- }
7377
- /** A finite number in `[0, 1]`, or undefined. */
7378
- function probability(value) {
7379
- return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
7380
- }
7381
- /** A finite number, or undefined. */
7382
- function finite(value) {
7383
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7384
- }
7385
- /** A record of finite numbers (probabilities), or undefined when the shape is off. */
7386
- function numberRecord(value) {
7387
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7388
- const out = {};
7389
- for (const [key, entry] of Object.entries(value)) {
7390
- const parsed = finite(entry);
7391
- if (parsed === void 0) return void 0;
7392
- out[key] = parsed;
7393
- }
7394
- return out;
7395
- }
7396
- /**
7397
- * Parse one answer entry strictly by its own `type`. A body whose answer does
7398
- * not carry the member its type promises is rejected rather than defaulted:
7399
- * an absent probability is exactly the case the caller must not read as "safe".
7400
- *
7401
- * @param value - one raw entry from the response's `answers` object.
7402
- * @returns the parsed answer, or undefined when the entry is malformed.
7403
- */
7404
- function parseSystemOneAnswer(value) {
7405
- if (typeof value !== "object" || value === null) return void 0;
7406
- const raw = value;
7407
- if (raw.type === "noul") {
7408
- const noul = probability(raw.noul);
7409
- return noul === void 0 ? void 0 : {
7410
- type: "noul",
7411
- noul
7412
- };
7413
- }
7414
- if (raw.type === "choice") {
7415
- if (typeof raw.choice !== "string" || raw.choice === "") return void 0;
7416
- const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
7417
- if (raw.confidence !== void 0 && confidence === void 0) return void 0;
7418
- const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
7419
- if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
7420
- return {
7421
- type: "choice",
7422
- choice: raw.choice,
7423
- ...confidence === void 0 ? {} : { confidence },
7424
- ...probabilities === void 0 ? {} : { probabilities }
7425
- };
7426
- }
7427
- if (raw.type === "score") {
7428
- const score = finite(raw.score);
7429
- if (score === void 0) return void 0;
7430
- const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
7431
- if (raw.confidence !== void 0 && confidence === void 0) return void 0;
7432
- const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
7433
- if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
7434
- return {
7435
- type: "score",
7436
- score,
7437
- ...confidence === void 0 ? {} : { confidence },
7438
- ...probabilities === void 0 ? {} : { probabilities }
7439
- };
7440
- }
7441
- }
7442
- /**
7443
- * Parse a decision response body. Every answer entry must be well formed — one
7444
- * malformed entry fails the whole response, because a partially understood
7445
- * decision is not a decision. Unknown extra members are ignored so a server-side
7446
- * addition cannot break the guard.
7447
- *
7448
- * @param value - the decoded JSON body.
7449
- * @returns the parsed response, or undefined when the body is not one.
7450
- */
7451
- function parseSystemOneResponse(value) {
7452
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7453
- const raw = value;
7454
- const rawAnswers = raw.answers;
7455
- if (typeof rawAnswers !== "object" || rawAnswers === null || Array.isArray(rawAnswers)) return void 0;
7456
- const entries = Object.entries(rawAnswers);
7457
- if (entries.length === 0) return void 0;
7458
- const answers = {};
7459
- for (const [id, entry] of entries) {
7460
- const answer = parseSystemOneAnswer(entry);
7461
- if (answer === void 0) return void 0;
7462
- answers[id] = answer;
7463
- }
7464
- const usage = raw.usage === void 0 ? void 0 : parseUsage(raw.usage);
7465
- if (raw.usage !== void 0 && usage === void 0) return void 0;
7466
- return {
7467
- ...typeof raw.model === "string" ? { model: raw.model } : {},
7468
- answers,
7469
- ...usage === void 0 ? {} : { usage }
7470
- };
7471
- }
7472
- /** Parse the optional `usage` member; an unreadable one is a malformed body. */
7473
- function parseUsage(value) {
7474
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7475
- const raw = value;
7476
- const inputTokens = raw.input_tokens === void 0 ? void 0 : finite(raw.input_tokens);
7477
- if (raw.input_tokens !== void 0 && inputTokens === void 0) return void 0;
7478
- const outputTokens = raw.output_tokens === void 0 ? void 0 : finite(raw.output_tokens);
7479
- if (raw.output_tokens !== void 0 && outputTokens === void 0) return void 0;
7480
- return {
7481
- ...inputTokens === void 0 ? {} : { inputTokens },
7482
- ...outputTokens === void 0 ? {} : { outputTokens }
7483
- };
7484
- }
7485
- /** Validate the outgoing request where the server would only reject it with a 400. */
7486
- function assertRequest(request) {
7487
- const ids = Object.keys(request.questions);
7488
- if (ids.length === 0) throw new SystemOneError("a decision request needs at least one question", "invalid-request");
7489
- if (ids.length > 20) throw new SystemOneError(`a decision request carries at most 20 questions (got ${ids.length})`, "invalid-request");
7490
- for (const id of ids) if (id.trim() === "") throw new SystemOneError("every decision question needs a non-empty id", "invalid-request");
7491
- }
7492
- /** Read the endpoint's error body into a one-line diagnosis. */
7493
- async function httpFailure(response) {
7494
- let detail = "";
7495
- try {
7496
- const slice = (await response.text()).trim().slice(0, 200);
7497
- if (slice !== "") detail = `: ${slice}`;
7498
- } catch {}
7499
- return new SystemOneError(`Command Code decision request failed (HTTP ${response.status})${detail}`, "http", response.status);
7500
- }
7501
- /**
7502
- * Run one decision request.
7503
- *
7504
- * @param deps - connection facts, credential chain and fetch seam.
7505
- * @param request - the state and typed questions.
7506
- * @param options - caller cancellation and the per-call budget.
7507
- * @returns the parsed decision.
7508
- * @throws {SystemOneError} on every failure path; callers fail closed.
7509
- */
7510
- async function runSystemOne(deps, request, options = {}) {
7511
- assertRequest(request);
7512
- const apiBase = deps.apiBase();
7513
- if (!URL.canParse(apiBase)) throw new SystemOneError(`Command Code decision request is misconfigured: apiBase ${JSON.stringify(apiBase)} is not a valid URL`, "invalid-request");
7514
- if (options.signal?.aborted === true) throw new SystemOneError("decision request aborted", "aborted");
7515
- const timeoutMs = options.timeoutMs ?? 1500;
7516
- const deadline = deadlineOf(options.signal, timeoutMs);
7517
- try {
7518
- let key;
7519
- try {
7520
- key = await withinDeadline(Promise.resolve().then(() => deps.resolveApiKey()), deadline, timeoutMs);
7521
- } catch (error) {
7522
- if (error instanceof SystemOneError && (error.kind === "aborted" || error.kind === "timeout")) throw error;
7523
- throw new SystemOneError(`Command Code decision request has no usable credential: ${error instanceof Error ? error.message : String(error)}`, "credential", void 0, { cause: error });
7524
- }
7525
- const body = {
7526
- model: request.model ?? "typesafe/jev",
7527
- state: request.state,
7528
- questions: request.questions
7529
- };
7530
- if (deadline.signal.aborted) throw deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout");
7531
- let response;
7532
- try {
7533
- response = await (deps.fetchImpl ?? fetch)(`${apiBase.replace(/\/$/, "")}${SYSTEMONE_ROUTE}`, {
7534
- method: "POST",
7535
- headers: {
7536
- "Content-Type": "application/json",
7537
- ...IDENTITY_ENCODING_HEADER,
7538
- Authorization: `Bearer ${key}`,
7539
- "x-command-code-version": COMMAND_CODE_CLI_VERSION,
7540
- ...attributionHeaders()
7541
- },
7542
- body: JSON.stringify(body),
7543
- signal: deadline.signal
7544
- });
7545
- } catch (error) {
7546
- if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
7547
- if (deadline.timedOut() || isAbortError(error)) throw new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout", void 0, { cause: error });
7548
- throw new SystemOneError(`Command Code decision request failed: ${error instanceof Error ? error.message : String(error)}`, "network", void 0, { cause: error });
7549
- }
7550
- if (!response.ok) throw await httpFailure(response);
7551
- let payload;
7552
- try {
7553
- payload = await response.json();
7554
- } catch (error) {
7555
- if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
7556
- throw new SystemOneError("Command Code returned an unparseable decision body", "invalid-response", void 0, { cause: error });
7557
- }
7558
- const parsed = parseSystemOneResponse(payload);
7559
- if (parsed === void 0) throw new SystemOneError("Command Code returned a decision body this plugin cannot read", "invalid-response");
7560
- return parsed;
7561
- } finally {
7562
- deadline.dispose();
7563
- }
7564
- }
7565
- //#endregion
7566
- //#region src/command-guard.ts
7567
- /** Tool names whose `arguments.command` this guard judges. */
7568
- const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
7569
- /** Question id the primary safety verdict is keyed by. */
7570
- const COMMAND_GUARD_QUESTION_ID = "safe";
7571
- /** Question id for the narrow-purpose part of a sandbox escalation. */
7572
- const COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID = "escalation_scope";
7573
- /** Question id for the necessity/proportionality part of a sandbox escalation. */
7574
- const COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID = "escalation_necessity";
7575
- /** The selectable levels, strictest first (the order the settings surfaces list them in). */
7576
- const COMMAND_GUARD_LEVELS = [
7577
- "high",
7578
- "medium",
7579
- "low"
7580
- ];
7581
- /** Each level's probability threshold. */
7582
- const COMMAND_GUARD_LEVEL_THRESHOLDS = {
7583
- high: .95,
7584
- medium: .9,
7585
- low: .8
7586
- };
7587
- /** The level an unset config reads as. */
7588
- const COMMAND_GUARD_DEFAULT_LEVEL = "medium";
7589
- /** Probability of "safe" at or above which the guard grants `allowed-once` by default. */
7590
- const COMMAND_GUARD_DEFAULT_THRESHOLD = COMMAND_GUARD_LEVEL_THRESHOLDS[COMMAND_GUARD_DEFAULT_LEVEL];
7591
- /** The threshold for a stored level; anything unrecognized reads as the default level. */
7592
- function commandGuardThreshold(level) {
7593
- return typeof level === "string" && Object.hasOwn(COMMAND_GUARD_LEVEL_THRESHOLDS, level) ? COMMAND_GUARD_LEVEL_THRESHOLDS[level] : COMMAND_GUARD_DEFAULT_THRESHOLD;
7594
- }
7595
- /**
7596
- * Decision budget; the approval prompt must not wait longer than this. Fixed
7597
- * rather than configurable: it only bounds how long a late verdict can delay
7598
- * the prompt, and every failure (a timeout included) already falls back to
7599
- * asking the user, so there is nothing for a user to tune. 3 s leaves room for
7600
- * credential resolution plus one decision round trip on a slow link.
7601
- */
7602
- const COMMAND_GUARD_TIMEOUT_MS = 3e3;
7603
- /**
7604
- * Longest command the guard will send for judgement. A longer one is delegated
7605
- * instead of truncated: a verdict on the first N characters of a command is a
7606
- * verdict on a different command.
7607
- */
7608
- const COMMAND_GUARD_MAX_COMMAND_CHARS = 6e3;
7609
- /**
7610
- * Hard denylist: commands the guard never auto-approves, and never even sends
7611
- * for judgement. Each entry is a deliberate, narrow reading of one destructive
7612
- * family — the goal is not to classify every dangerous command (that is the
7613
- * model's job) but to make sure the classics cannot be talked into a grant by a
7614
- * persuasive-looking command string.
7615
- *
7616
- * Every pattern is allowed to OVER-match: a false positive costs one ordinary
7617
- * human prompt, while a false negative would hand a destructive command to a
7618
- * model that has to guess. That asymmetry is why `sudo` is matched anywhere in
7619
- * the line rather than only in command position, and why `fdisk` is listed even
7620
- * though `fdisk -l` only lists partitions.
7621
- */
7622
- const COMMAND_GUARD_DENY_LIST = [
7623
- {
7624
- id: "rm-root",
7625
- pattern: /\brm\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*[rR][a-zA-Z]*|--recursive[^\n]*--force|--force[^\n]*--recursive)\s+(?:\/|\/\*|~|~\/\*|\$HOME(?:\/\*)?|\*|\.\/\*|\.)(?:\s|$)/
7626
- },
7627
- {
7628
- id: "rm-home",
7629
- pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(?:~|\$HOME)\//
7630
- },
7631
- {
7632
- id: "filesystem-write",
7633
- pattern: /\b(?:mkfs(?:\.\w+)?|wipefs|fdisk|parted|shred)\b/
7634
- },
7635
- {
7636
- id: "raw-device-write",
7637
- pattern: /\bdd\b[^\n]*\bof=\/dev\//
7638
- },
7639
- {
7640
- id: "device-redirect",
7641
- pattern: />{1,2}\s*\/dev\/(?:sd|disk|nvme|hd)/
7642
- },
7643
- {
7644
- id: "fork-bomb",
7645
- pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/
7646
- },
7647
- {
7648
- id: "privilege-escalation",
7649
- pattern: /\b(?:sudo|doas)\b/
7650
- },
7651
- {
7652
- id: "su",
7653
- pattern: /\bsu\s/
7654
- },
7655
- {
7656
- id: "chmod-world-write",
7657
- pattern: /\bchmod\b[^\n]*\b(?:777|a\+w|o\+w)\b/
7658
- },
7659
- {
7660
- id: "chown-root",
7661
- pattern: /\bchown\b[^\n]*\b(?:root|0):/
7662
- },
7663
- {
7664
- id: "git-push",
7665
- pattern: /\bgit\s+push\b/
7666
- },
7667
- {
7668
- id: "git-history-rewrite",
7669
- pattern: /\bgit\s+(?:filter-branch|filter-repo)\b/
7670
- },
7671
- {
7672
- id: "git-hard-reset",
7673
- pattern: /\bgit\s+reset\s+--hard\b/
7674
- },
7675
- {
7676
- id: "git-clean-force",
7677
- pattern: /\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f/
7678
- },
7679
- {
7680
- id: "git-discard-worktree",
7681
- pattern: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/
7682
- },
7683
- {
7684
- id: "git-stash-drop",
7685
- pattern: /\bgit\s+stash\s+(?:drop|clear)\b/
7686
- },
7687
- {
7688
- id: "publish",
7689
- pattern: /\b(?:npm|pnpm|yarn|bun)\s+publish\b/
7690
- },
7691
- {
7692
- id: "release-upload",
7693
- pattern: /\b(?:gh\s+release|twine\s+upload)\b/
7694
- },
7695
- {
7696
- id: "docker-push",
7697
- pattern: /\bdocker\s+push\b/
7698
- },
7699
- {
7700
- id: "cloud-upload",
7701
- pattern: /\b(?:aws\s+s3|gsutil|gcloud\s+storage|az\s+storage)\b[^\n]*\b(?:cp|sync|mv|rm|rb)\b/
7702
- },
7703
- {
7704
- id: "remote-copy",
7705
- pattern: /\b(?:scp|rsync)\b[^\n]*(?::|@)/
7706
- },
7707
- {
7708
- id: "pipe-to-shell",
7709
- pattern: /\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|dash|ksh|python\d*|node|ruby|perl)\b/
7710
- },
7711
- {
7712
- id: "power",
7713
- pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/
7714
- },
7715
- {
7716
- id: "kill-everything",
7717
- pattern: /\bkill\s+(?:-9\s+)?-1\b|\b(?:killall|pkill)\s+-9\b/
7718
- },
7719
- {
7720
- id: "credential-write",
7721
- pattern: /(?:>{1,2}|tee|cp|mv|rm|chmod|chown)\s[^\n]*(?:\.ssh\b|\.aws\b|\.gnupg\b|\.commandcode\/auth\.json|id_rsa|id_ed25519)/
7722
- },
7723
- {
7724
- id: "history-clear",
7725
- pattern: /\bhistory\s+-c\b|\bunset\s+HISTFILE\b/
7726
- },
7727
- {
7728
- id: "global-install",
7729
- pattern: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add)\s+(?:-g\b|--global\b)/
7730
- },
7731
- {
7732
- id: "system-install",
7733
- pattern: /\b(?:apt|apt-get|dnf|yum|zypper|apk|pacman|brew)\s+(?:install|remove|purge|uninstall|upgrade)\b/
7734
- },
7735
- {
7736
- id: "language-install",
7737
- pattern: /\b(?:pip3?|pipx|gem|cargo)\s+(?:install|add)\b/
7738
- }
7739
- ];
7740
- /**
7741
- * Why the guard refuses to judge a command, when it does.
7742
- *
7743
- * @param command - the raw command text.
7744
- * @returns the matching denylist id, or undefined.
7745
- */
7746
- function denyReasonOf(command) {
7747
- for (const entry of COMMAND_GUARD_DENY_LIST) if (entry.pattern.test(command)) return entry.id;
7748
- }
7749
- /** A non-empty string member, or undefined. */
7750
- function stringMember(value, key) {
7751
- if (typeof value !== "object" || value === null) return void 0;
7752
- const member = value[key];
7753
- return typeof member === "string" && member.trim() !== "" ? member : void 0;
7754
- }
7755
- /**
7756
- * Read a shell command out of a tool execution, or undefined when this call is
7757
- * not one the guard judges (a different tool, or arguments without a command).
7758
- *
7759
- * @param name - the executed tool's name.
7760
- * @param args - the execution's parsed `arguments`.
7761
- * @returns the fact to judge, without the asker's reason (that arrives later).
7762
- */
7763
- function shellCommandOf(name, args) {
7764
- if (!COMMAND_GUARD_SHELL_TOOLS.includes(name)) return void 0;
7765
- const command = stringMember(args, "command");
7766
- if (command === void 0) return void 0;
7767
- const description = stringMember(args, "description");
7768
- const workdir = stringMember(args, "workdir");
7769
- const sandboxPermissions = stringMember(args, "sandbox_permissions");
7770
- return {
7771
- tool: name,
7772
- command,
7773
- ...description === void 0 ? {} : { description },
7774
- ...workdir === void 0 ? {} : { workdir },
7775
- ...sandboxPermissions === void 0 ? {} : { sandboxPermissions }
7776
- };
7777
- }
7778
- /**
7779
- * Build the decision request for one command.
7780
- *
7781
- * A normal approval asks one question. A call that explicitly requests a wider
7782
- * sandbox gets the same safety question plus two veto questions. This keeps the
7783
- * common case cheap while making the broader permission change an explicit
7784
- * decision: JEV must agree that the extra access has a narrow purpose and is
7785
- * necessary, not merely that the command text looks familiar.
7786
- *
7787
- * The agent's own `description` and the harness's `reason` are included as
7788
- * context, but the criteria say the command text itself decides — a
7789
- * model-written description is a claim, not evidence.
7790
- *
7791
- * @param fact - the command and its context.
7792
- * @returns the request to send.
7793
- */
7794
- function buildSafetyRequest(fact) {
7795
- const escalated = fact.sandboxPermissions !== void 0;
7796
- const lines = [
7797
- `Tool: ${fact.tool}`,
7798
- "Command:",
7799
- fact.command,
7800
- ...fact.description === void 0 ? [] : ["", `The agent describes this command as: ${fact.description}`],
7801
- ...fact.workdir === void 0 ? [] : ["", `Working directory: ${fact.workdir}`],
7802
- ...fact.sandboxPermissions === void 0 ? [] : ["", `The harness is asking because this call requests a wider sandbox mode (${fact.sandboxPermissions}) than the one currently in force.`],
7803
- ...fact.reason === void 0 ? [] : ["", `The asker's reason for needing approval: ${fact.reason}`]
7804
- ];
7805
- const question = {
7806
- type: "noul",
7807
- instructions: escalated ? "Is it safe to run this command right now WITHOUT asking the user first, including the requested sandbox escalation? Judge the command text itself; a description or stated reason is only a claim about it." : "Is it safe to run this command right now WITHOUT asking the user first? Judge the command text itself; a description is only a claim about it.",
7808
- criteria: {
7809
- true: escalated ? "A normal, non-obfuscated read, build, or test operation whose outside-workspace effects are limited to conventional build/dependency caches or temporary files and are easily reversible: no unrelated user or system data is accessed, no data leaves the machine, and no credentials, git history or remote state are touched." : "Read-only, or its effects stay inside the current project and are trivially reversible: no data leaves the machine, nothing outside the project is modified, and no credentials, git history or remote state are touched.",
7810
- false: "Anything else. Broad or unrelated outside-workspace access, deleting or overwriting files outside the project, rewriting or publishing git history, uploading data anywhere, installing system or global packages, changing permissions, ownership or credentials, killing processes, running as another user or as root, piping a download into a shell, or a command obfuscated enough that its effect cannot be read."
7811
- }
7812
- };
7813
- const questions = { [COMMAND_GUARD_QUESTION_ID]: question };
7814
- if (escalated) {
7815
- questions[COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID] = {
7816
- type: "noul",
7817
- instructions: "Is the requested wider sandbox access limited to a narrow, conventional purpose supported by this command, such as a build cache, dependency cache, or temporary directory, rather than unrelated user files, credentials, other projects, or the network?",
7818
- criteria: {
7819
- true: "The wider access is plausibly limited to conventional local build/dependency caches or temporary files required by the command, with no unrelated host data or remote transfer in scope.",
7820
- false: "The command needs broad or unrelated host access, cannot establish a narrow purpose, or would use the wider permission to reach credentials, other projects, user files, or the network."
7821
- }
7822
- };
7823
- questions[COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID] = {
7824
- type: "noul",
7825
- instructions: "Is the requested sandbox escalation necessary and proportionate for this one command, rather than broader than the command needs? Treat the stated reason as a claim to check against the command, not as proof.",
7826
- criteria: {
7827
- true: "The command and its stated purpose support a one-shot escalation that is needed for the task and is no broader than the requested effect requires.",
7828
- false: "The wider mode is unnecessary, broader than needed, unsupported by the command, or the command is too ambiguous to justify the extra authority."
7829
- }
7830
- };
7831
- }
7832
- return {
7833
- state: lines.join("\n"),
7834
- questions
7835
- };
7836
- }
7837
- /**
7838
- * Read a probability out of a decision response.
7839
- *
7840
- * @param response - the parsed response.
7841
- * @param questionId - the question id to read (defaults to the guard's own).
7842
- * @returns the probability, or undefined when the answer is missing or is not
7843
- * a `noul` verdict.
7844
- */
7845
- function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
7846
- const answer = response.answers[questionId];
7847
- return answer !== void 0 && answer.type === "noul" ? answer.noul : void 0;
7848
- }
7849
- /**
7850
- * One-line, length-bounded excerpt of a command for the log. The full text is
7851
- * already in the session's tool call and was already sent to the decision
7852
- * endpoint, so this adds no exposure — it is what makes "why did this run
7853
- * without asking?" answerable from the log alone.
7854
- */
7855
- function commandExcerpt(command, limit = 160) {
7856
- const flat = command.replace(/\s+/g, " ").trim();
7857
- return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
7858
- }
7859
- /**
7860
- * The guard's stateful half: what was executed (by call id) and what has
7861
- * already been decided (by agent and complete decision context), both bounded.
7862
- *
7863
- * The execution cache is the bridge between the two seams — `tools/pre-execute`
7864
- * knows the arguments, `approval/request` knows the call id — and it is keyed by
7865
- * call id with an agent-identity check when read: an entry for an execution
7866
- * that never asked is never read, so there is nothing to clean up on the result
7867
- * side (and the ask happens before the result anyway).
7868
- */
7869
- var CommandGuard = class {
7870
- deps;
7871
- executions = /* @__PURE__ */ new Map();
7872
- decisions = /* @__PURE__ */ new WeakMap();
7873
- constructor(deps) {
7874
- this.deps = deps;
7875
- }
7876
- /**
7877
- * Remember one execution's command. Never throws, never changes a decision.
7878
- *
7879
- * @param execution - the execution about to run.
7880
- */
7881
- noteExecution(execution) {
7882
- const callId = execution?.callId;
7883
- if (typeof callId !== "string" || callId === "") return;
7884
- const fact = shellCommandOf(execution.name, execution.arguments);
7885
- if (fact === void 0) return;
7886
- this.executions.delete(callId);
7887
- this.executions.set(callId, {
7888
- fact,
7889
- agent: execution.agent
7890
- });
7891
- while (this.executions.size > 64) {
7892
- const oldest = this.executions.keys().next();
7893
- if (oldest.done === true) break;
7894
- this.executions.delete(oldest.value);
7895
- }
7896
- }
7897
- /** Forget every remembered execution and decision (disposal, tests). */
7898
- clear() {
7899
- this.executions.clear();
7900
- this.decisions = /* @__PURE__ */ new WeakMap();
7901
- }
7902
- /**
7903
- * Judge one approval request.
7904
- *
7905
- * @param request - the pending approval.
7906
- * @returns the verdict; `approve` is the only outcome that grants anything.
7907
- */
7908
- async judge(request) {
7909
- const settings = this.deps.settings();
7910
- if (!settings.enabled) return {
7911
- kind: "delegate",
7912
- reason: "guard disabled"
7913
- };
7914
- const callId = request.callId;
7915
- if (callId === void 0) return {
7916
- kind: "delegate",
7917
- reason: "approval carries no call id"
7918
- };
7919
- const observed = this.executions.get(callId);
7920
- if (observed === void 0 || observed.agent !== request.agent) return {
7921
- kind: "delegate",
7922
- reason: "the guard never saw this call for this agent"
7923
- };
7924
- const fact = observed.fact;
7925
- const excerpt = commandExcerpt(fact.command);
7926
- if (!COMMAND_GUARD_SHELL_TOOLS.includes(request.toolName)) return {
7927
- kind: "delegate",
7928
- reason: `not a shell tool (${request.toolName})`
7929
- };
7930
- if (fact.command.length > 6e3) return {
7931
- kind: "delegate",
7932
- reason: `command longer than ${COMMAND_GUARD_MAX_COMMAND_CHARS} characters`,
7933
- command: excerpt
7934
- };
7935
- const denied = denyReasonOf(fact.command);
7936
- if (denied !== void 0) return {
7937
- kind: "delegate",
7938
- reason: `denylisted command (${denied})`,
7939
- command: excerpt
7940
- };
7941
- const full = {
7942
- ...fact,
7943
- ...request.reason === void 0 ? {} : { reason: request.reason }
7944
- };
7945
- const memoKey = JSON.stringify(full);
7946
- const now = this.deps.now?.() ?? Date.now();
7947
- const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
7948
- const memo = agentDecisions?.get(memoKey);
7949
- if (memo !== void 0) {
7950
- if (now - memo.at < 6e5) return this.verdict(memo, settings.threshold, "memo", excerpt, full.sandboxPermissions !== void 0);
7951
- agentDecisions?.delete(memoKey);
7952
- }
7953
- let response;
7954
- try {
7955
- response = await this.deps.decide(buildSafetyRequest(full), {
7956
- ...request.signal === void 0 ? {} : { signal: request.signal },
7957
- timeoutMs: settings.timeoutMs
7958
- });
7959
- } catch (error) {
7960
- return {
7961
- kind: "delegate",
7962
- reason: `decision unavailable: ${error instanceof Error ? error.message : String(error)}`,
7963
- command: excerpt
7964
- };
7965
- }
7966
- const probability = safeProbabilityOf(response);
7967
- if (probability === void 0) return {
7968
- kind: "delegate",
7969
- reason: "the decision carried no safe verdict",
7970
- command: excerpt
7971
- };
7972
- let escalation;
7973
- if (full.sandboxPermissions !== void 0) {
7974
- const scope = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID);
7975
- const necessity = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID);
7976
- if (scope === void 0 || necessity === void 0) return {
7977
- kind: "delegate",
7978
- reason: "the decision carried no complete sandbox-escalation verdict",
7979
- command: excerpt
7980
- };
7981
- escalation = {
7982
- scope,
7983
- necessity
7984
- };
7985
- }
7986
- const memoEntry = {
7987
- probability,
7988
- ...escalation === void 0 ? {} : { escalation },
7989
- at: now
7990
- };
7991
- if (request.agent !== void 0) {
7992
- const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
7993
- decisions.set(memoKey, memoEntry);
7994
- while (decisions.size > 64) {
7995
- const oldest = decisions.keys().next();
7996
- if (oldest.done === true) break;
7997
- decisions.delete(oldest.value);
7998
- }
7999
- this.decisions.set(request.agent, decisions);
8000
- }
8001
- return this.verdict(memoEntry, settings.threshold, "model", excerpt, full.sandboxPermissions !== void 0);
8002
- }
8003
- /** Apply the threshold to the primary verdict and any escalation vetoes. */
8004
- verdict(entry, threshold, source, command, escalated) {
8005
- if (entry.probability < threshold) return {
8006
- kind: "delegate",
8007
- reason: `safe probability ${entry.probability.toFixed(3)} below ${threshold}`,
8008
- probability: entry.probability,
8009
- command
8010
- };
8011
- if (!escalated) return {
8012
- kind: "approve",
8013
- probability: entry.probability,
8014
- source,
8015
- command
8016
- };
8017
- if (entry.escalation === void 0) return {
8018
- kind: "delegate",
8019
- reason: "the cached sandbox-escalation verdict was incomplete",
8020
- command
8021
- };
8022
- if (entry.escalation.scope < threshold) return {
8023
- kind: "delegate",
8024
- reason: `sandbox scope probability ${entry.escalation.scope.toFixed(3)} below ${threshold}`,
8025
- probability: entry.probability,
8026
- command
8027
- };
8028
- if (entry.escalation.necessity < threshold) return {
8029
- kind: "delegate",
8030
- reason: `sandbox necessity probability ${entry.escalation.necessity.toFixed(3)} below ${threshold}`,
8031
- probability: entry.probability,
8032
- command
8033
- };
8034
- return {
8035
- kind: "approve",
8036
- probability: entry.probability,
8037
- source,
8038
- ...entry.escalation === void 0 ? {} : { escalation: entry.escalation },
8039
- command
8040
- };
8041
- }
8042
- };
8043
- /**
8044
- * Register the guard on a context that has the approval seam.
8045
- *
8046
- * Call this from inside `ctx.inject(['approval'], …)`: the seam's presence is
8047
- * what makes the feature meaningful (no approval service means no asks), and
8048
- * tying the listeners to that injected fiber is what makes them go away with it.
8049
- *
8050
- * @param ctx - the injected context (or a test double with `on`).
8051
- * @param deps - settings, the decision client, clock and logger.
8052
- * @returns a disposer for both listeners.
8053
- */
8054
- function applyCommandGuard(ctx, deps) {
8055
- const guard = new CommandGuard(deps);
8056
- const events = ctx;
8057
- const offPreExecute = events.on("tools/pre-execute", (execution, next) => {
8058
- guard.noteExecution(execution);
8059
- return next();
8060
- }, { prepend: true });
8061
- const offApproval = events.on("approval/request", async (request, next) => {
8062
- try {
8063
- if (!deps.settings().enabled) return next();
8064
- } catch (error) {
8065
- deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
8066
- return next();
8067
- }
8068
- let verdict;
8069
- try {
8070
- verdict = await guard.judge(request);
8071
- } catch (error) {
8072
- deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
8073
- return next();
8074
- }
8075
- if (verdict.kind === "approve") {
8076
- deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}${verdict.escalation === void 0 ? "" : `, sandbox scope ${verdict.escalation.scope.toFixed(3)}, necessity ${verdict.escalation.necessity.toFixed(3)}`}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
8077
- return "allowed-once";
8078
- }
8079
- deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
8080
- return next();
8081
- }, { prepend: true });
8082
- return () => {
8083
- guard.clear();
8084
- for (const off of [offPreExecute, offApproval]) if (typeof off === "function") off();
8085
- };
8086
- }
8087
- //#endregion
8088
7227
  //#region src/config-volatile.ts
8089
7228
  /**
8090
7229
  * Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
8091
7230
  *
8092
- * 0.1.7 rewrote settings around profile Config: `settings.describe()` projects
8093
- * each active entry's schema through `volatileForm()`, so a form contains ONLY
8094
- * fields whose schema nodes carry `meta.volatile`, and a form edit is refused
8095
- * unless its path lies beneath a marked node. On that generation the loader
8096
- * resolves each marked top-level field into a readonly `Volatile<T>` reference
8097
- * (`{ get() }`, frozen, written in place by the owning runtime) instead of a
8098
- * plain value, commits later changes WITHOUT remounting the fiber, and
8099
- * dispatches `loader/volatile-update` to the owning fiber.
8100
- *
8101
- * Two helpers keep the rest of the plugin reading a PLAIN `Config`:
8102
- *
8103
- * - `markVolatile(schema)` applies `.volatile()`, so 0.1.7's forms see every
8104
- * field we mark. The declared type is preserved through the mark on purpose:
8105
- * it changes what the schema PARSES to (a live reference), not the
8106
- * plugin-facing `Config` shape every consumer reads.
8107
- * - `unwrapVolatileConfig(config)` reads every top-level field through its
8108
- * reference when one is present — returning a FRESH object per call, since
8109
- * a reference's identity is stable while its value changes — and returns the
8110
- * config untouched when none is.
8111
- *
8112
- * Only TOP-LEVEL fields are marked (form writes name top-level paths such as
8113
- * `accounts` or `modelVisibility.<id>`, whose first segment is the marked
8114
- * node), so one unwrap level is complete. The literal `apiKey` secret stays
8115
- * unmarked on purpose: no settings surface writes it — the settings page and
8116
- * the dsh-TUI section both write keys through the credentials seam — so it
8117
- * keeps composition-only semantics, and a config-file edit to it reloading the
8118
- * fiber is the correct behavior for a secret literal.
7231
+ * 0.1.7's `settings.describe()` projects each entry's schema through
7232
+ * `volatileForm()`, so a form contains ONLY nodes carrying `meta.volatile`
7233
+ * and a form edit is refused unless its path lies beneath a marked node. The
7234
+ * loader then resolves each marked field into a frozen `{ get() }` reference
7235
+ * that is written IN PLACE (no remount) and announced with
7236
+ * `loader/volatile-update`. Two helpers keep the rest of the plugin reading a
7237
+ * PLAIN `Config`: `markVolatile()` applies the mark, and
7238
+ * `unwrapVolatileConfig()` reads every top-level field through its reference
7239
+ * when one is present — FRESH per call, because a reference's identity is
7240
+ * stable while its value changes.
7241
+ *
7242
+ * Only TOP-LEVEL fields are marked (a form write names a top-level path such
7243
+ * as `accounts` or `modelVisibility.<id>`), so one unwrap level is complete.
7244
+ * The literal `apiKey` secret stays unmarked on purpose: no settings surface
7245
+ * writes it — page and TUI both write keys through the credentials seam — so a
7246
+ * config-file edit to it reloading the fiber is correct for a secret literal.
8119
7247
  *
8120
7248
  * @module dsh-commandcode-provider/config-volatile
8121
7249
  */
8122
7250
  /**
8123
7251
  * Mark one schema field volatile.
8124
7252
  *
8125
- * `.volatile()` is unconditional: the mark is a schemastery 3.18.3 feature and
8126
- * the only engine this bundle supports (`dsh 0.1.7-rc.1`) pins `~3.18.4`, so
8127
- * the method is always present. It is reached through a structural member
8128
- * rather than schemastery's own typing so the function can keep the caller's
8129
- * declared field type, which is what lets `Config` stay the plain shape every
8130
- * consumer reads while the schema parses to live references.
8131
- *
8132
- * @param schema - The field schema.
8133
- * @returns The schema carrying `meta.volatile`.
7253
+ * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
7254
+ * only supported engine (`dsh 0.1.7-rc.2`) pins `~3.18.4`, so the method is
7255
+ * always present. It is reached through a structural member so the caller's
7256
+ * declared field type survives — the mark changes what the schema PARSES to (a
7257
+ * live reference), not the plugin-facing `Config` shape.
8134
7258
  */
8135
7259
  function markVolatile(schema) {
8136
7260
  return schema.volatile();
@@ -8138,10 +7262,6 @@ function markVolatile(schema) {
8138
7262
  /**
8139
7263
  * Mark every field of a Config field dict volatile, except the named
8140
7264
  * composition-only secrets (`apiKey` here, which no settings surface writes).
8141
- *
8142
- * @param fields - Freshly built field schemas.
8143
- * @param skip - Field names that must stay unmarked.
8144
- * @returns The field dict with every non-skipped field marked.
8145
7265
  */
8146
7266
  function markVolatileFields(fields, skip = []) {
8147
7267
  return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
@@ -8163,8 +7283,8 @@ function isVolatileRef(value) {
8163
7283
  *
8164
7284
  * Every top-level field found to be a volatile reference is read through
8165
7285
  * `.get()` — freshly on every call, so a live update lands on the very next
8166
- * read. The loader resolves EVERY marked node into a reference, unset fields
8167
- * included, so a loader-parsed config always yields a fresh object here; the
7286
+ * read (the loader resolves EVERY marked node into a reference, unset fields
7287
+ * included, so a loader-parsed config always yields a fresh object here). The
8168
7288
  * pass-through for a field that is not a reference is what keeps the unmarked
8169
7289
  * `apiKey` literal readable.
8170
7290
  */
@@ -8179,15 +7299,14 @@ function unwrapVolatileConfig(config) {
8179
7299
  //#region src/index.ts
8180
7300
  /**
8181
7301
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
8182
- * Code (unofficial; ported from pi-commandcode-provider@0.5.1).
7302
+ * Code (unofficial; ported from pi-commandcode-provider@0.5.1, MIT).
8183
7303
  *
8184
7304
  * Registers the `commandcode` provider route on `ctx.llm` and declares it in
8185
7305
  * the configurable-provider directory, so the web Models page shows a
8186
- * "Command Code" card with an API-key field and the model picker lists the
8187
- * live Command Code model catalog. Connection facts resolve per request over
8188
- * the plugin's live profile Config (volatile fields, unwrapped per read) and
8189
- * the credential seam, so a changed key, endpoint, or cache path reaches the
8190
- * next request without a restart.
7306
+ * "Command Code" card and the model picker lists the live catalog. Connection
7307
+ * facts are resolved per request from the live profile Config (volatile
7308
+ * fields, unwrapped per read) and the credential seam, so a changed key,
7309
+ * endpoint or cache path reaches the next request without a restart.
8191
7310
  *
8192
7311
  * ```yaml
8193
7312
  * - id: llm-commandcode
@@ -8214,14 +7333,12 @@ const PROVIDER = "commandcode";
8214
7333
  const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
8215
7334
  /**
8216
7335
  * The Config schema: every field volatile except the composition-only `apiKey`
8217
- * secret.
8218
- *
8219
- * dsh 0.1.7's settings forms are projected from the schema's `meta.volatile`
8220
- * nodes, so an unmarked field would be invisible to AND unwritable from the
8221
- * settings page and refused by form-edit path validation — and the loader
8222
- * hands `apply()` a live reference per marked field, committing later writes
8223
- * without remounting this fiber (the `loader/volatile-update` listener at the
8224
- * bottom of `apply` covers the facts that are not re-derived per read).
7336
+ * secret. 0.1.7's settings forms are projected from the schema's
7337
+ * `meta.volatile` nodes, so an unmarked field would be invisible to AND
7338
+ * unwritable from the settings page. The loader also hands `apply()` a live
7339
+ * reference per marked field, so writes commit in place without remounting
7340
+ * this fiber — the `loader/volatile-update` listener at the bottom of `apply`
7341
+ * re-derives the two facts that are not re-read per request.
8225
7342
  */
8226
7343
  const Config = z.object(markVolatileFields({
8227
7344
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
@@ -8234,14 +7351,6 @@ const Config = z.object(markVolatileFields({
8234
7351
  transportMaxRetries: z.number().min(0).max(50),
8235
7352
  filterModelsByPlan: z.boolean(),
8236
7353
  visibleModels: z.array(z.string()),
8237
- /**
8238
- * Per-model visibility overrides for the terminal settings page's checkbox
8239
- * list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
8240
- * PATH, so two checkboxes sharing one path would overwrite each other's
8241
- * draft and the section's last model would decide every write; a map gives
8242
- * each checkbox a path of its own. An id listed here wins over
8243
- * {@link Config.visibleModels}; ids absent here keep following it.
8244
- */
8245
7354
  modelVisibility: z.dict(z.boolean()),
8246
7355
  webSearch: z.boolean().default(true),
8247
7356
  showSidebarQuota: z.boolean().default(false),
@@ -8256,8 +7365,6 @@ const Config = z.object(markVolatileFields({
8256
7365
  models: z.array(z.string()),
8257
7366
  account: z.string()
8258
7367
  })),
8259
- commandGuard: z.boolean().default(false),
8260
- commandGuardLevel: z.union([...COMMAND_GUARD_LEVELS]).default(COMMAND_GUARD_DEFAULT_LEVEL),
8261
7368
  zdr: z.boolean().default(false),
8262
7369
  lang: z.string().pattern(/^(zh|en)$/).default("zh")
8263
7370
  }, ["apiKey"]));
@@ -8265,7 +7372,7 @@ const Config = z.object(markVolatileFields({
8265
7372
  * The one explicit resolve step from raw config to validated connection
8266
7373
  * facts. Programmatic construction may bypass Schemastery normalization, so
8267
7374
  * every default is re-judged here — for the composition entry at load and for
8268
- * each settings snapshot at its first use.
7375
+ * every settings-backed read.
8269
7376
  */
8270
7377
  function resolveAdapterOptions(config) {
8271
7378
  return {
@@ -8282,11 +7389,10 @@ function resolveAdapterOptions(config) {
8282
7389
  };
8283
7390
  }
8284
7391
  /**
8285
- * Per-model visibility overrides, cleaned for the adapter. Programmatic
8286
- * construction may bypass Schemastery normalization, so a non-object or a
8287
- * non-boolean entry is dropped here instead of reaching the picker filter.
8288
- * @param raw - The `modelVisibility` value from any config source.
8289
- * @returns A frozen id → boolean map, or undefined when nothing is set.
7392
+ * Per-model visibility overrides, cleaned for the adapter: a non-object or a
7393
+ * non-boolean entry is dropped rather than reaching the picker filter, and an
7394
+ * all-empty map reads as no map. Returns a fresh id → boolean map, or
7395
+ * undefined when nothing is set.
8290
7396
  */
8291
7397
  function readModelVisibility(raw) {
8292
7398
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
@@ -8484,26 +7590,6 @@ function apply(ctx, config) {
8484
7590
  applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
8485
7591
  }, "dsh-commandcode-provider: web search selection");
8486
7592
  });
8487
- const commandGuardSettings = () => {
8488
- const raw = current();
8489
- return {
8490
- enabled: raw.commandGuard === true,
8491
- threshold: commandGuardThreshold(raw.commandGuardLevel),
8492
- timeoutMs: COMMAND_GUARD_TIMEOUT_MS
8493
- };
8494
- };
8495
- ctx.inject(["approval"], (approvalCtx) => {
8496
- applyCommandGuard(approvalCtx, {
8497
- settings: commandGuardSettings,
8498
- decide: (request, decideOptions) => runSystemOne({
8499
- apiBase: () => options().apiBase,
8500
- resolveApiKey: () => resolveApiKey(options())
8501
- }, request, decideOptions),
8502
- log: (message) => {
8503
- ctx.logger.info(`llm-commandcode: ${message}`);
8504
- }
8505
- });
8506
- });
8507
7593
  let refreshTuiSettings;
8508
7594
  ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
8509
7595
  refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {