@mars-sea/dsh-commandcode-provider 0.11.16 → 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,20 +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.2` `dist/cli.mjs`, unchanged from 1.65.0:
399
- * `modelSupportsZdr(id)` is exactly `!nonZdrSet.has(canonicalize(id))`, and
400
- * `knownModelSupportsZdr`
401
- * carries the same membership in the sibling route table — the union is this
402
- * set). The official docs (commandcode.ai/docs/resources/zdr) put it in prose
403
- * — "99% of our models have ZDR-capable upstreams … only a small handful of
404
- * models are affected" — so the CLI's exclusion list is the only per-model
405
- * evidence there is; a ZDR request naming one of these fails with HTTP 422
406
- * `cmd_zdr_no_providers` instead of routing through a provider that retains.
398
+ * registry (`command-code@1.66.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
399
+ * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
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.
407
409
  *
408
410
  * Why a NEGATIVE set, and why "not listed" answers TRUE: 99% of the catalog is
409
411
  * covered, so the maintained difference is the exception list. This helper is
410
412
  * informational; the adapter sends the ZDR header for EVERY request when the
411
- * 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
412
414
  * unsupported model rather than silently dropping the privacy guarantee.
413
415
  *
414
416
  * `minimax/minimax-m3-free` is the one entry the public catalog
@@ -419,16 +421,9 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
419
421
  * Keep in sync via the dsh-commandcode-upstream skill: the CLI's registry data
420
422
  * (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
421
423
  * flags) is upstream-internal routing, not a per-model contract, so this table
422
- * is the snapshot of the exclusion set and nothing more. The 1.62.0 → 1.64.0
423
- * diff of that union is EMPTY: both anchors were extracted from both bundles
424
- * during the 2026-09-23 check and each carries the same 20 members — which is
425
- * how rare a change here is expected to be. The 1.64.0 → 1.65.0 diff (2026-09-24)
426
- * is the counterexample that proves the check still runs: exactly one member
427
- * joins (`stealth/space-bunny-alpha`), and it is in both anchors. `meituan/
428
- * LongCat-2.0` is the one member the two anchors disagree about (it is in
429
- * `modelSupportsZdr`'s set in both releases and in neither `knownModelSupportsZdr`
430
- * set), so the union is
431
- * 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).
432
427
  */
433
428
  const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
434
429
  "MiniMaxAI/MiniMax-M3",
@@ -443,6 +438,7 @@ const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
443
438
  "poolside/laguna-s-2.1-free",
444
439
  "sakana/fugu-ultra",
445
440
  "stealth/space-bunny-alpha",
441
+ "stealth/pixel-canary",
446
442
  "stepfun/Step-3.7-Flash",
447
443
  "stepfun/Step-5-Preview",
448
444
  "xai/grok-4.5",
@@ -462,36 +458,18 @@ function supportsZeroDataRetention(modelId) {
462
458
  return !KNOWN_NON_ZDR_MODELS.has(modelId);
463
459
  }
464
460
  /**
465
- * Models the official CLI's model table (command-code@1.53.0) marks
466
- * `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
467
- * think automatically, with Command Code driving the depth. This is the
468
- * authoritative "thinks, effort not adjustable" set: `KNOWN_EFFORTS` (which
469
- * mirrors the CLI's effort map exactly) stays the sole source for selectable
470
- * effort levels, and this snapshot is not surfaced in the picker's compact
471
- * 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.
472
467
  *
473
- * Source: the command-code@1.53.0 bundled model table (dist/cli.mjs),
474
- * cross-checked with https://commandcode.ai/docs/reference/cli/models.
475
- * (`stealth/ox-alpha` left this set in command-code@1.32.1, which gave it
476
- * selectable `['low', 'high', 'max']` efforts; the preview then ended in
477
- * 1.34.0, removing the model from the catalog entirely. `tencent/hy4-preview`
478
- * joined this set in command-code@1.37.0 — reasoning:!0, no efforts, 1M
479
- * context, routed through OpenRouter — then gained selectable
480
- * `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
481
- * `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
482
- * command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
483
- * to `KNOWN_EFFORTS`. command-code@1.42.0 added LongCat 2.0 (then
484
- * `meituan/LongCat-2.0:free`, now the paid `meituan/LongCat-2.0`; reasoning:!0,
485
- * no efforts). command-code@1.45.0 gave the Muse Spark family
486
- * (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
487
- * `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
488
- * command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
489
- * `['low', 'medium', 'high']` efforts — it moved to `KNOWN_EFFORTS` too.
490
- * command-code@1.52.0 added `inclusionai/ling-3.0-flash-sante:free`
491
- * (reasoning:!0, no efforts).)
492
- * command-code@1.53.0 added `deepseek/deepseek-v4.1-flash` with selectable
493
- * ['low', 'high', 'max'] efforts, so it lives in `KNOWN_EFFORTS`, not here.)
494
- * 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).
495
473
  */
496
474
  const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
497
475
  "Qwen/Qwen3.6-Max-Preview",
@@ -520,7 +498,7 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
520
498
  *
521
499
  * Measured 2026-09-16 against the live catalog (command-code@1.54.0): all 69
522
500
  * models were posted to `/provider/v1/chat/completions`; exactly these eight
523
- * — 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
524
502
  * normally by `/alpha/generate` (a lower-plan key gets the ordinary
525
503
  * `MODEL_NOT_IN_PLAN` 403 there, never a routing error). So the CLI transport
526
504
  * is a complete fallback and the adapter does not need a Messages transport.
@@ -531,13 +509,15 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
531
509
  * the Provider API by default — picking any Claude model there failed every
532
510
  * request before this snapshot existed.
533
511
  *
534
- * Keep in sync when models ship (see the dsh-commandcode-upstream skill).
535
512
  * `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
536
- * Messages-only, so a Claude model added upstream is routed correctly by an
537
- * un-updated plugin instead of hard-failing with the 400 above; the worst case
538
- * of that rule going stale the other way (upstream teaching
539
- * `/provider/v1/chat/completions` to serve Claude) is one model riding the CLI
540
- * 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).
541
521
  */
542
522
  const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
543
523
  "claude-sonnet-5",
@@ -555,31 +535,14 @@ function requiresMessagesEndpoint(modelId) {
555
535
  }
556
536
  /**
557
537
  * The minimum subscription plan a model is included in, per the official plan
558
- * pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`, `/docs/plans/max`
559
- * and `/docs/resources/pricing-limits`). Each plan's model list is a superset of
560
- * the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max. Models absent from every
561
- * plan list (Claude Opus/Fable, Fugu Ultra) are Provider-tier.
562
- * `claude-fable-5-1` (Claude Fable 5.1, added in command-code@1.40.0) is
563
- * Provider/Max-tier exactly like `claude-fable-5` — its availability matrix on
564
- * the official plan/pricing pages grants individual-provider/max/ultra and
565
- * teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
566
- * command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
567
- * LongCat 2.0 (Go — a free promo until 2026-09-19, when the backend renamed
568
- * `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0`); 1.43.0 added
569
- * `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
570
- * (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
571
- * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
572
- * added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
573
- * `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
574
- * `z-ai/glm-5.3-flashx` (Go); command-code@1.59.0 added `xai/grok-4.7` (GOAT)
575
- * and 1.60.0 added `stepfun/Step-5-Preview` (Go); command-code@1.62.0 added the
576
- * MiMo V2.6 family — `xiaomi/mimo-v2.6-flash` + `xiaomi/mimo-v2.6-pro` (Go) and
577
- * `xiaomi/mimo-v2.6-pro-ultraspeed` (GOAT) — so the 1.58.0 -> 1.62.0 window's
578
- * only tier changes are additions and the superset chain still holds. The
579
- * 1.62.0 -> 1.65.0 window continues the pattern: 1.64.0 added
580
- * `claude-opus-5-5` (Provider/Max), `gpt-6-sol` (Pro) and `gpt-6-luna` (Go),
581
- * and 1.65.0 added `stealth/space-bunny-alpha` (Go, every tier) — additions
582
- * 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.
583
546
  *
584
547
  * The Provider API exposes no plan metadata, so this snapshot is the source of
585
548
  * truth for the picker's plan annotation — it answers "which plan do I need to
@@ -624,6 +587,7 @@ const KNOWN_PLANS = {
624
587
  "stepfun/Step-3.5-Flash": "go",
625
588
  "stepfun/Step-3.7-Flash": "go",
626
589
  "stepfun/Step-5-Preview": "go",
590
+ "stealth/pixel-canary": "go",
627
591
  "stealth/space-bunny-alpha": "go",
628
592
  "tencent/hy3-paid": "go",
629
593
  "tencent/hy4-preview": "go",
@@ -717,11 +681,11 @@ function compareByPlan(a, b) {
717
681
  }
718
682
  /**
719
683
  * Subscription plan table, synced from the official CLI bundle's plan maps
720
- * (located by the `"individual-go"` key in command-code@1.53.0 `dist/cli.mjs`,
721
- * re-verified unchanged through 1.53.0): subscription `planId`
722
- * prefix → display name and the plan's monthly credit total. This is the
723
- * account's own subscription (from `/alpha/billing/subscriptions`) — distinct
724
- * 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.
725
689
  *
726
690
  * `tierWeight` is plugin-added (not from the CLI maps): the plan's rank on
727
691
  * the {@link PLAN_ORDER} scale, used by the picker's plan filter
@@ -836,6 +800,10 @@ const KNOWN_DEALS = {
836
800
  "stealth/space-bunny-alpha": {
837
801
  label: "FREE",
838
802
  free: true
803
+ },
804
+ "stealth/pixel-canary": {
805
+ label: "FREE",
806
+ free: true
839
807
  }
840
808
  };
841
809
  /**
@@ -844,24 +812,11 @@ const KNOWN_DEALS = {
844
812
  * charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
845
813
  * weekday, full price) **Monday to Friday only**; the other 17 hours of a
846
814
  * weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
847
- * The V4 Flash Vision (exp) variant (command-code@1.32.0) shares the V4 Flash
848
- * rates exactly — $0.15/$0.60 off-peak and $0.30/$1.20 peak, per the page's own
849
- * `timeOfDay` block, not merely 2× its own off-peak figures: a rate that is
850
- * internally consistent can still be the wrong row, which is why the vendored
851
- * price table (`./model-prices.ts`) is synced from the page and not hand-kept.
852
- * The picker shows the
853
- * *current* state as a compact
854
- * label (`Peak`/`Half`) matching the English noun style of the other markers
855
- * (`Image`, `FREE`), so a developer can tell at a glance whether calling the
856
- * model right now is cheap or expensive.
857
- *
858
- * Authoritative extraction: the pricing page embeds a model JSON array whose
859
- * hourly-priced entries carry a `timeOfDay` block
860
- * (`{ windows: "01–04 & 06–10 UTC, Mon–Fri", peakHoursPerDay: 7,
861
- * offPeakHoursPerDay: 17, peak: {...}, offPeak: {...} }`). Exactly four models
862
- * carry it: V4 Pro, V4 Flash, V4 Flash Vision (exp), and V4.1 Flash (added in
863
- * command-code@1.53.0 at $0.15/$0.60 off-peak, $0.30/$1.20 peak — the same
864
- * 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.
865
820
  *
866
821
  * Extraction caution: the rendered HTML rows are a trap. Each annotation div
867
822
  * sits inside its OWN row's container, immediately before the NEXT row starts,
@@ -869,7 +824,11 @@ const KNOWN_DEALS = {
869
824
  * to the model printed after it — that is how `deepseek/deepseek-v4-flash-fast`
870
825
  * was wrongly added here (its row is flat-priced at $0.28/$0.56/$0.07 and has
871
826
  * no `timeOfDay` block). Trust the embedded JSON's `timeOfDay` membership and
872
- * 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.
873
832
  *
874
833
  * Keep in sync with the official pricing page when the model set, the peak
875
834
  * windows, or the weekday rule change (see the dsh-commandcode-upstream skill).
@@ -908,11 +867,8 @@ function isPeakPricingHour(now = Date.now()) {
908
867
  return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
909
868
  }
910
869
  /**
911
- * Whether `now` (defaults to `Date.now()`) falls in a peak-pricing window for
912
- * time-of-day-priced models. Peak rates apply Monday–Friday (UTC) only: the
913
- * official rule charges Saturday and Sunday completely off-peak for all 24
914
- * hours, so a weekend timestamp is off-peak even inside `PEAK_HOUR_RANGES`.
915
- * `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.
916
872
  */
917
873
  function peakPricingState(modelId, now = Date.now()) {
918
874
  if (!KNOWN_PEAK_PRICING.has(modelId)) return void 0;
@@ -1802,7 +1758,7 @@ const MODEL_PRICE_ROWS = [
1802
1758
  {
1803
1759
  id: "step-3.5-flash",
1804
1760
  rates: [
1805
- .1,
1761
+ .09,
1806
1762
  .3,
1807
1763
  .02
1808
1764
  ]
@@ -1838,6 +1794,14 @@ const MODEL_PRICE_ROWS = [
1838
1794
  2.501,
1839
1795
  .042
1840
1796
  ]
1797
+ },
1798
+ {
1799
+ id: "typesafe/jev",
1800
+ rates: [
1801
+ .042,
1802
+ 0,
1803
+ 0
1804
+ ]
1841
1805
  }
1842
1806
  ];
1843
1807
  /**
@@ -1854,8 +1818,8 @@ const PRICE_SLUG_OVERRIDES = { "nvidia/nemotron-3-ultra-550b-a55b": "nemotron-3-
1854
1818
  * not consistently: `tencent/hy4-preview` keeps its prefix while
1855
1819
  * `Qwen/Qwen3.8-Max-0902` becomes `qwen-3.8-max-0902`, with a hyphen the
1856
1820
  * catalog id does not have. Generating candidates and taking the first that
1857
- * exists in the vendored table absorbs that drift without a hand-maintained
1858
- * map of seventy ids.
1821
+ * exists in the vendored table absorbs that drift without a hand-maintained map
1822
+ * of eighty ids.
1859
1823
  */
1860
1824
  function priceSlugCandidates(modelId) {
1861
1825
  const lower = modelId.toLowerCase();
@@ -1910,13 +1874,10 @@ function wireRow(id, slug, row) {
1910
1874
  * Rows are keyed by CATALOG id wherever the two namespaces reconcile, because
1911
1875
  * that is what a session reports, and every row also carries its page slug as a
1912
1876
  * second lookup key. Price rows no catalog model claims are served under the
1913
- * slug alone, so drift in either direction still prices: a page rename the
1914
- * catalog has not followed, or a model the catalog snapshot has not learned
1915
- * yet. Free models are served explicitly at zero so the composer can say so
1916
- * instead of showing nothing.
1917
- *
1918
- * The peak windows travel with the table, so the browser applies the very
1919
- * 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.
1920
1881
  */
1921
1882
  function modelPriceTable() {
1922
1883
  const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
@@ -1970,13 +1931,13 @@ function peakHour(at, windows) {
1970
1931
  return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
1971
1932
  }
1972
1933
  /**
1973
- * 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.
1974
1936
  *
1975
1937
  * A tiered model returns the matching band and does NOT then apply `peak`: the
1976
- * page publishes those two dimensions independently, no row carries both today,
1977
- * and the generator would report a tiered row on every sync — so the combination
1978
- * becoming real is visible before it reaches a user. This ordering is a latent
1979
- * 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.
1980
1941
  */
1981
1942
  function requestRates(price, at, contextTokens, table) {
1982
1943
  const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
@@ -2020,11 +1981,11 @@ function costGroupKey(group, table) {
2020
1981
  //#endregion
2021
1982
  //#region src/cost-projection.ts
2022
1983
  const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
2023
- const finite$1 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
1984
+ const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
2024
1985
  function tokensOf(value) {
2025
1986
  const u = record$2(value);
2026
- if (!u || !finite$1(u.inputTokens) || !finite$1(u.outputTokens)) return void 0;
2027
- 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;
2028
1989
  return {
2029
1990
  uncachedInputTokens: u.inputTokens,
2030
1991
  outputTokens: u.outputTokens,
@@ -2050,13 +2011,13 @@ function usageOf(type, data) {
2050
2011
  }
2051
2012
  function parseState(value) {
2052
2013
  const s = record$2(value), facts = record$2(s?.facts);
2053
- 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");
2054
2015
  const group = (v) => {
2055
2016
  const g = record$2(v), t = record$2(g?.tokens);
2056
- 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]));
2057
2018
  };
2058
2019
  const sel = record$2(s.selection), last = record$2(s.last);
2059
- 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");
2060
2021
  return value;
2061
2022
  }
2062
2023
  /** Optional registry uses only synchronous parse/view; no new runtime module is required. */
@@ -2090,11 +2051,11 @@ function createCostProjection(table = modelPriceTable()) {
2090
2051
  }
2091
2052
  if (event.type === "step/start" || event.type === "llm/retry-started") return {
2092
2053
  ...state,
2093
- at: finite$1(event.time) ? event.time : null,
2054
+ at: finite(event.time) ? event.time : null,
2094
2055
  last: null
2095
2056
  };
2096
2057
  const tokens = usageOf(event.type, data);
2097
- 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;
2098
2059
  const previous = state.last?.turn === data.turn && state.last.step === data.step ? state.last.group : void 0;
2099
2060
  const group = {
2100
2061
  provider: state.selection?.provider ?? "",
@@ -2166,34 +2127,18 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2166
2127
  /**
2167
2128
  * Multi-account pool for the Command Code provider (host side).
2168
2129
  *
2169
- * One Command Code subscription (e.g. the Go plan's 5-hour window) is
2170
- * metered; a user with several subscriptions wants a request that hits one
2171
- * account's limit to continue on the next account without a visible failure.
2172
- * 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.
2173
2135
  *
2174
- * - {@link CommandCodeAccountPool.resolveKey} hands out the first account
2175
- * whose key is not currently marked exhausted — or, when the request's
2176
- * model matches a {@link CommandCodeModelAccountRule} and that account is
2177
- * usable, the routed account — resolving each slot's key lazily (literal
2178
- * config key → credential seam → launch environment → the official CLI
2179
- * auth file for the default slot only).
2180
- * - {@link CommandCodeAccountPool.markRejected} records a 429 or 401 against
2181
- * the exact API key, so several slots sharing one key share one state. A
2182
- * 429 that NAMED a usage window marks the window exhausted; a 429 that
2183
- * named none is recorded as a plain throttle, which rotates the same way
2184
- * but is never reported as an exhausted window (issue #54).
2185
- * - When every account is marked, the pool probes each key's
2186
- * `/alpha/billing/credits` window limits (through the injected
2187
- * {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
2188
- * no longer reports `exceeded` is revived, otherwise the pool throws a
2189
- * `RATE_LIMIT` error naming the earliest reset time.
2190
- * - An account the user explicitly asked for — the manual pin
2191
- * ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
2192
- * routed account — is probed the same way when a mark would demote it, so
2193
- * 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.
2194
2139
  *
2195
- * The pool is deliberately cordis-free (like the adapter): every host fact
2196
- * 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.
2197
2142
  *
2198
2143
  * @module dsh-commandcode-provider/accounts
2199
2144
  */
@@ -2201,22 +2146,21 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
2201
2146
  * Upper bound on the retry wait this pool attaches to the all-exhausted
2202
2147
  * `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
2203
2148
  * `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
2204
- * provider-specified wait verbatim only at or below that cap — in normal mode
2205
- * a LONGER attached wait makes the executor abandon the retry entirely
2206
- * instead of falling back to local backoff, which would turn "poll until the
2207
- * 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".
2208
2152
  */
2209
2153
  const RETRY_MAX_DELAY_MS = 9e5;
2210
2154
  /**
2211
2155
  * How long an explicitly selected account's rate-limit mark may stand before
2212
- * its window is probed again. A 429 whose reset was never learned marks the
2213
- * key `unknown`, which {@link accountUsable} refuses forever — deliberately,
2214
- * because the all-marked revival pass is the only thing meant to clear it and
2215
- * it costs one billing call per key. That is fine for a key nobody asked for,
2216
- * and wrong for the one the user pinned: a single 429 would otherwise demote
2217
- * the user's own selection until the process restarts (issue #51). The probe
2218
- * is therefore allowed once per interval per key, which bounds an endpoint
2219
- * 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.
2220
2164
  */
2221
2165
  const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
2222
2166
  /** A labeled, human-readable clock reading for error messages. */
@@ -2235,10 +2179,10 @@ function accountUsable(state) {
2235
2179
  return false;
2236
2180
  }
2237
2181
  /**
2238
- * Pick the account that should serve now: the manually preferred slot when it
2239
- * is usable, otherwise the first usable account in rotation order; undefined
2240
- * when no account is usable. Shared by the pool (request path) and the plugin
2241
- * 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.
2242
2186
  */
2243
2187
  function selectActiveAccount(accounts, preferredId) {
2244
2188
  const usable = accounts.filter((account) => accountUsable(account.state));
@@ -2321,27 +2265,26 @@ var CommandCodeAccountPool = class {
2321
2265
  return out;
2322
2266
  }
2323
2267
  /**
2324
- * Hand out the key for a request: the model-routed account when the
2325
- * request's model matches a rule (and that account is usable), else the
2326
- * manually preferred account when usable, else the first usable account in
2327
- * rotation order. Returns `undefined` when no account resolves any key at
2328
- * all (the caller then reports the missing credential). Throws
2329
- * `RATE_LIMIT` — naming the earliest window reset — or
2330
- * `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.
2331
2276
  *
2332
2277
  * `options.model` is the request's model id; routing rules re-read per
2333
- * resolution, so a settings change applies live.
2334
- *
2335
- * `options.tried` lists the keys this request has already used — the
2336
- * just-rejected one included. They are removed from the resolution entirely,
2337
- * which is what lets one request walk a four-account pool: an account-scoped
2338
- * rejection that does not mark the key (no credits, a model outside the
2339
- * account's plan) would otherwise be offered again on every attempt, and the
2340
- * 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.
2341
2284
  *
2342
- * An explicit selection (the pin or a model rule) that a rate-limit mark
2343
- * would demote is probed before the fallback serves, so "falls back while
2344
- * 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).
2345
2288
  */
2346
2289
  async resolveKey(options) {
2347
2290
  const accounts = await this.resolvedAccounts();
@@ -2397,21 +2340,18 @@ var CommandCodeAccountPool = class {
2397
2340
  throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
2398
2341
  }
2399
2342
  /**
2400
- * Record a rejection against one key. `rate-limit` marks the key's usage
2401
- * WINDOW exhausted — as a `cooldown` until `resetAtMs` when the rejection
2402
- * body named the provider's own reset time, otherwise as an `unknown` mark
2403
- * whose window is probed lazily. `throttled` (the plain 429 that named no
2404
- * window) marks the key with the `throttle` cause instead: it leaves rotation
2405
- * exactly like a window mark does, but the pool's own diagnosis keeps saying
2406
- * "rate limited" rather than inventing an exhausted window. A 401
2407
- * (`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
2408
2350
  * changes.
2409
2351
  *
2410
- * `resetAtMs` is seconds-to-millis converted by the adapter from the
2411
- * provider's `error.rateLimit.reset`: knowing the real reset immediately is
2412
- * what keeps an exhausted account out of rotation for exactly as long as the
2413
- * provider said. It only applies to a window mark; a plain throttle keeps the
2414
- * 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.
2415
2355
  */
2416
2356
  markRejected(apiKey, rejection, resetAtMs) {
2417
2357
  if (rejection === "invalid-credential") this.states.set(apiKey, {
@@ -2443,15 +2383,12 @@ var CommandCodeAccountPool = class {
2443
2383
  * One account's key: literal → credential seam → auth file (default slot).
2444
2384
  *
2445
2385
  * Every source is normalized here, at the single point where a slot's key
2446
- * enters the pool. The adapter sends the key through the harness's
2447
- * `assertUsableApiKey()`, which trims it — a stored key from the credentials
2448
- * seam, a `.env` line, or a shell export all pick up surrounding whitespace
2449
- * — and reports that trimmed form back to `markRejected()`. Returning the
2450
- * raw value would file every 429/401 mark under a key no later lookup can
2451
- * find: rotation would re-offer the same account, the account card would show
2452
- * no mark, and the usage endpoints would 401 while chat kept working.
2453
- * Normalizing once makes resolution, probing, marking, and the request path
2454
- * 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.
2455
2392
  */
2456
2393
  async resolveSlotKey(slot) {
2457
2394
  if (slot.literal !== void 0) {
@@ -2474,12 +2411,11 @@ var CommandCodeAccountPool = class {
2474
2411
  }
2475
2412
  }
2476
2413
  /**
2477
- * The account the user explicitly asked for: the slot a model rule routes
2478
- * the request to when that id exists among the resolved slots, else the
2479
- * manually pinned slot. Consulted only on the fallback path — a usable
2480
- * routed account already returned above — so unlike
2481
- * {@link selectAccountForModel} it does NOT require the account to be
2482
- * 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.
2483
2419
  */
2484
2420
  explicitAccount(accounts, model, preferredId) {
2485
2421
  const rule = matchModelRule(model, this.deps.modelAccountRules?.());
@@ -2489,9 +2425,9 @@ var CommandCodeAccountPool = class {
2489
2425
  /**
2490
2426
  * Whether an explicitly selected account's mark is due for a window probe.
2491
2427
  * Only an `unknown` mark (a 429 whose reset was never learned) is worth
2492
- * re-probing: a `cooldown` already carries its reset time and expires by
2493
- * itself, and a `disabled` (401) key stays out until the stored credential
2494
- * 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.
2495
2431
  */
2496
2432
  canProbeExplicit(account) {
2497
2433
  if (account.state?.kind !== "unknown") return false;
@@ -2505,8 +2441,8 @@ var CommandCodeAccountPool = class {
2505
2441
  /**
2506
2442
  * Probe one explicitly selected account's window and apply the answer. A
2507
2443
  * window that is no longer exceeded drops the mark, so the user's own
2508
- * selection serves again on this very request. An exceeded one is stamped
2509
- * 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
2510
2446
  * {@link accountUsable} lets the account back in with no further probe. A
2511
2447
  * probe that fails changes nothing: the mark stays `unknown` and the next
2512
2448
  * attempt waits out the interval.
@@ -2558,22 +2494,20 @@ var CommandCodeAccountPool = class {
2558
2494
  * read a window as exceeded) → the window diagnosis, naming the earliest
2559
2495
  * known reset when the provider published one;
2560
2496
  * - every mark a plain throttle (a 429 that said nothing about a window, and
2561
- * no probe could confirm one) → a throttle diagnosis that says so. Claiming
2562
- * "all accounts have exhausted their usage window" here is issue #54: the
2563
- * report's account panel showed the five-hour window at 2% and the weekly
2564
- * 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.
2565
2501
  *
2566
- * Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
2567
- * 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.
2568
2508
  *
2569
- * The attached `providerRetryAfterMs` is the exact wait until the earliest
2570
- * known window reset, so dsh-llm-retry sleeps through the window instead of
2571
- * polling at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}:
2572
- * the executor honors a provider wait verbatim only at or below the policy's
2573
- * `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
2574
- * (normal mode), which would turn "poll until the window opens" into "fail
2575
- * now". Longer resets simply ride the capped local backoff and the probe
2576
- * 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.
2577
2511
  */
2578
2512
  function allAccountsUnusable(accounts) {
2579
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");
@@ -2616,14 +2550,14 @@ const REQUEST_IMAGE_MAX_LONG_EDGE = 1568;
2616
2550
  const REQUEST_IMAGE_MAX_ENCODED_BYTES = 1048576;
2617
2551
  /**
2618
2552
  * Aspect-preserving projection onto a long-edge budget; never enlarges, and
2619
- * never returns a zero dimension (a 1-px degenerate side is what the round of
2620
- * an extreme aspect ratio can produce, and a 0 would make the encoder refuse).
2553
+ * never returns a zero dimension.
2621
2554
  *
2622
2555
  * `dsh-attachment` exports an equivalent `longEdgeDimensions`, but it does not
2623
2556
  * guard its inputs: a reference whose declared size is 0 or NaN — a malformed
2624
2557
  * or fabricated attachment record — would project to a zero-sized target and
2625
- * make the encoder refuse the whole request. The guard is the reason this stays
2626
- * 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.
2627
2561
  */
2628
2562
  function longEdgeDimensions(width, height, longEdge) {
2629
2563
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return {
@@ -2667,16 +2601,13 @@ function requestImageTarget(ref) {
2667
2601
  * that the heuristic prices at ~0 tokens each, so context pressure is
2668
2602
  * under-reported and the session walks into its own context window.
2669
2603
  *
2670
- * This route is a gateway in front of many upstreams — one catalog id per model,
2671
- * but the visual-token rule belongs to whoever serves it. So the estimate is per
2672
- * FAMILY, using each vendor's published rule, and the families are recognised by
2673
- * the catalog id (which names the vendor). Every formula is stated here rather
2674
- * than imported: the only in-tree implementations live inside the official
2675
- * adapters, and this plugin deliberately does not depend on those protocols.
2676
- *
2677
- * These are ESTIMATES for context accounting, not invoices — the provider's own
2678
- * usage remains authoritative for a completed request, exactly as the published
2679
- * 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.
2680
2611
  *
2681
2612
  * @module dsh-commandcode-provider/image-tokens
2682
2613
  */
@@ -2826,14 +2757,11 @@ function scaleBy(size, factor) {
2826
2757
  * Opt-in raw-stream trace for the Command Code transports.
2827
2758
  *
2828
2759
  * Why this exists: this route's failure mode of record is a response that ENDS
2829
- * EARLY. The gateway, an intermediary proxy, or the network closes the socket
2830
- * mid-generation, so the adapter's read loop sees `done` without ever seeing
2831
- * the transport's terminal event. From inside the adapter those two endings are
2832
- * indistinguishable — the provider cut the stream, a proxy timed out, or the
2833
- * socket was dropped — and the only artifact that separates them is the raw
2834
- * wire: which events actually arrived, when the last one landed, how long the
2835
- * socket then sat silent, and how it ended. Without that, a truncated answer is
2836
- * 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.
2837
2765
  *
2838
2766
  * Enable it per process:
2839
2767
  *
@@ -2843,24 +2771,25 @@ function scaleBy(size, factor) {
2843
2771
  * ```
2844
2772
  *
2845
2773
  * Any value that is not a flag is a path, and the explicit off-spellings
2846
- * (`0`, `false`, `no`, `off`) are recognized as "off" rather than being taken
2847
- * for a file name.
2774
+ * (`0`, `false`, `no`, `off`) are read as "off" rather than as a file name.
2848
2775
  *
2849
2776
  * Every model request appends one JSONL record per raw chunk plus lifecycle
2850
- * records (`response`, `eof`, `idle-timeout`, `read-error`, `end`,
2851
- * `stream-close`), each stamped with `at` — milliseconds since that request's
2852
- * first byte. The chunk records carry the provider's own payload verbatim
2853
- * (bounded per chunk), because a truncated answer is exactly what is being
2854
- * diagnosed: treat the file as conversation content, not as a log safe to
2855
- * share. It never carries the request body, an API key, or any request header.
2856
- *
2857
- * Deliberate trade-offs:
2858
- * - OFF costs one `process.env` read per request.
2859
- * - ON appends synchronously, one file open per chunk. The trace must survive
2860
- * a host killed mid-diagnosis, and the microseconds it costs are irrelevant
2861
- * beside the seconds-scale silences it is there to measure.
2862
- * - A trace that cannot be written turns itself off instead of failing the
2863
- * 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.
2864
2793
  *
2865
2794
  * @module
2866
2795
  */
@@ -2883,7 +2812,7 @@ const FLAG_VALUES = /* @__PURE__ */ new Set([
2883
2812
  * A user turning the trace off writes `false` as readily as they write `1` to
2884
2813
  * turn it on, and without this set every one of these spellings fell through to
2885
2814
  * "…so it must be a path" — the trace switched ON and wrote the conversation to
2886
- * 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.
2887
2816
  */
2888
2817
  const OFF_VALUES = /* @__PURE__ */ new Set([
2889
2818
  "0",
@@ -2935,6 +2864,7 @@ function openStreamTrace(scope, options = {}) {
2935
2864
  if (path === void 0) return OFF;
2936
2865
  const now = options.now ?? Date.now;
2937
2866
  const startedAt = now();
2867
+ const streamId = randomUUID();
2938
2868
  let written = 0;
2939
2869
  let stopped = false;
2940
2870
  let capped = false;
@@ -2954,6 +2884,7 @@ function openStreamTrace(scope, options = {}) {
2954
2884
  line = `${JSON.stringify({
2955
2885
  at: now() - startedAt,
2956
2886
  scope,
2887
+ streamId,
2957
2888
  ...record
2958
2889
  })}\n`;
2959
2890
  } catch {
@@ -2967,6 +2898,7 @@ function openStreamTrace(scope, options = {}) {
2967
2898
  line = `${JSON.stringify({
2968
2899
  at: now() - startedAt,
2969
2900
  scope,
2901
+ streamId,
2970
2902
  event: "trace-capped",
2971
2903
  bytes: written
2972
2904
  })}\n`;
@@ -2978,9 +2910,14 @@ function openStreamTrace(scope, options = {}) {
2978
2910
  stopped = true;
2979
2911
  }
2980
2912
  };
2981
- write({ event: "stream-open" });
2913
+ write({
2914
+ event: "stream-open",
2915
+ startedAt
2916
+ });
2982
2917
  return {
2983
- enabled: true,
2918
+ get enabled() {
2919
+ return !stopped && !capped;
2920
+ },
2984
2921
  record(event, fields) {
2985
2922
  write(fields === void 0 ? { event } : {
2986
2923
  event,
@@ -3004,20 +2941,17 @@ function openStreamTrace(scope, options = {}) {
3004
2941
  * community-maintained integration; you need your own Command Code account
3005
2942
  * and API key or subscription, and Command Code's terms apply.
3006
2943
  *
3007
- * Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
3008
- * re-verified through command-code@1.65.2):
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 | cache-write-tokens | 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.2";
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();
@@ -4203,13 +4252,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
4203
4252
  * unbounded loop): `RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`/
4204
4253
  * `EMPTY_RESPONSE` retry up to 1000 times with waits doubling from 500 ms
4205
4254
  * and capping at 15 minutes (±10% jitter), so an exhausted 5-hour window
4206
- * recovers in-session instead of failing after two tries. Permanent
4207
- * failures (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`,
4208
- * plan rejections) are absent from the whitelist and surface immediately
4209
- * instead of looping. Waits the pool/adapter attach as
4210
- * `providerRetryAfterMs` are honored verbatim at or below the 15-minute
4211
- * cap and never attached above it (in normal mode a longer attached wait
4212
- * 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).
4213
4262
  *
4214
4263
  * Captured once at route registration (dsh-llm snapshots this value), so a
4215
4264
  * future config knob for it would apply on profile restart, not per request.
@@ -4240,12 +4289,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4240
4289
  * session reports far less context than it is actually carrying. The price is
4241
4290
  * computed at the dimensions this route would send (the same request target
4242
4291
  * `readImageRequest` encodes to), so the estimate tracks the wire rather than
4243
- * the stored original.
4244
- *
4245
- * Both payload generations are handled, because they disagree: ≤0.1.5 hands
4246
- * over bare `ImageAttachmentRef`s, ≥0.1.6 hands over `ImageBlock`s that also
4247
- * carry the surface's `offloaded` mark. Returning a price per occurrence, in
4248
- * 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.
4249
4294
  */
4250
4295
  imageRequestPricing(_provider, model) {
4251
4296
  const imageCapable = KNOWN_IMAGE_MODELS.has(model);
@@ -4331,10 +4376,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
4331
4376
  }
4332
4377
  /**
4333
4378
  * Fetch one account endpoint and parse its JSON body. Returns the HTTP
4334
- * status alongside the parsed record so each caller applies its own
4335
- * failure accounting: the billing probe fails open silently, the usage
4336
- * report books failures per endpoint. Non-2xx and invalid JSON bodies come
4337
- * 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.
4338
4383
  *
4339
4384
  * `timeoutMs` is the caller's budget for this one request. The default is the
4340
4385
  * CATALOG probe's short cap, which fits the picker's fail-open reads; the
@@ -4371,9 +4416,6 @@ var CommandCodeAdapter = class extends LlmAdapter {
4371
4416
  * per picker load). `undefined` means the filter cannot be evaluated at all
4372
4417
  * — no key resolved — which {@link modelVisibleForAnyAccount} reads as
4373
4418
  * "show everything".
4374
- *
4375
- * The serving account is only the first entry: with several accounts the
4376
- * model list must not depend on which of them rotation happens to be using.
4377
4419
  */
4378
4420
  async loadPoolBillingAccess() {
4379
4421
  const keys = await this.poolAccountKeys();
@@ -4486,10 +4528,9 @@ var CommandCodeAdapter = class extends LlmAdapter {
4486
4528
  * but a hard refusal. `/alpha/generate` routes every one of them, so this
4487
4529
  * costs nothing.
4488
4530
  *
4489
- * That decision is deliberately NOT written to `protocolCache`. The cache is
4490
- * keyed by API key alone, so remembering it would pin the whole ACCOUNT to
4491
- * the CLI transport and drag every other model — DeepSeek, GLM, Qwen, all of
4492
- * 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.
4493
4534
  */
4494
4535
  resolveProtocol(apiKey, model) {
4495
4536
  const forced = this.deps.options().protocol;
@@ -4515,18 +4556,14 @@ var CommandCodeAdapter = class extends LlmAdapter {
4515
4556
  * the default resolves the currently active account.
4516
4557
  *
4517
4558
  * Two failure modes are NOT "the network is down", and the report must say
4518
- * so rather than let {@link classifyTotalFailure} blame the connection:
4519
- *
4520
- * - A key that no HTTP header can carry (a stray newline from a paste, a
4521
- * full-width character) makes `fetch` throw a `TypeError` **before any
4522
- * I/O**, for every endpoint at once. The chat path refuses such a key
4523
- * through `assertUsableApiKey`; this path must too, and it answers
4524
- * `blocked: 'invalid-key'` instead of four phantom transport failures.
4525
- * - The four endpoints get the SAME per-request budget as a chat call
4526
- * (`connection.requestTimeoutMs`, default 60 s), not the catalog's 10 s
4527
- * probe budget: on a slow link a 10 s cap made every account query time
4528
- * out while chat kept working, which is exactly a "check your network"
4529
- * 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.
4530
4567
  */
4531
4568
  async getUsage(apiKey) {
4532
4569
  const connection = this.deps.options();
@@ -4604,17 +4641,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
4604
4641
  * the same time (the two are metered separately), and reading only the
4605
4642
  * shorter one revived such an account as "usable" — the pool then handed it
4606
4643
  * out, the provider rejected the request again, and the next all-marked pass
4607
- * revived it once more, so a used-up account kept coming back (issue #51's
4608
- * follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
4609
- * published window is exceeded, and `resetAt` is the LATEST reset among the
4610
- * exceeded ones — the binding constraint, because a clear five-hour window
4611
- * 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.
4612
4649
  *
4613
4650
  * Returns `undefined` when the probe itself failed (transport, non-200, or a
4614
4651
  * payload with no window limits at all) — a failed probe never changes pool
4615
- * state. The monthly/purchased/free credit balances are deliberately NOT part
4616
- * of this answer: they are balances rather than windows, they carry no reset
4617
- * 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.
4618
4654
  */
4619
4655
  async probeWindowLimits(apiKey) {
4620
4656
  try {
@@ -4680,73 +4716,110 @@ var CommandCodeAdapter = class extends LlmAdapter {
4680
4716
  readImage
4681
4717
  });
4682
4718
  let body = await buildBody(protocol);
4719
+ const trace = openStreamTrace(`${options.provider}/${options.model}`);
4683
4720
  const tried = /* @__PURE__ */ new Set();
4721
+ let attemptNumber = 0;
4722
+ let responseMetadata = {
4723
+ protocol,
4724
+ headers: {}
4725
+ };
4684
4726
  const connectDeps = {
4685
4727
  options,
4686
4728
  connection,
4687
- 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
+ }
4688
4746
  };
4689
4747
  let connected;
4690
- for (;;) {
4691
- tried.add(apiKey);
4692
- const attempt = await connectGenerate(connectDeps, apiKey, protocol, body);
4693
- if ("response" in attempt) {
4694
- connected = attempt;
4695
- break;
4696
- }
4697
- if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText)) {
4698
- protocol = "cli";
4699
- this.rememberProtocol(apiKey, true);
4700
- body = await buildBody("cli");
4701
- continue;
4702
- }
4703
- if (attempt.status === 413 && REQUEST_IMAGE_BUDGETS.length > 1) {
4704
- const missing = this.surfaceOffload.requiredImageOffload(requestOptions.messages, coreBudget(REQUEST_IMAGE_BUDGETS[1]), imageVersionBytes);
4705
- if (missing > 0) throw imageOffloadRequired(missing);
4706
- }
4707
- const rotate = this.deps.rotateApiKey;
4708
- const rejection = classifyAccountRejection(attempt.status, attempt.errText);
4709
- if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4710
- const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4711
- tried: [...tried],
4712
- ...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)
4713
4760
  });
4714
- if (next !== void 0 && !tried.has(next)) {
4715
- 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");
4716
4770
  continue;
4717
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);
4718
4796
  }
4719
- if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4720
- const reset = rejection.resetAtMs;
4721
- const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4722
- const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4723
- const window = rejection.reason === "rate-limit";
4724
- 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);
4725
- }
4726
- 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);
4727
4804
  }
4728
4805
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
4729
4806
  const { response, cleanup } = connected;
4730
4807
  if (!response.body) {
4731
4808
  cleanup();
4732
- 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);
4733
4815
  }
4734
4816
  const reader = response.body.getReader();
4735
4817
  const decoder = new TextDecoder();
4736
4818
  let buffer = "";
4737
- const trace = openStreamTrace(`${options.provider}/${options.model}`);
4738
4819
  const streamStartedAt = Date.now();
4739
4820
  let chunkCount = 0;
4740
4821
  let totalBytes = 0;
4741
4822
  let lastChunkAt = streamStartedAt;
4742
- trace.record("response", {
4743
- protocol,
4744
- endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
4745
- status: response.status,
4746
- attempts: tried.size,
4747
- maxTokens,
4748
- ...reasoningEffort === void 0 ? {} : { reasoningEffort }
4749
- });
4750
4823
  let idleTimer;
4751
4824
  let idleFired = false;
4752
4825
  const armIdle = () => {
@@ -4768,6 +4841,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
4768
4841
  let usageEmitted = false;
4769
4842
  let endRecorded = false;
4770
4843
  function* handle(event) {
4844
+ captureResponseIdentifiers(responseMetadata, event);
4771
4845
  for (const chunk of handleEvent(asm, protocol, event)) if (chunk.type === "finish") finish = chunk;
4772
4846
  else if (chunk.type === "usage") usage = chunk.usage;
4773
4847
  else yield chunk;
@@ -4838,11 +4912,15 @@ var CommandCodeAdapter = class extends LlmAdapter {
4838
4912
  chunks: chunkCount,
4839
4913
  bytes: totalBytes,
4840
4914
  totalMs: Date.now() - streamStartedAt,
4841
- sawContent: asm.sawContent
4915
+ sawContent: asm.sawContent,
4916
+ responseMetadata
4842
4917
  });
4843
4918
  endRecorded = true;
4844
4919
  if (failure !== void 0) throw failure;
4845
- yield finish;
4920
+ yield hasResponseIdentifiers(responseMetadata) ? {
4921
+ ...finish,
4922
+ replayState: { response: responseMetadata }
4923
+ } : finish;
4846
4924
  } else {
4847
4925
  const elapsed = Date.now() - streamStartedAt;
4848
4926
  const detail = `${chunkCount} chunk(s), ${totalBytes} bytes, ${elapsed}ms`;
@@ -4851,7 +4929,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
4851
4929
  chunks: chunkCount,
4852
4930
  bytes: totalBytes,
4853
4931
  totalMs: elapsed,
4854
- sawContent: asm.sawContent
4932
+ sawContent: asm.sawContent,
4933
+ responseMetadata
4855
4934
  });
4856
4935
  endRecorded = true;
4857
4936
  if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
@@ -4871,9 +4950,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
4871
4950
  sawContent: asm.sawContent,
4872
4951
  chunks: chunkCount,
4873
4952
  bytes: totalBytes,
4874
- totalMs: Date.now() - streamStartedAt
4953
+ totalMs: Date.now() - streamStartedAt,
4954
+ responseMetadata
4875
4955
  });
4876
- throw error;
4956
+ throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
4877
4957
  } finally {
4878
4958
  clearIdle();
4879
4959
  cleanup();
@@ -4890,16 +4970,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
4890
4970
  * status alone cannot distinguish them. The status also decides the failure
4891
4971
  * CODE via {@link httpErrorCode}, which is what the retry policy routes on —
4892
4972
  * a transient 5xx must not be reported as a permanent provider rejection. A
4893
- * 429's `Retry-After` header rides along as
4894
- * `providerRetryAfterMs` so dsh-llm-retry can wait exactly that long instead
4895
- * of guessing at the backoff cadence — capped at RETRY_MAX_DELAY_MS, because
4896
- * in normal mode a longer attached wait makes the executor abandon the retry
4897
- * 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.
4898
4977
  *
4899
4978
  * One body is read, not just its status: a client-side rejection whose
4900
4979
  * `error.code`/`type`/`message` names the model context window is reported as
4901
- * `CONTEXT_WINDOW_EXCEEDED` (see the branch below), because that failure has a
4902
- * 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.
4903
4983
  */
4904
4984
  function generateHttpError(status, errText, retryAfterMs) {
4905
4985
  let providerCode;
@@ -4932,16 +5012,11 @@ function generateHttpError(status, errText, retryAfterMs) {
4932
5012
  * status, never the provider's `error.type` — so a status that arrives here as
4933
5013
  * a permanent code is never retried, whatever it says about itself.
4934
5014
  *
4935
- * 5xx is the provider's own "we are temporarily unavailable" class (Cloudflare's
4936
- * 520-527 included; a 520 body says exactly that in words) and the only sane
4937
- * answer to it is the byte-identical resend the retry policy exists to make.
4938
- * Keeping it on `PROVIDER_HTTP_ERROR` — absent from the whitelist by design, so
4939
- * that a 403 plan rejection or a 400 shape error fails fast — made the one
4940
- * failure that asks to be retried the only one that never was, while the same
4941
- * status arriving as an in-band stream `error` event was classified retryable
4942
- * `SERVER` (see the `error` case of {@link handleCliEvent}). 408 is the same
4943
- * story for the request itself. Everything else stays permanent: a 400/403/404/
4944
- * 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.
4945
5020
  */
4946
5021
  function httpErrorCode(status) {
4947
5022
  if (status === 429) return "RATE_LIMIT";
@@ -5206,12 +5281,6 @@ function emptyCompletionError(reason, maxTokens, usage) {
5206
5281
  return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
5207
5282
  }
5208
5283
  /**
5209
- * True when a Provider API pre-stream rejection is Command Code's Go-plan
5210
- * gate (`upgrade_required`). Only this exact class of rejection should fall
5211
- * back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
5212
- * ordinary errors so real account/model problems are not masked.
5213
- */
5214
- /**
5215
5284
  * Classify a pre-stream rejection that is about the ACCOUNT rather than about
5216
5285
  * the request, using the official CLI's own rules (command-code@1.56.0:
5217
5286
  * `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
@@ -5221,46 +5290,31 @@ function emptyCompletionError(reason, maxTokens, usage) {
5221
5290
  * oversized body or a context overflow must fail fast instead of walking the
5222
5291
  * whole account pool.
5223
5292
  *
5224
- * Issue #51's follow-up is why this exists. Only 429/401 used to rotate, so
5225
- * every other account-scoped rejection was terminal AND invisible to the pool:
5226
- * an account that could not pay, or could not run the model, stayed "usable",
5227
- * was handed out again on the next request, and produced the same error again
5228
- * ("切到一个不可用账号后就报 400,还挺频繁"). The three reasons differ in what
5229
- * the host should do with them, which is the caller's decision, not this
5230
- * 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:
5231
5299
  *
5232
- * - `rate-limit`: a usage WINDOW is spent. The code `RATE_LIMITED` is the
5233
- * authoritative signal — the CLI accepts the code OR a 429 status — and the
5234
- * body's `error.rateLimit` names the window and its `reset` (in seconds),
5235
- * so the host can hold the account out until the provider's own reset time.
5236
- * - `throttled`: the provider refused the request (429 or a bare
5237
- * `RATE_LIMITED` code) without naming a usage window. It is account-scoped
5238
- * like the above, so it rotates and marks the key the same way, but it is
5239
- * NOT evidence that a metered window is spent: the CLI's own
5240
- * `parseWindowLimitError` returns a window limit only when
5241
- * `resolveWindowLabel` can name one, and a burst/model/spend limiter the
5242
- * billing endpoint cannot see is exactly this shape (issue #54's report: a
5243
- * 429 whose probe read five-hour 2% and weekly 10% while the error claimed
5244
- * both were exhausted).
5245
- * - `invalid-credential`: 401; the key itself is bad.
5246
- * - `unavailable`: the key is fine but this account cannot serve: no credits
5247
- * (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
5248
- * `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
5249
- * or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
5250
- * the account: the fact is about the model or the balance, it can change
5251
- * without the key changing, and a `:free` model can still be served by an
5252
- * account with an empty balance. These codes are recognized on EVERY
5253
- * status — a 5xx that proxies the provider's own credits/plan body is
5254
- * 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.
5255
5309
  */
5256
5310
  /**
5257
5311
  * The provider's structured "this account cannot serve this request" codes.
5258
5312
  *
5259
5313
  * Status-independent on purpose, mirroring the CLI's classifier, which reads
5260
- * the code before any status guard. A 5xx that carries one of these must not
5261
- * be retried as a transient provider failure: the same request against the
5262
- * same account cannot succeed, so the caller rotates past it (without marking
5263
- * 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.
5264
5318
  */
5265
5319
  const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
5266
5320
  "USAGE_EXCEEDED",
@@ -5310,12 +5364,12 @@ const MAX_TRUSTED_RESET_MS = 2592e6;
5310
5364
  * `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
5311
5365
  *
5312
5366
  * `window` is present only when the body actually names one: a
5313
- * `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the
5314
- * "usage limit for your plan" wording (read as `weekly` when it says so, else
5315
- * `fiveHour`). That is the CLI's own bar for calling a rejection a WINDOW
5316
- * limit, and it is what keeps a bare 429 — a burst limiter, a model-level
5317
- * limit, a spend cap `windowLimits` cannot show — from being reported as an
5318
- * 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).
5319
5373
  *
5320
5374
  * `resetAtMs` is the reset the body publishes: `error.rateLimit.reset` is
5321
5375
  * SECONDS (`1e3 * reset`, like the CLI), and a `resets at <ISO>` stamp in the
@@ -5346,6 +5400,13 @@ function readWindowLimitEvidence(errText) {
5346
5400
  ...resetAtMs === void 0 ? {} : { resetAtMs }
5347
5401
  };
5348
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
+ */
5349
5410
  function isUpgradeRequiredError(status, errText) {
5350
5411
  if (status !== 403) return false;
5351
5412
  const lower = errText.toLowerCase();
@@ -5361,7 +5422,9 @@ function isUpgradeRequiredError(status, errText) {
5361
5422
  const message = stringValue(error.message) ?? "";
5362
5423
  if (message.toLowerCase().includes("go plan") && message.toLowerCase().includes("api access")) return true;
5363
5424
  }
5364
- } catch {}
5425
+ } catch {
5426
+ return false;
5427
+ }
5365
5428
  return false;
5366
5429
  }
5367
5430
  /** Map an OpenAI Chat Completions usage payload to harness disjoint counts. */
@@ -5414,7 +5477,6 @@ const commandcodeCommand = {
5414
5477
  weeklyLine: " 📅 每周 {used} / {cap}{warn}",
5415
5478
  windowBarLine: " └ {bar} 重置 {when}",
5416
5479
  exceededWarning: " ⚠️ 超限!",
5417
- resetSuffix: "重置 {when}",
5418
5480
  partialFailures: "⚠️ 部分端点失败: {list}",
5419
5481
  noData: "(无数据 — 请检查 API 密钥)",
5420
5482
  errorText: "获取 Command Code 用量失败:{message}"
@@ -5446,25 +5508,19 @@ const commandcodeCommand = {
5446
5508
  weeklyLine: " 📅 Weekly {used} / {cap}{warn}",
5447
5509
  windowBarLine: " └ {bar} resets {when}",
5448
5510
  exceededWarning: " ⚠️ exceeded!",
5449
- resetSuffix: "resets {when}",
5450
5511
  partialFailures: "⚠️ some endpoints failed: {list}",
5451
5512
  noData: "(no data — check your API key)",
5452
5513
  errorText: "Could not fetch Command Code usage: {message}"
5453
5514
  }
5454
5515
  };
5455
5516
  /**
5456
- * Resolve the active locale for a Host-side command run.
5457
- *
5458
- * Priority: explicit `override` (from `Config.lang`) → `LC_ALL` → `LANG` →
5459
- * the conventional fallback (`'zh'`, matching the existing single-language
5460
- * 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.
5461
5520
  *
5462
- * The values are matched on the leading tag only — `zh_CN.UTF-8`,
5463
- * `zh-Hans`, `zh` all map to `'zh'`; everything starting with `en` maps to
5464
- * `'en'`; anything else falls back to `'zh'` (a non-`en` shell that
5465
- * already has Chinese in the terminal is the closest sensible default;
5466
- * a Western shell that happens to be neither keeps the existing Chinese
5467
- * 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.
5468
5524
  */
5469
5525
  function pickCommandLocale(override, env = process.env) {
5470
5526
  if (override === "zh" || override === "en") return override;
@@ -5477,11 +5533,10 @@ function commandCopy(locale, key) {
5477
5533
  }
5478
5534
  //#endregion
5479
5535
  //#region src/commands.ts
5480
- /** Format a dollar amount. */
5481
5536
  function money(value) {
5482
5537
  return `$${value.toFixed(4)}`;
5483
5538
  }
5484
- /** Format a dollar amount compactly (2 decimals). */
5539
+ /** Compact dollar amount (2 decimals), for the summary and window rows. */
5485
5540
  function moneyShort(value) {
5486
5541
  return `$${value.toFixed(2)}`;
5487
5542
  }
@@ -5493,14 +5548,14 @@ function tokensCompact(value) {
5493
5548
  return String(value);
5494
5549
  }
5495
5550
  /**
5496
- * Format a success-rate percentage (already in percent units): at most two
5497
- * 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
5498
5553
  * `./client/usage.ts`, which the settings card uses.
5499
5554
  */
5500
5555
  function successRateText(value) {
5501
5556
  return String(Number(value.toFixed(2)));
5502
5557
  }
5503
- /** 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. */
5504
5559
  function resetLabel(ms) {
5505
5560
  if (ms <= 0) return "n/a";
5506
5561
  return new Date(ms).toLocaleString();
@@ -5604,16 +5659,16 @@ const REMOTE_SERVICE = "commandcodeUsage";
5604
5659
  /** The wire namespace all Command Code endpoints share. */
5605
5660
  const REMOTE_NAMESPACE = "commandcode";
5606
5661
  /**
5607
- * Build the validator helpers one endpoint uses. `prefix` names the
5608
- * endpoint in the rejection message (e.g. `commandcode/report result:`), so
5609
- * each wire file keeps its own diagnostic phrasing while sharing the helper
5610
- * 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.
5611
5665
  *
5612
5666
  * The helpers return the reject call directly in the failure branch: since
5613
5667
  * `reject` is typed `never`, the ternary's union collapses to the success type
5614
5668
  * without relying on TypeScript's control-flow analysis of a never-returning
5615
5669
  * call (which only recognizes function declarations, not the destructured
5616
- * 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.
5617
5672
  */
5618
5673
  function makeBoundaryValidator(prefix) {
5619
5674
  const reject = (field) => {
@@ -5641,10 +5696,8 @@ function makeStrictCodec(typeSymbol, schema) {
5641
5696
  }
5642
5697
  /**
5643
5698
  * Build one strict invocation descriptor. Every Command Code Remote shares the
5644
- * `commandcode` namespace, the `commandcodeUsage` service, and a strict
5645
- * `mode: 'strict'` result — only the endpoint, method, result type symbol, and
5646
- * schema differ — so the boilerplate lives here once and each endpoint supplies
5647
- * 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.
5648
5701
  */
5649
5702
  function makeRemoteDescriptor(endpoint, method, typeSymbol, schema) {
5650
5703
  return {
@@ -5668,7 +5721,6 @@ const USAGE_REPORT_ENDPOINT = "commandcode/report";
5668
5721
  * so rejection messages name the offending boundary.
5669
5722
  */
5670
5723
  const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
5671
- /** Validate one window-limit block (`fiveHour` / `weekly`). */
5672
5724
  /**
5673
5725
  * Parse one quota window, or undefined when the frame carries no such window.
5674
5726
  *
@@ -5969,12 +6021,10 @@ var CommandCodeUsageService = class extends TypertRemoteService {
5969
6021
  this.deps = deps;
5970
6022
  }
5971
6023
  /**
5972
- * Account, usage, and credit state for the settings page's account card —
5973
- * one entry per pool account when the plugin entry wired `reports`, a
5974
- * single default-account entry otherwise. Degrades per endpoint like the
5975
- * `/commandcode` command (failures land in `report.failures`); throws
5976
- * `MISSING_CREDENTIAL` when no key resolves, which the Gateway folds into
5977
- * 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.
5978
6028
  */
5979
6029
  async report() {
5980
6030
  if (this.deps.reports !== void 0) return this.deps.reports();
@@ -5989,20 +6039,19 @@ var CommandCodeUsageService = class extends TypertRemoteService {
5989
6039
  }] };
5990
6040
  }
5991
6041
  /**
5992
- * The full model catalog for the settings page's model editors (the
5993
- * routing-rule editor and the visible-models filter). The browser never
5994
- * calls the Command Code API directly — the Host serves the catalog
5995
- * (already fetched/cached by the adapter) so models can be picked from
5996
- * 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.
5997
6046
  */
5998
6047
  async models() {
5999
6048
  return this.deps.listModels?.() ?? { models: [] };
6000
6049
  }
6001
6050
  /**
6002
6051
  * The model price table the composer prices an in-progress session with.
6003
- * Static vendored data (the official pricing page's rates), served Host-side
6004
- * so the browser bundle never carries a copy that could drift from the
6005
- * 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.
6006
6055
  */
6007
6056
  async prices() {
6008
6057
  return (this.deps.prices ?? modelPriceTable)();
@@ -6010,8 +6059,7 @@ var CommandCodeUsageService = class extends TypertRemoteService {
6010
6059
  /**
6011
6060
  * Start (or rejoin) a browser-login attempt and return its fresh status —
6012
6061
  * `waiting` carrying the Studio URL. Rejects when the flow cannot start
6013
- * (no free loopback port, disposed plugin); the Gateway folds the throw
6014
- * into the failure branch the page renders.
6062
+ * (no free loopback port, disposed plugin).
6015
6063
  */
6016
6064
  async loginBegin(targetRef) {
6017
6065
  return this.requireLogin().begin(targetRef);
@@ -6054,11 +6102,8 @@ function applyUsageRemote(ctx, deps) {
6054
6102
  //#endregion
6055
6103
  //#region src/login.ts
6056
6104
  /**
6057
- * Host half of the Command Code browser login (the loopback flow).
6058
- *
6059
- * Mirrors what the official `command-code login` CLI command performs
6060
- * (reverse-engineered from `command-code@1.32.1`, `createAuthFlowController`
6061
- * + `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.
6062
6107
  *
6063
6108
  * 1. Bind a temporary HTTP server on `127.0.0.1`, first available port from
6064
6109
  * 5959 upward (10 attempts).
@@ -6070,12 +6115,12 @@ function applyUsageRemote(ctx, deps) {
6070
6115
  * 4. The delivered key is validated against `GET {apiBase}/alpha/whoami`
6071
6116
  * before anything is stored.
6072
6117
  *
6073
- * Server behaviour is mirrored exactly: POST-only `/callback`, a 10 KB body
6074
- * cap, JSON responses (`{success:true}` / `{success:false,error}`), CORS for
6075
- * the Studio origins only, and state-token equality as the anti-forgery
6076
- * check. One deliberate hardening over the CLI build: the CORS origin is
6077
- * echoed only when it is allowlisted (the CLI falls back to the first
6078
- * 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.
6079
6124
  *
6080
6125
  * Storage stays out of this module: the plugin entry supplies
6081
6126
  * {@link CommandCodeLoginFlowDeps.storeKey}, which writes through the dsh
@@ -6185,16 +6230,15 @@ var CommandCodeLoginFlow = class {
6185
6230
  * asynchronously (`complete()`), and that window is open to a cancel or a
6186
6231
  * fresh `begin()`; the generation lets a late completion recognize that it
6187
6232
  * no longer owns the status face and stop instead of storing a credential
6188
- * the user cancelled and flipping the page back to success.
6233
+ * the user cancelled.
6189
6234
  */
6190
6235
  attemptSeq = 0;
6191
6236
  /**
6192
- * The start currently binding a port, if any. Every `begin()` that arrives
6193
- * before it settles joins it: two independent starts would each bind their
6194
- * own loopback server, and only the LAST one is reachable by
6195
- * {@link CommandCodeLoginFlow.teardown} — the orphan keeps listening and
6196
- * answering `/callback` for the process's lifetime, and ten of them exhaust
6197
- * 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.
6198
6242
  */
6199
6243
  starting;
6200
6244
  /** A live attempt's destination; different rows may not rejoin it. */
@@ -6548,20 +6592,15 @@ function corsOrigin(origin) {
6548
6592
  //#endregion
6549
6593
  //#region src/web-search.ts
6550
6594
  /**
6551
- * dsh-commandcode-provider — Command Code web search provider over `ctx.web`.
6595
+ * Command Code web-search provider over `ctx.web`.
6552
6596
  *
6553
- * 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
6554
6598
  * `{ query, numResults, allowedDomains?, blockedDomains? }` to
6555
6599
  * `{apiBase}/alpha/web-search` and reads `{ results: [{ title, url, snippet }] }`
6556
- * back. It authenticates with the SAME `Authorization: Bearer <key>` header and
6557
- * `x-command-code-version` the model adapter uses, so this provider reuses the
6558
- * plugin's existing credential chain (`COMMANDCODE_API_KEY` → credentials seam →
6559
- * `~/.commandcode/auth.json`) — no separate DeepSeek key, no extra endpoint.
6560
- *
6561
- * This mirrors the host-side `@deepseek-ai/dsh-web-search-deepseek` provider in
6562
- * shape: a cordis-free class registered into the web seam, resolving its key per
6563
- * search, mapping each server-side result to the harness's normalized
6564
- * `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.
6565
6604
  *
6566
6605
  * @module dsh-commandcode-provider/web-search
6567
6606
  */
@@ -6569,12 +6608,9 @@ function corsOrigin(origin) {
6569
6608
  const COMMANDCODE_SEARCH_PROVIDER_ID = "commandcode";
6570
6609
  /**
6571
6610
  * The factory-declared search provider id dsh ships by default (from
6572
- * `dsh-base`'s cordis patch `web.config.searchProvider`). Kept as a
6573
- * documented reference only: disabling this plugin's `webSearch` toggle
6574
- * restores the previously selected backend (see
6575
- * {@link applyCommandCodeSearchSelection}) — it never forces this default,
6576
- * because forcing it is what used to silence sibling search plugins such as
6577
- * 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).
6578
6614
  */
6579
6615
  const DEFAULT_WEB_SEARCH_PROVIDER_ID = "deepseek-official";
6580
6616
  /** Fresh selection state: the plugin starts out not owning the selection. */
@@ -6590,24 +6626,15 @@ function commandCodeSearchSelection() {
6590
6626
  * providers (issue #26).
6591
6627
  *
6592
6628
  * - Enabling writes `commandcode` and remembers whatever it displaced. When
6593
- * the plugin already owns the selection (e.g. a settings save while still
6594
- * on), the original `displaced` value is kept — the field currently holds
6595
- * our own id, which must never be mistaken for the user's backend.
6596
- * - Disabling hands the selection back to the remembered backend. When the
6597
- * state holds no memory (a fresh boot straight into `webSearch: false`),
6598
- * the field is left alone: the runtime's current value — a sibling's
6599
- * cordis pin such as `searchProvider: modsearch`, or unset for
6600
- * auto-select — already says what the user wants.
6601
- * - When the field already reads `commandcode` at first touch (e.g. a
6602
- * surviving runtime the plugin did not set, or a manual
6603
- * `searchProvider: commandcode` pin), `displaced` stays undefined and
6604
- * `preexisting` is set, so the later disable is a no-op rather than a guess
6605
- * at the factory default. A "no-op" means the field is left ALONE: writing
6606
- * that `undefined` back would destroy the user's own pin, and dsh-web reads
6607
- * a cleared `searchProviderId` as auto-select — where a second usable
6608
- * provider (the shipped `deepseek-official` is usable whenever a DeepSeek
6609
- * key resolves) makes EVERY later search throw
6610
- * `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`.
6611
6638
  *
6612
6639
  * Never throws: like the low-level rewrite, a hardened runtime shape degrades
6613
6640
  * to registered-but-unselected.
@@ -6661,7 +6688,7 @@ function toSource(result) {
6661
6688
  ...snippet !== void 0 && snippet.length > 0 ? { snippet } : {}
6662
6689
  };
6663
6690
  }
6664
- function isAbortError$1(error) {
6691
+ function isAbortError(error) {
6665
6692
  return error instanceof DOMException && error.name === "AbortError";
6666
6693
  }
6667
6694
  /** Build the provider's stable cancellation error while retaining the caller's reason. */
@@ -6672,11 +6699,11 @@ function throwIfAborted(signal) {
6672
6699
  if (signal?.aborted === true) throw searchAborted(signal, void 0);
6673
6700
  }
6674
6701
  /**
6675
- * A `ctx.web` search provider backed by the Command Code Provider API. Reuses
6676
- * the plugin's credential chain and `apiBase`, so search "just works" with the
6677
- * existing key — the model-facing `web_search` tool needs no separate
6678
- * configuration. Selection between multiple search providers is the web seam's
6679
- * 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).
6680
6707
  */
6681
6708
  var CommandCodeSearchProvider = class {
6682
6709
  deps;
@@ -6716,7 +6743,7 @@ var CommandCodeSearchProvider = class {
6716
6743
  ...signal !== void 0 ? { signal } : {}
6717
6744
  });
6718
6745
  } catch (error) {
6719
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6746
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6720
6747
  throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
6721
6748
  }
6722
6749
  if (!response.ok) {
@@ -6731,7 +6758,7 @@ var CommandCodeSearchProvider = class {
6731
6758
  if (typeof code === "string" || typeof inner === "string") message += `: ${typeof code === "string" ? code : ""}${typeof code === "string" && typeof inner === "string" ? " — " : ""}${typeof inner === "string" ? inner : ""}`;
6732
6759
  }
6733
6760
  } catch (error) {
6734
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6761
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6735
6762
  const slice = (await response.text().catch(() => "")).trim().slice(0, 200);
6736
6763
  if (slice !== "") message += `: ${slice}`;
6737
6764
  }
@@ -6741,7 +6768,7 @@ var CommandCodeSearchProvider = class {
6741
6768
  try {
6742
6769
  payload = await response.json();
6743
6770
  } catch (error) {
6744
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6771
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6745
6772
  throw new WebError("Command Code web search returned an unparseable response body", "WEB_PROVIDER_ERROR", { cause: error });
6746
6773
  }
6747
6774
  const results = payload?.results;
@@ -6765,7 +6792,7 @@ var CommandCodeSearchProvider = class {
6765
6792
  try {
6766
6793
  key = await this.deps.resolveKey();
6767
6794
  } catch (error) {
6768
- if (signal?.aborted === true || isAbortError$1(error)) throw searchAborted(signal, error);
6795
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
6769
6796
  if (error instanceof Error && typeof error.code === "string") {
6770
6797
  const code = error.code;
6771
6798
  if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
@@ -6806,11 +6833,10 @@ const OTHER_GROUP_ID = "models-other";
6806
6833
  * free before paid inside a tier, then by id.
6807
6834
  *
6808
6835
  * The list is the static capability snapshot rather than a live catalog read
6809
- * on purpose — a settings page must draw synchronously, and the snapshot is
6810
- * synced from the same upstream table the picker's tier headings come from.
6811
- * A model added upstream after this build still reaches the user: an empty
6812
- * allowlist shows everything, and a model named in the allowlist but absent
6813
- * 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.
6814
6840
  */
6815
6841
  function commandCodeTuiModelChoices() {
6816
6842
  return Object.keys(KNOWN_PLANS).map((id) => ({
@@ -6838,14 +6864,13 @@ function storedIds(value) {
6838
6864
  * Build the section descriptor. Pure, so tests can pin the exact fields
6839
6865
  * without a dsh-TUI host.
6840
6866
  *
6841
- * Field choices worth keeping: the two option-bearing fields (`activeAccount`,
6842
- * `lang`) are `text` + `options` rather than `select`, because a `select`
6843
- * cannot express "unset" — cycling only ever lands on a declared option, so a
6844
- * `select` would strand the user on a pinned value with no way back to
6845
- * automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
6846
- * unset state reachable. `filterModelsByPlan` formats its EFFECTIVE default
6847
- * (unset means true at the adapter), so a fresh install reads true instead of
6848
- * 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)".
6849
6874
  */
6850
6875
  function buildCommandCodeTuiSection(deps) {
6851
6876
  const ref = deps.apiKeyRef();
@@ -6875,12 +6900,9 @@ function buildCommandCodeTuiSection(deps) {
6875
6900
  * ONE draft: every one of them then parses that same draft on save, all N
6876
6901
  * write ops address the same path, and only the LAST field's op survives —
6877
6902
  * which silently rewrote the allowlist from the last catalog model instead
6878
- * of the one that was toggled. A per-model key is what makes a checkbox
6879
- * express one model's state.
6880
- *
6881
- * An override equal to what the array already says is written as a CLEAR, so
6882
- * toggling a model back to its inherited state leaves no residue and the
6883
- * 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.
6884
6906
  */
6885
6907
  const modelField = (id, hint, group) => ({
6886
6908
  path: ["modelVisibility", id],
@@ -6953,54 +6975,6 @@ function buildCommandCodeTuiSection(deps) {
6953
6975
  };
6954
6976
  }
6955
6977
  },
6956
- {
6957
- path: ["commandGuard"],
6958
- group: "advanced",
6959
- kind: "boolean",
6960
- label: "AI command guard",
6961
- descriptions: { zh: "AI 命令安全预判" },
6962
- 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.",
6963
- hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全;沙箱提权还要分别判断范围与必要性,三项都足够确定才自动放行,其余照常弹窗。默认关闭。" },
6964
- format: (value) => value === true ? "true" : "false",
6965
- parse: (text) => ({
6966
- kind: "set",
6967
- value: text.trim() === "true"
6968
- })
6969
- },
6970
- {
6971
- path: ["commandGuardLevel"],
6972
- group: "advanced",
6973
- kind: "select",
6974
- label: "Auto-approve threshold",
6975
- descriptions: { zh: "自动放行阈值" },
6976
- hint: "How confident typesafe/jev must be before a command runs without asking. Default: medium.",
6977
- hintDescriptions: { zh: "typesafe/jev 需要多确定才免弹窗放行;默认「中」。" },
6978
- options: [
6979
- {
6980
- value: "high",
6981
- label: "High (0.95, approves least)",
6982
- descriptions: { zh: "高(0.95,放行最少)" }
6983
- },
6984
- {
6985
- value: "medium",
6986
- label: "Medium (0.9)",
6987
- descriptions: { zh: "中(0.9)" }
6988
- },
6989
- {
6990
- value: "low",
6991
- label: "Low (0.8, approves most)",
6992
- descriptions: { zh: "低(0.8,放行最多)" }
6993
- }
6994
- ],
6995
- format: (value) => value === "high" || value === "low" ? value : "medium",
6996
- parse: (text) => {
6997
- const trimmed = text.trim();
6998
- return trimmed === "high" || trimmed === "medium" || trimmed === "low" ? {
6999
- kind: "set",
7000
- value: trimmed
7001
- } : void 0;
7002
- }
7003
- },
7004
6978
  {
7005
6979
  path: ["zdr"],
7006
6980
  group: "advanced",
@@ -7135,12 +7109,10 @@ function sectionSignature(section) {
7135
7109
  /**
7136
7110
  * Register the Command Code section on a dsh-TUI host.
7137
7111
  *
7138
- * @param ctx - the context of an activated `tuiSettingsSections` injection.
7139
- * @param deps - plugin-owned facts the section reads.
7140
7112
  * @returns a refresh function that re-registers the section when a fact it
7141
- * renders changed (the plugin entry calls it from its settings `onChange`
7142
- * hook), or `undefined` when the seam is unusable. The returned function is
7143
- * 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.
7144
7116
  */
7145
7117
  function applyCommandCodeTuiSettings(ctx, deps) {
7146
7118
  const service = tuiSettingsService(ctx);
@@ -7176,45 +7148,26 @@ function applyCommandCodeTuiSettings(ctx, deps) {
7176
7148
  /**
7177
7149
  * A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
7178
7150
  *
7179
- * The route's retry policy (`providerRetryPolicy` in `./adapter.ts`) is
7180
- * deliberately near-unbounded: 1000 attempts with waits doubling to a 15-minute
7181
- * cap. That shape exists for the failures a provider ASKS to have retried — an
7182
- * exhausted 5-hour window, a Cloudflare 520 — where the wait is the point.
7183
- *
7184
- * A transport failure is not that. The reporter's second event at ~710k
7185
- * context was `Connect Timeout Error ... timeout: 10000ms` (undici's own
7186
- * connect timeout; this plugin's `requestTimeoutMs` is 60s) and, on another
7187
- * attempt, `read ECONNRESET` against `/alpha/generate`. Because `TRANSPORT` is
7188
- * in the whitelist, each of those entered the same 1000-attempt loop: with
7189
- * `initialDelayMs: 500` doubling, the wait before attempt 11 alone is 512 s and
7190
- * the cumulative wait before it is ~500 s — the "模型请求重试已取消(11/1000)·
7191
- * 482s" the reporter photographed is that 512 s countdown, 30 s into it — and
7192
- * those waits are counted BEFORE each retry attempt, not after it.
7193
- *
7194
- * A connection that cannot be established is either a sub-second blip (the
7195
- * first few attempts recover it, invisibly) or an outage that no amount of
7196
- * waiting inside one request will fix. So transport failures get their own
7197
- * budget, and when it is exhausted the failure is surfaced with the diagnosis
7198
- * instead of manufacturing a multi-minute stall. Waiting out a real outage is
7199
- * the user's call — the next send starts a fresh budget — and
7200
- * `dsh-llm-retry`'s window for rate limits is untouched.
7201
- *
7202
- * Deliberately NOT counted here: the pre-stream recovery the ADAPTER performs
7203
- * inside one `stream()` call (account rotation on 429/401, the 413 image-budget
7204
- * step-down, the Provider-API → CLI protocol switch). Those are invisible to
7205
- * the caller and bounded on their own; this budget caps only the harness's
7206
- * retry loop.
7207
- *
7208
- * Reset rule: the budget clears at each new STEP — one step is exactly one
7209
- * model request (`step/start` is appended immediately before the call) — so the
7210
- * cap is per logical request and the next step of the same turn gets its own
7211
- * grace. `agent/status` → `idle` is NOT the reset signal even though it looks
7212
- * like the natural one: the agent loop's `setPhase` emits it only on a status
7213
- * CHANGE, and a turn's steps all run inside one `running` phase, so it never
7214
- * fires between them — a budget reset only there would be per-turn, and an
7215
- * outage would fail every later step of that turn immediately instead of
7216
- * retrying each. `assistant/attempt` is wrong for the opposite reason: the loop
7217
- * 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
7218
7171
  * `agent/request-error`, so resetting on it would clear the budget on every
7219
7172
  * failure and restore the very loop this module exists to stop.
7220
7173
  *
@@ -7248,15 +7201,13 @@ function resetTransportFailures(agent) {
7248
7201
  /**
7249
7202
  * What one session event means for the transport budget.
7250
7203
  *
7251
- * Exported and pure on purpose: the choice of event is the part of this design
7252
- * that is easy to get plausibly wrong (`agent/status` → `idle` looks like the
7253
- * natural reset and is not one — see the module comment), so it is stated once,
7254
- * 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.
7255
7207
  *
7256
7208
  * @param type - the appended session event's type.
7257
- * @returns `'reset'` for a new step (one model request), `'forget'` when the
7258
- * turn ended and the session → agent mapping is no longer needed, otherwise
7259
- * `'none'`.
7209
+ * @returns `'reset'` for a new step, `'forget'` when the turn ended (the
7210
+ * session → agent mapping is no longer needed), otherwise `'none'`.
7260
7211
  */
7261
7212
  function transportResetAction(type) {
7262
7213
  if (type === "step/start") return "reset";
@@ -7266,880 +7217,44 @@ function transportResetAction(type) {
7266
7217
  /**
7267
7218
  * The diagnosis a capped turn ends with. Bilingual, because the harness renders
7268
7219
  * a failed turn's message verbatim and this plugin's other user-facing failures
7269
- * are bilingual too (see the adapter's error builders). It names the retry
7270
- * budget and the checks that actually help, instead of the bare "fetch failed"
7271
- * 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.
7272
7222
  */
7273
7223
  function transportBudgetMessage(failureMessage, maxRetries) {
7274
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`;
7275
7225
  }
7276
7226
  //#endregion
7277
- //#region src/systemone.ts
7278
- /**
7279
- * Host-side client for Command Code's System One decision endpoint
7280
- * (`POST {apiBase}/provider/v1/systemone`).
7281
- *
7282
- * `typesafe/jev` is **not a chat model**: it takes a `state` plus typed
7283
- * questions and answers with probabilities, never text, and it never streams.
7284
- * It is therefore absent from `/provider/v1/models` (so the picker can never
7285
- * offer it) and must never be registered as a model route — this plugin uses it
7286
- * as an internal advisor instead: the command guard in `./command-guard.ts`
7287
- * asks it whether a shell command is safe to auto-approve.
7288
- *
7289
- * The three question types are the whole vocabulary (command-code@1.65.2
7290
- * `systemOneRequestSchema`, unchanged since 1.64.0):
7291
- *
7292
- * - `noul` — "is this true?", optional `criteria: { true, false }` describing
7293
- * each verdict. Answers with `noul`, the probability of *yes*.
7294
- * - `choice` — "which one?", `criteria` maps each option key to its
7295
- * description. Answers with `choice`, plus optional `confidence` and
7296
- * `probabilities`.
7297
- * - `score` — "where on this rubric?", `criteria` is an array of at least two
7298
- * levels, lowest first. Answers with `score`, `legend` and `probabilities`.
7299
- *
7300
- * A request carries at most 20 questions and every answer is keyed by the
7301
- * question id it belongs to. `usage` is reported per request, in tokens.
7302
- *
7303
- * Billing: free through 2026-09-24T23:59:59Z, then $0.042 per 1M input tokens
7304
- * with output free. The model has **no ZDR-capable upstream**, so a request
7305
- * carrying `x-cmd-zdr: 1` is refused with HTTP 422 `cmd_zdr_no_providers` —
7306
- * this client never sends that header, and it must stay that way even though
7307
- * the plugin now ships a ZDR switch for chat (`Config.zdr` → the `x-cmd-zdr: 1`
7308
- * header per model, gated on `KNOWN_NON_ZDR_MODELS`): the decision endpoint is
7309
- * reached through THIS module's own request path, never through the adapter's
7310
- * chat connection, so the two cannot drift — and a deployment that later widens
7311
- * ZDR must exclude this endpoint rather than route it through the switch.
7312
- *
7313
- * Auth is the plugin's ordinary chain (the multi-account pool, the credentials
7314
- * seam, the CLI auth file) through the same `Authorization: Bearer <key>` and
7315
- * `x-command-code-version` headers every other endpoint uses.
7316
- *
7317
- * @module dsh-commandcode-provider/systemone
7318
- */
7319
- /** Path of the decision endpoint, relative to `apiBase`. */
7320
- const SYSTEMONE_ROUTE = "/provider/v1/systemone";
7321
- /** The one error type this client throws. Callers fail closed on it. */
7322
- var SystemOneError = class extends Error {
7323
- kind;
7324
- status;
7325
- constructor(message, kind, status, options) {
7326
- super(message, options);
7327
- this.kind = kind;
7328
- this.status = status;
7329
- this.name = "SystemOneError";
7330
- }
7331
- };
7332
- /**
7333
- * Combine the caller's signal with a timeout, keeping the two distinguishable:
7334
- * a timeout is this client's own budget expiring (the guard delegates to the
7335
- * user) while a caller abort means the approval was already withdrawn (answer
7336
- * nothing) — both fail closed, but the diagnosis must not lie about which.
7337
- *
7338
- * The timer is a plain `setTimeout` rather than `AbortSignal.timeout()`, whose
7339
- * timer Node unrefs: with nothing else pending (a test double, a socket that
7340
- * never opened) an unref'd timer lets the process exit while the promise is
7341
- * still pending, which reads as a hang rather than as the timeout it is.
7342
- */
7343
- function deadlineOf(signal, timeoutMs) {
7344
- const controller = new AbortController();
7345
- let timedOut = false;
7346
- const timer = setTimeout(() => {
7347
- timedOut = true;
7348
- controller.abort();
7349
- }, timeoutMs);
7350
- const onCallerAbort = () => controller.abort();
7351
- if (signal !== void 0) {
7352
- if (signal.aborted) controller.abort();
7353
- else signal.addEventListener("abort", onCallerAbort, { once: true });
7354
- }
7355
- return {
7356
- signal: controller.signal,
7357
- callerAborted: () => signal?.aborted === true,
7358
- timedOut: () => timedOut,
7359
- dispose: () => {
7360
- clearTimeout(timer);
7361
- signal?.removeEventListener("abort", onCallerAbort);
7362
- }
7363
- };
7364
- }
7365
- /** Whether a caught value is an abort-shaped failure. */
7366
- function isAbortError(error) {
7367
- return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
7368
- }
7369
- /** Stop waiting for an operation that does not accept an AbortSignal. */
7370
- function withinDeadline(operation, deadline, timeoutMs) {
7371
- return new Promise((resolve, reject) => {
7372
- const onAbort = () => {
7373
- reject(deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout"));
7374
- };
7375
- deadline.signal.addEventListener("abort", onAbort, { once: true });
7376
- operation.then((value) => {
7377
- deadline.signal.removeEventListener("abort", onAbort);
7378
- resolve(value);
7379
- }, (error) => {
7380
- deadline.signal.removeEventListener("abort", onAbort);
7381
- reject(error);
7382
- });
7383
- if (deadline.signal.aborted) onAbort();
7384
- });
7385
- }
7386
- /** A finite number in `[0, 1]`, or undefined. */
7387
- function probability(value) {
7388
- return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
7389
- }
7390
- /** A finite number, or undefined. */
7391
- function finite(value) {
7392
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7393
- }
7394
- /** A record of finite numbers (probabilities), or undefined when the shape is off. */
7395
- function numberRecord(value) {
7396
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7397
- const out = {};
7398
- for (const [key, entry] of Object.entries(value)) {
7399
- const parsed = finite(entry);
7400
- if (parsed === void 0) return void 0;
7401
- out[key] = parsed;
7402
- }
7403
- return out;
7404
- }
7405
- /**
7406
- * Parse one answer entry strictly by its own `type`. A body whose answer does
7407
- * not carry the member its type promises is rejected rather than defaulted:
7408
- * an absent probability is exactly the case the caller must not read as "safe".
7409
- *
7410
- * @param value - one raw entry from the response's `answers` object.
7411
- * @returns the parsed answer, or undefined when the entry is malformed.
7412
- */
7413
- function parseSystemOneAnswer(value) {
7414
- if (typeof value !== "object" || value === null) return void 0;
7415
- const raw = value;
7416
- if (raw.type === "noul") {
7417
- const noul = probability(raw.noul);
7418
- return noul === void 0 ? void 0 : {
7419
- type: "noul",
7420
- noul
7421
- };
7422
- }
7423
- if (raw.type === "choice") {
7424
- if (typeof raw.choice !== "string" || raw.choice === "") return void 0;
7425
- const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
7426
- if (raw.confidence !== void 0 && confidence === void 0) return void 0;
7427
- const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
7428
- if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
7429
- return {
7430
- type: "choice",
7431
- choice: raw.choice,
7432
- ...confidence === void 0 ? {} : { confidence },
7433
- ...probabilities === void 0 ? {} : { probabilities }
7434
- };
7435
- }
7436
- if (raw.type === "score") {
7437
- const score = finite(raw.score);
7438
- if (score === void 0) return void 0;
7439
- const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
7440
- if (raw.confidence !== void 0 && confidence === void 0) return void 0;
7441
- const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
7442
- if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
7443
- return {
7444
- type: "score",
7445
- score,
7446
- ...confidence === void 0 ? {} : { confidence },
7447
- ...probabilities === void 0 ? {} : { probabilities }
7448
- };
7449
- }
7450
- }
7451
- /**
7452
- * Parse a decision response body. Every answer entry must be well formed — one
7453
- * malformed entry fails the whole response, because a partially understood
7454
- * decision is not a decision. Unknown extra members are ignored so a server-side
7455
- * addition cannot break the guard.
7456
- *
7457
- * @param value - the decoded JSON body.
7458
- * @returns the parsed response, or undefined when the body is not one.
7459
- */
7460
- function parseSystemOneResponse(value) {
7461
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7462
- const raw = value;
7463
- const rawAnswers = raw.answers;
7464
- if (typeof rawAnswers !== "object" || rawAnswers === null || Array.isArray(rawAnswers)) return void 0;
7465
- const entries = Object.entries(rawAnswers);
7466
- if (entries.length === 0) return void 0;
7467
- const answers = {};
7468
- for (const [id, entry] of entries) {
7469
- const answer = parseSystemOneAnswer(entry);
7470
- if (answer === void 0) return void 0;
7471
- answers[id] = answer;
7472
- }
7473
- const usage = raw.usage === void 0 ? void 0 : parseUsage(raw.usage);
7474
- if (raw.usage !== void 0 && usage === void 0) return void 0;
7475
- return {
7476
- ...typeof raw.model === "string" ? { model: raw.model } : {},
7477
- answers,
7478
- ...usage === void 0 ? {} : { usage }
7479
- };
7480
- }
7481
- /** Parse the optional `usage` member; an unreadable one is a malformed body. */
7482
- function parseUsage(value) {
7483
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
7484
- const raw = value;
7485
- const inputTokens = raw.input_tokens === void 0 ? void 0 : finite(raw.input_tokens);
7486
- if (raw.input_tokens !== void 0 && inputTokens === void 0) return void 0;
7487
- const outputTokens = raw.output_tokens === void 0 ? void 0 : finite(raw.output_tokens);
7488
- if (raw.output_tokens !== void 0 && outputTokens === void 0) return void 0;
7489
- return {
7490
- ...inputTokens === void 0 ? {} : { inputTokens },
7491
- ...outputTokens === void 0 ? {} : { outputTokens }
7492
- };
7493
- }
7494
- /** Validate the outgoing request where the server would only reject it with a 400. */
7495
- function assertRequest(request) {
7496
- const ids = Object.keys(request.questions);
7497
- if (ids.length === 0) throw new SystemOneError("a decision request needs at least one question", "invalid-request");
7498
- if (ids.length > 20) throw new SystemOneError(`a decision request carries at most 20 questions (got ${ids.length})`, "invalid-request");
7499
- for (const id of ids) if (id.trim() === "") throw new SystemOneError("every decision question needs a non-empty id", "invalid-request");
7500
- }
7501
- /** Read the endpoint's error body into a one-line diagnosis. */
7502
- async function httpFailure(response) {
7503
- let detail = "";
7504
- try {
7505
- const slice = (await response.text()).trim().slice(0, 200);
7506
- if (slice !== "") detail = `: ${slice}`;
7507
- } catch {}
7508
- return new SystemOneError(`Command Code decision request failed (HTTP ${response.status})${detail}`, "http", response.status);
7509
- }
7510
- /**
7511
- * Run one decision request.
7512
- *
7513
- * @param deps - connection facts, credential chain and fetch seam.
7514
- * @param request - the state and typed questions.
7515
- * @param options - caller cancellation and the per-call budget.
7516
- * @returns the parsed decision.
7517
- * @throws {SystemOneError} on every failure path; callers fail closed.
7518
- */
7519
- async function runSystemOne(deps, request, options = {}) {
7520
- assertRequest(request);
7521
- const apiBase = deps.apiBase();
7522
- if (!URL.canParse(apiBase)) throw new SystemOneError(`Command Code decision request is misconfigured: apiBase ${JSON.stringify(apiBase)} is not a valid URL`, "invalid-request");
7523
- if (options.signal?.aborted === true) throw new SystemOneError("decision request aborted", "aborted");
7524
- const timeoutMs = options.timeoutMs ?? 1500;
7525
- const deadline = deadlineOf(options.signal, timeoutMs);
7526
- try {
7527
- let key;
7528
- try {
7529
- key = await withinDeadline(Promise.resolve().then(() => deps.resolveApiKey()), deadline, timeoutMs);
7530
- } catch (error) {
7531
- if (error instanceof SystemOneError && (error.kind === "aborted" || error.kind === "timeout")) throw error;
7532
- throw new SystemOneError(`Command Code decision request has no usable credential: ${error instanceof Error ? error.message : String(error)}`, "credential", void 0, { cause: error });
7533
- }
7534
- const body = {
7535
- model: request.model ?? "typesafe/jev",
7536
- state: request.state,
7537
- questions: request.questions
7538
- };
7539
- if (deadline.signal.aborted) throw deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout");
7540
- let response;
7541
- try {
7542
- response = await (deps.fetchImpl ?? fetch)(`${apiBase.replace(/\/$/, "")}${SYSTEMONE_ROUTE}`, {
7543
- method: "POST",
7544
- headers: {
7545
- "Content-Type": "application/json",
7546
- ...IDENTITY_ENCODING_HEADER,
7547
- Authorization: `Bearer ${key}`,
7548
- "x-command-code-version": COMMAND_CODE_CLI_VERSION,
7549
- ...attributionHeaders()
7550
- },
7551
- body: JSON.stringify(body),
7552
- signal: deadline.signal
7553
- });
7554
- } catch (error) {
7555
- if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
7556
- if (deadline.timedOut() || isAbortError(error)) throw new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout", void 0, { cause: error });
7557
- throw new SystemOneError(`Command Code decision request failed: ${error instanceof Error ? error.message : String(error)}`, "network", void 0, { cause: error });
7558
- }
7559
- if (!response.ok) throw await httpFailure(response);
7560
- let payload;
7561
- try {
7562
- payload = await response.json();
7563
- } catch (error) {
7564
- if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
7565
- throw new SystemOneError("Command Code returned an unparseable decision body", "invalid-response", void 0, { cause: error });
7566
- }
7567
- const parsed = parseSystemOneResponse(payload);
7568
- if (parsed === void 0) throw new SystemOneError("Command Code returned a decision body this plugin cannot read", "invalid-response");
7569
- return parsed;
7570
- } finally {
7571
- deadline.dispose();
7572
- }
7573
- }
7574
- //#endregion
7575
- //#region src/command-guard.ts
7576
- /** Tool names whose `arguments.command` this guard judges. */
7577
- const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
7578
- /** Question id the primary safety verdict is keyed by. */
7579
- const COMMAND_GUARD_QUESTION_ID = "safe";
7580
- /** Question id for the narrow-purpose part of a sandbox escalation. */
7581
- const COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID = "escalation_scope";
7582
- /** Question id for the necessity/proportionality part of a sandbox escalation. */
7583
- const COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID = "escalation_necessity";
7584
- /** The selectable levels, strictest first (the order the settings surfaces list them in). */
7585
- const COMMAND_GUARD_LEVELS = [
7586
- "high",
7587
- "medium",
7588
- "low"
7589
- ];
7590
- /** Each level's probability threshold. */
7591
- const COMMAND_GUARD_LEVEL_THRESHOLDS = {
7592
- high: .95,
7593
- medium: .9,
7594
- low: .8
7595
- };
7596
- /** The level an unset config reads as. */
7597
- const COMMAND_GUARD_DEFAULT_LEVEL = "medium";
7598
- /** Probability of "safe" at or above which the guard grants `allowed-once` by default. */
7599
- const COMMAND_GUARD_DEFAULT_THRESHOLD = COMMAND_GUARD_LEVEL_THRESHOLDS[COMMAND_GUARD_DEFAULT_LEVEL];
7600
- /** The threshold for a stored level; anything unrecognized reads as the default level. */
7601
- function commandGuardThreshold(level) {
7602
- return typeof level === "string" && Object.hasOwn(COMMAND_GUARD_LEVEL_THRESHOLDS, level) ? COMMAND_GUARD_LEVEL_THRESHOLDS[level] : COMMAND_GUARD_DEFAULT_THRESHOLD;
7603
- }
7604
- /**
7605
- * Decision budget; the approval prompt must not wait longer than this. Fixed
7606
- * rather than configurable: it only bounds how long a late verdict can delay
7607
- * the prompt, and every failure (a timeout included) already falls back to
7608
- * asking the user, so there is nothing for a user to tune. 3 s leaves room for
7609
- * credential resolution plus one decision round trip on a slow link.
7610
- */
7611
- const COMMAND_GUARD_TIMEOUT_MS = 3e3;
7612
- /**
7613
- * Longest command the guard will send for judgement. A longer one is delegated
7614
- * instead of truncated: a verdict on the first N characters of a command is a
7615
- * verdict on a different command.
7616
- */
7617
- const COMMAND_GUARD_MAX_COMMAND_CHARS = 6e3;
7618
- /**
7619
- * Hard denylist: commands the guard never auto-approves, and never even sends
7620
- * for judgement. Each entry is a deliberate, narrow reading of one destructive
7621
- * family — the goal is not to classify every dangerous command (that is the
7622
- * model's job) but to make sure the classics cannot be talked into a grant by a
7623
- * persuasive-looking command string.
7624
- *
7625
- * Every pattern is allowed to OVER-match: a false positive costs one ordinary
7626
- * human prompt, while a false negative would hand a destructive command to a
7627
- * model that has to guess. That asymmetry is why `sudo` is matched anywhere in
7628
- * the line rather than only in command position, and why `fdisk` is listed even
7629
- * though `fdisk -l` only lists partitions.
7630
- */
7631
- const COMMAND_GUARD_DENY_LIST = [
7632
- {
7633
- id: "rm-root",
7634
- 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|$)/
7635
- },
7636
- {
7637
- id: "rm-home",
7638
- pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(?:~|\$HOME)\//
7639
- },
7640
- {
7641
- id: "filesystem-write",
7642
- pattern: /\b(?:mkfs(?:\.\w+)?|wipefs|fdisk|parted|shred)\b/
7643
- },
7644
- {
7645
- id: "raw-device-write",
7646
- pattern: /\bdd\b[^\n]*\bof=\/dev\//
7647
- },
7648
- {
7649
- id: "device-redirect",
7650
- pattern: />{1,2}\s*\/dev\/(?:sd|disk|nvme|hd)/
7651
- },
7652
- {
7653
- id: "fork-bomb",
7654
- pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/
7655
- },
7656
- {
7657
- id: "privilege-escalation",
7658
- pattern: /\b(?:sudo|doas)\b/
7659
- },
7660
- {
7661
- id: "su",
7662
- pattern: /\bsu\s/
7663
- },
7664
- {
7665
- id: "chmod-world-write",
7666
- pattern: /\bchmod\b[^\n]*\b(?:777|a\+w|o\+w)\b/
7667
- },
7668
- {
7669
- id: "chown-root",
7670
- pattern: /\bchown\b[^\n]*\b(?:root|0):/
7671
- },
7672
- {
7673
- id: "git-push",
7674
- pattern: /\bgit\s+push\b/
7675
- },
7676
- {
7677
- id: "git-history-rewrite",
7678
- pattern: /\bgit\s+(?:filter-branch|filter-repo)\b/
7679
- },
7680
- {
7681
- id: "git-hard-reset",
7682
- pattern: /\bgit\s+reset\s+--hard\b/
7683
- },
7684
- {
7685
- id: "git-clean-force",
7686
- pattern: /\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f/
7687
- },
7688
- {
7689
- id: "git-discard-worktree",
7690
- pattern: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/
7691
- },
7692
- {
7693
- id: "git-stash-drop",
7694
- pattern: /\bgit\s+stash\s+(?:drop|clear)\b/
7695
- },
7696
- {
7697
- id: "publish",
7698
- pattern: /\b(?:npm|pnpm|yarn|bun)\s+publish\b/
7699
- },
7700
- {
7701
- id: "release-upload",
7702
- pattern: /\b(?:gh\s+release|twine\s+upload)\b/
7703
- },
7704
- {
7705
- id: "docker-push",
7706
- pattern: /\bdocker\s+push\b/
7707
- },
7708
- {
7709
- id: "cloud-upload",
7710
- pattern: /\b(?:aws\s+s3|gsutil|gcloud\s+storage|az\s+storage)\b[^\n]*\b(?:cp|sync|mv|rm|rb)\b/
7711
- },
7712
- {
7713
- id: "remote-copy",
7714
- pattern: /\b(?:scp|rsync)\b[^\n]*(?::|@)/
7715
- },
7716
- {
7717
- id: "pipe-to-shell",
7718
- pattern: /\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|dash|ksh|python\d*|node|ruby|perl)\b/
7719
- },
7720
- {
7721
- id: "power",
7722
- pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/
7723
- },
7724
- {
7725
- id: "kill-everything",
7726
- pattern: /\bkill\s+(?:-9\s+)?-1\b|\b(?:killall|pkill)\s+-9\b/
7727
- },
7728
- {
7729
- id: "credential-write",
7730
- pattern: /(?:>{1,2}|tee|cp|mv|rm|chmod|chown)\s[^\n]*(?:\.ssh\b|\.aws\b|\.gnupg\b|\.commandcode\/auth\.json|id_rsa|id_ed25519)/
7731
- },
7732
- {
7733
- id: "history-clear",
7734
- pattern: /\bhistory\s+-c\b|\bunset\s+HISTFILE\b/
7735
- },
7736
- {
7737
- id: "global-install",
7738
- pattern: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add)\s+(?:-g\b|--global\b)/
7739
- },
7740
- {
7741
- id: "system-install",
7742
- pattern: /\b(?:apt|apt-get|dnf|yum|zypper|apk|pacman|brew)\s+(?:install|remove|purge|uninstall|upgrade)\b/
7743
- },
7744
- {
7745
- id: "language-install",
7746
- pattern: /\b(?:pip3?|pipx|gem|cargo)\s+(?:install|add)\b/
7747
- }
7748
- ];
7749
- /**
7750
- * Why the guard refuses to judge a command, when it does.
7751
- *
7752
- * @param command - the raw command text.
7753
- * @returns the matching denylist id, or undefined.
7754
- */
7755
- function denyReasonOf(command) {
7756
- for (const entry of COMMAND_GUARD_DENY_LIST) if (entry.pattern.test(command)) return entry.id;
7757
- }
7758
- /** A non-empty string member, or undefined. */
7759
- function stringMember(value, key) {
7760
- if (typeof value !== "object" || value === null) return void 0;
7761
- const member = value[key];
7762
- return typeof member === "string" && member.trim() !== "" ? member : void 0;
7763
- }
7764
- /**
7765
- * Read a shell command out of a tool execution, or undefined when this call is
7766
- * not one the guard judges (a different tool, or arguments without a command).
7767
- *
7768
- * @param name - the executed tool's name.
7769
- * @param args - the execution's parsed `arguments`.
7770
- * @returns the fact to judge, without the asker's reason (that arrives later).
7771
- */
7772
- function shellCommandOf(name, args) {
7773
- if (!COMMAND_GUARD_SHELL_TOOLS.includes(name)) return void 0;
7774
- const command = stringMember(args, "command");
7775
- if (command === void 0) return void 0;
7776
- const description = stringMember(args, "description");
7777
- const workdir = stringMember(args, "workdir");
7778
- const sandboxPermissions = stringMember(args, "sandbox_permissions");
7779
- return {
7780
- tool: name,
7781
- command,
7782
- ...description === void 0 ? {} : { description },
7783
- ...workdir === void 0 ? {} : { workdir },
7784
- ...sandboxPermissions === void 0 ? {} : { sandboxPermissions }
7785
- };
7786
- }
7787
- /**
7788
- * Build the decision request for one command.
7789
- *
7790
- * A normal approval asks one question. A call that explicitly requests a wider
7791
- * sandbox gets the same safety question plus two veto questions. This keeps the
7792
- * common case cheap while making the broader permission change an explicit
7793
- * decision: JEV must agree that the extra access has a narrow purpose and is
7794
- * necessary, not merely that the command text looks familiar.
7795
- *
7796
- * The agent's own `description` and the harness's `reason` are included as
7797
- * context, but the criteria say the command text itself decides — a
7798
- * model-written description is a claim, not evidence.
7799
- *
7800
- * @param fact - the command and its context.
7801
- * @returns the request to send.
7802
- */
7803
- function buildSafetyRequest(fact) {
7804
- const escalated = fact.sandboxPermissions !== void 0;
7805
- const lines = [
7806
- `Tool: ${fact.tool}`,
7807
- "Command:",
7808
- fact.command,
7809
- ...fact.description === void 0 ? [] : ["", `The agent describes this command as: ${fact.description}`],
7810
- ...fact.workdir === void 0 ? [] : ["", `Working directory: ${fact.workdir}`],
7811
- ...fact.sandboxPermissions === void 0 ? [] : ["", `The harness is asking because this call requests a wider sandbox mode (${fact.sandboxPermissions}) than the one currently in force.`],
7812
- ...fact.reason === void 0 ? [] : ["", `The asker's reason for needing approval: ${fact.reason}`]
7813
- ];
7814
- const question = {
7815
- type: "noul",
7816
- 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.",
7817
- criteria: {
7818
- 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.",
7819
- 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."
7820
- }
7821
- };
7822
- const questions = { [COMMAND_GUARD_QUESTION_ID]: question };
7823
- if (escalated) {
7824
- questions[COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID] = {
7825
- type: "noul",
7826
- 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?",
7827
- criteria: {
7828
- 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.",
7829
- 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."
7830
- }
7831
- };
7832
- questions[COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID] = {
7833
- type: "noul",
7834
- 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.",
7835
- criteria: {
7836
- 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.",
7837
- false: "The wider mode is unnecessary, broader than needed, unsupported by the command, or the command is too ambiguous to justify the extra authority."
7838
- }
7839
- };
7840
- }
7841
- return {
7842
- state: lines.join("\n"),
7843
- questions
7844
- };
7845
- }
7846
- /**
7847
- * Read a probability out of a decision response.
7848
- *
7849
- * @param response - the parsed response.
7850
- * @param questionId - the question id to read (defaults to the guard's own).
7851
- * @returns the probability, or undefined when the answer is missing or is not
7852
- * a `noul` verdict.
7853
- */
7854
- function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
7855
- const answer = response.answers[questionId];
7856
- return answer !== void 0 && answer.type === "noul" ? answer.noul : void 0;
7857
- }
7858
- /**
7859
- * One-line, length-bounded excerpt of a command for the log. The full text is
7860
- * already in the session's tool call and was already sent to the decision
7861
- * endpoint, so this adds no exposure — it is what makes "why did this run
7862
- * without asking?" answerable from the log alone.
7863
- */
7864
- function commandExcerpt(command, limit = 160) {
7865
- const flat = command.replace(/\s+/g, " ").trim();
7866
- return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
7867
- }
7868
- /**
7869
- * The guard's stateful half: what was executed (by call id) and what has
7870
- * already been decided (by agent and complete decision context), both bounded.
7871
- *
7872
- * The execution cache is the bridge between the two seams — `tools/pre-execute`
7873
- * knows the arguments, `approval/request` knows the call id — and it is keyed by
7874
- * call id with an agent-identity check when read: an entry for an execution
7875
- * that never asked is never read, so there is nothing to clean up on the result
7876
- * side (and the ask happens before the result anyway).
7877
- */
7878
- var CommandGuard = class {
7879
- deps;
7880
- executions = /* @__PURE__ */ new Map();
7881
- decisions = /* @__PURE__ */ new WeakMap();
7882
- constructor(deps) {
7883
- this.deps = deps;
7884
- }
7885
- /**
7886
- * Remember one execution's command. Never throws, never changes a decision.
7887
- *
7888
- * @param execution - the execution about to run.
7889
- */
7890
- noteExecution(execution) {
7891
- const callId = execution?.callId;
7892
- if (typeof callId !== "string" || callId === "") return;
7893
- const fact = shellCommandOf(execution.name, execution.arguments);
7894
- if (fact === void 0) return;
7895
- this.executions.delete(callId);
7896
- this.executions.set(callId, {
7897
- fact,
7898
- agent: execution.agent
7899
- });
7900
- while (this.executions.size > 64) {
7901
- const oldest = this.executions.keys().next();
7902
- if (oldest.done === true) break;
7903
- this.executions.delete(oldest.value);
7904
- }
7905
- }
7906
- /** Forget every remembered execution and decision (disposal, tests). */
7907
- clear() {
7908
- this.executions.clear();
7909
- this.decisions = /* @__PURE__ */ new WeakMap();
7910
- }
7911
- /**
7912
- * Judge one approval request.
7913
- *
7914
- * @param request - the pending approval.
7915
- * @returns the verdict; `approve` is the only outcome that grants anything.
7916
- */
7917
- async judge(request) {
7918
- const settings = this.deps.settings();
7919
- if (!settings.enabled) return {
7920
- kind: "delegate",
7921
- reason: "guard disabled"
7922
- };
7923
- const callId = request.callId;
7924
- if (callId === void 0) return {
7925
- kind: "delegate",
7926
- reason: "approval carries no call id"
7927
- };
7928
- const observed = this.executions.get(callId);
7929
- if (observed === void 0 || observed.agent !== request.agent) return {
7930
- kind: "delegate",
7931
- reason: "the guard never saw this call for this agent"
7932
- };
7933
- const fact = observed.fact;
7934
- const excerpt = commandExcerpt(fact.command);
7935
- if (!COMMAND_GUARD_SHELL_TOOLS.includes(request.toolName)) return {
7936
- kind: "delegate",
7937
- reason: `not a shell tool (${request.toolName})`
7938
- };
7939
- if (fact.command.length > 6e3) return {
7940
- kind: "delegate",
7941
- reason: `command longer than ${COMMAND_GUARD_MAX_COMMAND_CHARS} characters`,
7942
- command: excerpt
7943
- };
7944
- const denied = denyReasonOf(fact.command);
7945
- if (denied !== void 0) return {
7946
- kind: "delegate",
7947
- reason: `denylisted command (${denied})`,
7948
- command: excerpt
7949
- };
7950
- const full = {
7951
- ...fact,
7952
- ...request.reason === void 0 ? {} : { reason: request.reason }
7953
- };
7954
- const memoKey = JSON.stringify(full);
7955
- const now = this.deps.now?.() ?? Date.now();
7956
- const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
7957
- const memo = agentDecisions?.get(memoKey);
7958
- if (memo !== void 0) {
7959
- if (now - memo.at < 6e5) return this.verdict(memo, settings.threshold, "memo", excerpt, full.sandboxPermissions !== void 0);
7960
- agentDecisions?.delete(memoKey);
7961
- }
7962
- let response;
7963
- try {
7964
- response = await this.deps.decide(buildSafetyRequest(full), {
7965
- ...request.signal === void 0 ? {} : { signal: request.signal },
7966
- timeoutMs: settings.timeoutMs
7967
- });
7968
- } catch (error) {
7969
- return {
7970
- kind: "delegate",
7971
- reason: `decision unavailable: ${error instanceof Error ? error.message : String(error)}`,
7972
- command: excerpt
7973
- };
7974
- }
7975
- const probability = safeProbabilityOf(response);
7976
- if (probability === void 0) return {
7977
- kind: "delegate",
7978
- reason: "the decision carried no safe verdict",
7979
- command: excerpt
7980
- };
7981
- let escalation;
7982
- if (full.sandboxPermissions !== void 0) {
7983
- const scope = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID);
7984
- const necessity = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID);
7985
- if (scope === void 0 || necessity === void 0) return {
7986
- kind: "delegate",
7987
- reason: "the decision carried no complete sandbox-escalation verdict",
7988
- command: excerpt
7989
- };
7990
- escalation = {
7991
- scope,
7992
- necessity
7993
- };
7994
- }
7995
- const memoEntry = {
7996
- probability,
7997
- ...escalation === void 0 ? {} : { escalation },
7998
- at: now
7999
- };
8000
- if (request.agent !== void 0) {
8001
- const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
8002
- decisions.set(memoKey, memoEntry);
8003
- while (decisions.size > 64) {
8004
- const oldest = decisions.keys().next();
8005
- if (oldest.done === true) break;
8006
- decisions.delete(oldest.value);
8007
- }
8008
- this.decisions.set(request.agent, decisions);
8009
- }
8010
- return this.verdict(memoEntry, settings.threshold, "model", excerpt, full.sandboxPermissions !== void 0);
8011
- }
8012
- /** Apply the threshold to the primary verdict and any escalation vetoes. */
8013
- verdict(entry, threshold, source, command, escalated) {
8014
- if (entry.probability < threshold) return {
8015
- kind: "delegate",
8016
- reason: `safe probability ${entry.probability.toFixed(3)} below ${threshold}`,
8017
- probability: entry.probability,
8018
- command
8019
- };
8020
- if (!escalated) return {
8021
- kind: "approve",
8022
- probability: entry.probability,
8023
- source,
8024
- command
8025
- };
8026
- if (entry.escalation === void 0) return {
8027
- kind: "delegate",
8028
- reason: "the cached sandbox-escalation verdict was incomplete",
8029
- command
8030
- };
8031
- if (entry.escalation.scope < threshold) return {
8032
- kind: "delegate",
8033
- reason: `sandbox scope probability ${entry.escalation.scope.toFixed(3)} below ${threshold}`,
8034
- probability: entry.probability,
8035
- command
8036
- };
8037
- if (entry.escalation.necessity < threshold) return {
8038
- kind: "delegate",
8039
- reason: `sandbox necessity probability ${entry.escalation.necessity.toFixed(3)} below ${threshold}`,
8040
- probability: entry.probability,
8041
- command
8042
- };
8043
- return {
8044
- kind: "approve",
8045
- probability: entry.probability,
8046
- source,
8047
- ...entry.escalation === void 0 ? {} : { escalation: entry.escalation },
8048
- command
8049
- };
8050
- }
8051
- };
8052
- /**
8053
- * Register the guard on a context that has the approval seam.
8054
- *
8055
- * Call this from inside `ctx.inject(['approval'], …)`: the seam's presence is
8056
- * what makes the feature meaningful (no approval service means no asks), and
8057
- * tying the listeners to that injected fiber is what makes them go away with it.
8058
- *
8059
- * @param ctx - the injected context (or a test double with `on`).
8060
- * @param deps - settings, the decision client, clock and logger.
8061
- * @returns a disposer for both listeners.
8062
- */
8063
- function applyCommandGuard(ctx, deps) {
8064
- const guard = new CommandGuard(deps);
8065
- const events = ctx;
8066
- const offPreExecute = events.on("tools/pre-execute", (execution, next) => {
8067
- guard.noteExecution(execution);
8068
- return next();
8069
- }, { prepend: true });
8070
- const offApproval = events.on("approval/request", async (request, next) => {
8071
- try {
8072
- if (!deps.settings().enabled) return next();
8073
- } catch (error) {
8074
- deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
8075
- return next();
8076
- }
8077
- let verdict;
8078
- try {
8079
- verdict = await guard.judge(request);
8080
- } catch (error) {
8081
- deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
8082
- return next();
8083
- }
8084
- if (verdict.kind === "approve") {
8085
- 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)"}`);
8086
- return "allowed-once";
8087
- }
8088
- deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
8089
- return next();
8090
- }, { prepend: true });
8091
- return () => {
8092
- guard.clear();
8093
- for (const off of [offPreExecute, offApproval]) if (typeof off === "function") off();
8094
- };
8095
- }
8096
- //#endregion
8097
7227
  //#region src/config-volatile.ts
8098
7228
  /**
8099
7229
  * Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
8100
7230
  *
8101
- * 0.1.7 rewrote settings around profile Config: `settings.describe()` projects
8102
- * each active entry's schema through `volatileForm()`, so a form contains ONLY
8103
- * fields whose schema nodes carry `meta.volatile`, and a form edit is refused
8104
- * unless its path lies beneath a marked node. On that generation the loader
8105
- * resolves each marked top-level field into a readonly `Volatile<T>` reference
8106
- * (`{ get() }`, frozen, written in place by the owning runtime) instead of a
8107
- * plain value, commits later changes WITHOUT remounting the fiber, and
8108
- * dispatches `loader/volatile-update` to the owning fiber.
8109
- *
8110
- * Two helpers keep the rest of the plugin reading a PLAIN `Config`:
8111
- *
8112
- * - `markVolatile(schema)` applies `.volatile()`, so 0.1.7's forms see every
8113
- * field we mark. The declared type is preserved through the mark on purpose:
8114
- * it changes what the schema PARSES to (a live reference), not the
8115
- * plugin-facing `Config` shape every consumer reads.
8116
- * - `unwrapVolatileConfig(config)` reads every top-level field through its
8117
- * reference when one is present — returning a FRESH object per call, since
8118
- * a reference's identity is stable while its value changes — and returns the
8119
- * config untouched when none is.
8120
- *
8121
- * Only TOP-LEVEL fields are marked (form writes name top-level paths such as
8122
- * `accounts` or `modelVisibility.<id>`, whose first segment is the marked
8123
- * node), so one unwrap level is complete. The literal `apiKey` secret stays
8124
- * unmarked on purpose: no settings surface writes it — the settings page and
8125
- * the dsh-TUI section both write keys through the credentials seam — so it
8126
- * keeps composition-only semantics, and a config-file edit to it reloading the
8127
- * 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.
8128
7247
  *
8129
7248
  * @module dsh-commandcode-provider/config-volatile
8130
7249
  */
8131
7250
  /**
8132
7251
  * Mark one schema field volatile.
8133
7252
  *
8134
- * `.volatile()` is unconditional: the mark is a schemastery 3.18.3 feature and
8135
- * the only engine this bundle supports (`dsh 0.1.7-rc.2`) pins `~3.18.4`, so
8136
- * the method is always present. It is reached through a structural member
8137
- * rather than schemastery's own typing so the function can keep the caller's
8138
- * declared field type, which is what lets `Config` stay the plain shape every
8139
- * consumer reads while the schema parses to live references.
8140
- *
8141
- * @param schema - The field schema.
8142
- * @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.
8143
7258
  */
8144
7259
  function markVolatile(schema) {
8145
7260
  return schema.volatile();
@@ -8147,10 +7262,6 @@ function markVolatile(schema) {
8147
7262
  /**
8148
7263
  * Mark every field of a Config field dict volatile, except the named
8149
7264
  * composition-only secrets (`apiKey` here, which no settings surface writes).
8150
- *
8151
- * @param fields - Freshly built field schemas.
8152
- * @param skip - Field names that must stay unmarked.
8153
- * @returns The field dict with every non-skipped field marked.
8154
7265
  */
8155
7266
  function markVolatileFields(fields, skip = []) {
8156
7267
  return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
@@ -8172,8 +7283,8 @@ function isVolatileRef(value) {
8172
7283
  *
8173
7284
  * Every top-level field found to be a volatile reference is read through
8174
7285
  * `.get()` — freshly on every call, so a live update lands on the very next
8175
- * read. The loader resolves EVERY marked node into a reference, unset fields
8176
- * 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
8177
7288
  * pass-through for a field that is not a reference is what keeps the unmarked
8178
7289
  * `apiKey` literal readable.
8179
7290
  */
@@ -8188,15 +7299,14 @@ function unwrapVolatileConfig(config) {
8188
7299
  //#region src/index.ts
8189
7300
  /**
8190
7301
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
8191
- * Code (unofficial; ported from pi-commandcode-provider@0.5.1).
7302
+ * Code (unofficial; ported from pi-commandcode-provider@0.5.1, MIT).
8192
7303
  *
8193
7304
  * Registers the `commandcode` provider route on `ctx.llm` and declares it in
8194
7305
  * the configurable-provider directory, so the web Models page shows a
8195
- * "Command Code" card with an API-key field and the model picker lists the
8196
- * live Command Code model catalog. Connection facts resolve per request over
8197
- * the plugin's live profile Config (volatile fields, unwrapped per read) and
8198
- * the credential seam, so a changed key, endpoint, or cache path reaches the
8199
- * 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.
8200
7310
  *
8201
7311
  * ```yaml
8202
7312
  * - id: llm-commandcode
@@ -8223,14 +7333,12 @@ const PROVIDER = "commandcode";
8223
7333
  const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
8224
7334
  /**
8225
7335
  * The Config schema: every field volatile except the composition-only `apiKey`
8226
- * secret.
8227
- *
8228
- * dsh 0.1.7's settings forms are projected from the schema's `meta.volatile`
8229
- * nodes, so an unmarked field would be invisible to AND unwritable from the
8230
- * settings page and refused by form-edit path validation — and the loader
8231
- * hands `apply()` a live reference per marked field, committing later writes
8232
- * without remounting this fiber (the `loader/volatile-update` listener at the
8233
- * 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.
8234
7342
  */
8235
7343
  const Config = z.object(markVolatileFields({
8236
7344
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
@@ -8243,14 +7351,6 @@ const Config = z.object(markVolatileFields({
8243
7351
  transportMaxRetries: z.number().min(0).max(50),
8244
7352
  filterModelsByPlan: z.boolean(),
8245
7353
  visibleModels: z.array(z.string()),
8246
- /**
8247
- * Per-model visibility overrides for the terminal settings page's checkbox
8248
- * list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
8249
- * PATH, so two checkboxes sharing one path would overwrite each other's
8250
- * draft and the section's last model would decide every write; a map gives
8251
- * each checkbox a path of its own. An id listed here wins over
8252
- * {@link Config.visibleModels}; ids absent here keep following it.
8253
- */
8254
7354
  modelVisibility: z.dict(z.boolean()),
8255
7355
  webSearch: z.boolean().default(true),
8256
7356
  showSidebarQuota: z.boolean().default(false),
@@ -8265,8 +7365,6 @@ const Config = z.object(markVolatileFields({
8265
7365
  models: z.array(z.string()),
8266
7366
  account: z.string()
8267
7367
  })),
8268
- commandGuard: z.boolean().default(false),
8269
- commandGuardLevel: z.union([...COMMAND_GUARD_LEVELS]).default(COMMAND_GUARD_DEFAULT_LEVEL),
8270
7368
  zdr: z.boolean().default(false),
8271
7369
  lang: z.string().pattern(/^(zh|en)$/).default("zh")
8272
7370
  }, ["apiKey"]));
@@ -8274,7 +7372,7 @@ const Config = z.object(markVolatileFields({
8274
7372
  * The one explicit resolve step from raw config to validated connection
8275
7373
  * facts. Programmatic construction may bypass Schemastery normalization, so
8276
7374
  * every default is re-judged here — for the composition entry at load and for
8277
- * each settings snapshot at its first use.
7375
+ * every settings-backed read.
8278
7376
  */
8279
7377
  function resolveAdapterOptions(config) {
8280
7378
  return {
@@ -8291,11 +7389,10 @@ function resolveAdapterOptions(config) {
8291
7389
  };
8292
7390
  }
8293
7391
  /**
8294
- * Per-model visibility overrides, cleaned for the adapter. Programmatic
8295
- * construction may bypass Schemastery normalization, so a non-object or a
8296
- * non-boolean entry is dropped here instead of reaching the picker filter.
8297
- * @param raw - The `modelVisibility` value from any config source.
8298
- * @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.
8299
7396
  */
8300
7397
  function readModelVisibility(raw) {
8301
7398
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
@@ -8493,26 +7590,6 @@ function apply(ctx, config) {
8493
7590
  applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
8494
7591
  }, "dsh-commandcode-provider: web search selection");
8495
7592
  });
8496
- const commandGuardSettings = () => {
8497
- const raw = current();
8498
- return {
8499
- enabled: raw.commandGuard === true,
8500
- threshold: commandGuardThreshold(raw.commandGuardLevel),
8501
- timeoutMs: COMMAND_GUARD_TIMEOUT_MS
8502
- };
8503
- };
8504
- ctx.inject(["approval"], (approvalCtx) => {
8505
- applyCommandGuard(approvalCtx, {
8506
- settings: commandGuardSettings,
8507
- decide: (request, decideOptions) => runSystemOne({
8508
- apiBase: () => options().apiBase,
8509
- resolveApiKey: () => resolveApiKey(options())
8510
- }, request, decideOptions),
8511
- log: (message) => {
8512
- ctx.logger.info(`llm-commandcode: ${message}`);
8513
- }
8514
- });
8515
- });
8516
7593
  let refreshTuiSettings;
8517
7594
  ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
8518
7595
  refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {