@mars-sea/dsh-commandcode-provider 0.13.3 → 0.14.0

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
@@ -103,7 +103,7 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
103
103
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
104
104
  *
105
105
  * Everything here is synced from official sources — the command-code CLI
106
- * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.82.1) and
106
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.82.5) and
107
107
  * the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
108
108
  * for the extraction procedures. Keeping the snapshot in its own module
109
109
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
@@ -138,6 +138,11 @@ const KNOWN_EFFORTS = {
138
138
  "medium",
139
139
  "xhigh"
140
140
  ],
141
+ "Qwen/Qwen3.8-Max-Prime": [
142
+ "low",
143
+ "medium",
144
+ "xhigh"
145
+ ],
141
146
  "claude-fable-5-1": [
142
147
  "low",
143
148
  "medium",
@@ -391,6 +396,16 @@ const KNOWN_EFFORTS = {
391
396
  "high",
392
397
  "xhigh"
393
398
  ],
399
+ "meta/muse-spark-open": [
400
+ "low",
401
+ "medium",
402
+ "high"
403
+ ],
404
+ "nvidia/nemotron-3.5-super-vl": [
405
+ "low",
406
+ "medium",
407
+ "high"
408
+ ],
394
409
  "gpt-6-astra": [
395
410
  "low",
396
411
  "medium",
@@ -453,6 +468,13 @@ const KNOWN_EFFORTS = {
453
468
  "medium",
454
469
  "high",
455
470
  "xhigh"
471
+ ],
472
+ "stealth/cat-jam:free": [
473
+ "low",
474
+ "medium",
475
+ "high",
476
+ "xhigh",
477
+ "max"
456
478
  ]
457
479
  };
458
480
  /**
@@ -480,6 +502,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
480
502
  "Qwen/Qwen3.8-Max",
481
503
  "Qwen/Qwen3.8-Max-0902",
482
504
  "Qwen/Qwen3.8-Omni-Flash",
505
+ "Qwen/Qwen3.8-Max-Prime",
483
506
  "claude-fable-5-1",
484
507
  "claude-fable-5",
485
508
  "claude-haiku-4-5-20251001",
@@ -518,6 +541,8 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
518
541
  "meta/muse-spark-1.3",
519
542
  "meta/muse-spark-1.3-contributor",
520
543
  "meta/muse-wonder-1.0",
544
+ "meta/muse-spark-open",
545
+ "nvidia/nemotron-3.5-super-vl",
521
546
  "mistral/mistral-large-4",
522
547
  "moonshotai/Kimi-K2.5",
523
548
  "moonshotai/Kimi-K2.6",
@@ -540,14 +565,16 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
540
565
  "z-ai/glm-5.3-flashx"
541
566
  ]);
542
567
  /**
543
- * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
544
- * registry (`command-code@1.82.1` `dist/cli.mjs`): `modelSupportsZdr(id)` is
545
- * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
546
- * carries the same membership in the sibling route table — the UNION of both
547
- * is this set. Reading only the sibling route table dropped
548
- * `meituan/LongCat-2.0`, which sat in `modelSupportsZdr` alone through
549
- * 1.73.0; the 1.74.3 table lists it in NEITHER set, so as of this snapshot it
550
- * is no longer excluded. The official docs (commandcode.ai/docs/resources/
568
+ * Models WITHOUT a zero-data-retention upstream. Through command-code@1.82.2
569
+ * the CLI's own registry answered this: `modelSupportsZdr(id)` was exactly
570
+ * `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr` carried the
571
+ * membership in the sibling route table — the UNION of both was this set, and
572
+ * the two anchors disagreed by one id (`meituan/LongCat-2.0:free`, which 1.74.1
573
+ * dropped from the first anchor while the canonical helper kept it).
574
+ * command-code@1.82.4 removes that disagreement by making the verdict a catalog
575
+ * lookup — `resolveKnownCatalogModel({model,catalog})?.zdr ?? false` — so this
576
+ * set is now the live catalog's `zdr:false` membership and nothing more. The
577
+ * official docs (commandcode.ai/docs/resources/
551
578
  * zdr) put coverage in prose — "99% of our models have ZDR-capable upstreams … only
552
579
  * a small handful of models are affected" — so the CLI's exclusion list is
553
580
  * the only per-model evidence there is; a ZDR request naming one of these
@@ -560,10 +587,19 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
560
587
  * switch is on — the provider remains the routing authority and refuses an
561
588
  * unsupported model rather than silently dropping the privacy guarantee.
562
589
  *
563
- * `minimax/minimax-m3-free` is the one entry the public catalog
564
- * (`/provider/v1/models`) does not serve (it is CLI/pricing-visible and hidden
565
- * from the picker); it stays listed because the CLI carries it and a Go-plan
566
- * request can still name it.
590
+ * `minimax/minimax-m3-free` is the one entry the public catalog's default view
591
+ * (`/provider/v1/models`) does not serve through the picker (it is CLI/pricing-visible
592
+ * and hidden from the picker); it stays listed because the CLI carries it and a Go-plan
593
+ * request can still name it. Both this row and `stealth/cat-jam:free` ARE served
594
+ * by the `?view=client` catalog, which is where the 1.82.4 verdict comes from.
595
+ *
596
+ * command-code@1.82.4 moved the authority for this verdict: `knownModelSupportsZdr`
597
+ * no longer negates a bundled Set, it returns `resolveKnownCatalogModel({model,
598
+ * catalog})?.zdr ?? false`, so an id the catalog does not carry (or carries with
599
+ * `zdr:false`) is NOT covered. That inverts the default this helper was built on
600
+ * ("not listed" answered TRUE); the snapshot keeps its negative-set shape because
601
+ * 99% of the catalog is still covered, but membership is now read from the
602
+ * `?view=client` catalog and confirmed against the canonical helper set.
567
603
  *
568
604
  * Keep in sync via the dsh-commandcode-upstream skill: the CLI's registry data
569
605
  * (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
@@ -584,6 +620,11 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
584
620
  * `listSelectableModelIds()`, so they are pruned along with the rest of the
585
621
  * tables and `supportsZeroDataRetention` now answers TRUE for them, matching
586
622
  * what the current CLI says.
623
+ *
624
+ * 1.82.4 then reversed 1.74.1's removal of `meituan/LongCat-2.0:free` and added
625
+ * the `stealth/cat-jam:free` preview, taking this set to 24 members — the size
626
+ * of the live catalog's `zdr:false` list. The canonical helper set still carries
627
+ * 23 because `cat-jam:free` postdates the artifact's embedded snapshot.
587
628
  */
588
629
  const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
589
630
  "MiniMaxAI/MiniMax-M3",
@@ -595,10 +636,12 @@ const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
595
636
  "meta/muse-spark-1.3",
596
637
  "meta/muse-spark-1.3-contributor",
597
638
  "meta/muse-wonder-1.0",
639
+ "meituan/LongCat-2.0:free",
598
640
  "minimax/minimax-m3-free",
599
641
  "poolside/laguna-s-2.1-free",
600
642
  "sakana/fugu-ultra",
601
643
  "stealth/glyph-cluster:free",
644
+ "stealth/cat-jam:free",
602
645
  "stepfun/Step-3.7-Flash",
603
646
  "stepfun/Step-5-Preview",
604
647
  "xai/grok-4.5",
@@ -646,8 +689,7 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
646
689
  "thinkingmachines/inkling",
647
690
  "thinkingmachines/inkling-small",
648
691
  "poolside/laguna-s-2.1-free",
649
- "meituan/LongCat-2.0",
650
- "inclusionai/ling-3.0-flash-sante:free"
692
+ "meituan/LongCat-2.0"
651
693
  ]);
652
694
  /**
653
695
  * Catalog models the Provider API serves ONLY through `/provider/v1/messages`
@@ -715,11 +757,12 @@ function requiresMessagesEndpoint(modelId) {
715
757
  * catalog ids as of 2026-10-15. Each row carries a comment naming the entry it
716
758
  * came from.
717
759
  *
718
- * One of those 76 rests on more than a vendor lookup, hand-recorded in the
719
- * script's `MANUAL_CEILINGS`: `inclusionai/ling-3.0-flash-sante:free` (32768)
720
- * lost both its vendor row and its unanimous third-party vote (vercel files
721
- * 32000), and is pinned so a regeneration cannot drop a live model's ceiling
722
- * back to the default. (`gpt-6.1-sol` (128000) needs no pin — OpenAI's own row
760
+ * No row needs a hand-recorded pin as of 2026-10-16: the one that did
761
+ * (`inclusionai/ling-3.0-flash-sante:free`, pinned at 32768 because it had lost
762
+ * both its vendor row and its unanimous third-party vote) left the live catalog
763
+ * with the 1.82.4 catalog migration, so the script's `MANUAL_CEILINGS` map is
764
+ * now empty and every remaining row resolves from a vendor row or a unanimous
765
+ * third-party vote. (`gpt-6.1-sol` (128000) never needed a pin — OpenAI's own row
723
766
  * carries it and every other provider carrying the id agrees.) All are still
724
767
  * correctable — a stricter gateway costs one refused request, and
725
768
  * `streamRequest()` remembers what the endpoint said.
@@ -769,7 +812,6 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
769
812
  ["gpt-6-luna", 128e3],
770
813
  ["gpt-6-sol", 128e3],
771
814
  ["gpt-6.1-sol", 128e3],
772
- ["inclusionai/ling-3.0-flash-sante:free", 32768],
773
815
  ["meituan/LongCat-2.0", 131072],
774
816
  ["meta/muse-spark-1.1", 131072],
775
817
  ["meta/muse-spark-1.2", 131072],
@@ -820,15 +862,20 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
820
862
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
821
863
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
822
864
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
823
- * Provider-tier. Re-verified at command-code@1.82.1 (2026-10-14): 89 catalog
824
- * ids at 54/64/79/87/89 cumulative, a strict superset chain. Mistral Large 4
865
+ * Provider-tier. Re-verified at command-code@1.82.4 (2026-10-16) against the
866
+ * live `?view=client` catalog: 93 ids, still a strict superset chain. Mistral
867
+ * Large 4
825
868
  * (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
826
869
  * added Claude Haiku 5.5 at Pro and the free Glyph Cluster preview at Go.
827
870
  * 1.79.2 lowers Haiku 5.5's minimum plan to GOAT; 1.80.0 adds Muse Wonder at
828
- * Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. Every other
871
+ * Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. The 1.82.4
872
+ * train adds Qwen 3.8 Max Prime at GOAT plus Muse Spark Open, Nemotron 3.5
873
+ * Super VL and the free Cat Jam preview at Go, and drops Ling 3.0 Flash Sante,
874
+ * which left the live catalog. Every other
829
875
  * tier is unchanged.
830
876
  * The only removals since 1.49.0 are the two retired stealth previews (Pixel
831
- * Canary at 1.73.1, Space Bunny Alpha at 1.74.3). Per-entry tags below name the
877
+ * Canary at 1.73.1, Space Bunny Alpha at 1.74.3) and Ling 3.0 Flash Sante
878
+ * (2026-10-16). Per-entry tags below name the
832
879
  * releases that added or moved each row.
833
880
  *
834
881
  * The Provider API exposes no plan metadata, so this snapshot is the source of
@@ -852,6 +899,7 @@ const KNOWN_PLANS = {
852
899
  "Qwen/Qwen3.8-Flash": "go",
853
900
  "Qwen/Qwen3.8-Max": "go",
854
901
  "Qwen/Qwen3.8-Max-0902": "go",
902
+ "Qwen/Qwen3.8-Max-Prime": "goat",
855
903
  "Qwen/Qwen3.8-Omni-Flash": "go",
856
904
  "deepseek/deepseek-v4-flash-fast": "go",
857
905
  "deepseek/deepseek-v4.1-flash": "go",
@@ -862,10 +910,10 @@ const KNOWN_PLANS = {
862
910
  "gpt-5.6-luna": "go",
863
911
  "gpt-6-luna": "go",
864
912
  "meituan/LongCat-2.0": "go",
865
- "inclusionai/ling-3.0-flash-sante:free": "go",
866
913
  "inclusionai/ling-3.1-flash": "go",
867
914
  "meta/muse-spark-1.2-contributor": "go",
868
915
  "meta/muse-spark-1.3-contributor": "go",
916
+ "meta/muse-spark-open": "go",
869
917
  "mistral/mistral-large-4": "go",
870
918
  "moonshotai/Kimi-K2.5": "go",
871
919
  "moonshotai/Kimi-K2.6": "go",
@@ -873,10 +921,12 @@ const KNOWN_PLANS = {
873
921
  "moonshotai/Kimi-K2.7-Code-Highspeed": "go",
874
922
  "moonshotai/Kimi-K3": "go",
875
923
  "nvidia/nemotron-3-ultra-550b-a55b": "go",
924
+ "nvidia/nemotron-3.5-super-vl": "go",
876
925
  "poolside/laguna-s-2.1-free": "go",
877
926
  "stepfun/Step-3.5-Flash": "go",
878
927
  "stepfun/Step-3.7-Flash": "go",
879
928
  "stepfun/Step-5-Preview": "go",
929
+ "stealth/cat-jam:free": "go",
880
930
  "stealth/glyph-cluster:free": "go",
881
931
  "tencent/hy3-paid": "go",
882
932
  "tencent/hy4-preview": "go",
@@ -1069,7 +1119,7 @@ const KNOWN_DEALS = {
1069
1119
  label: "FREE",
1070
1120
  free: true
1071
1121
  },
1072
- "inclusionai/ling-3.0-flash-sante:free": {
1122
+ "stealth/cat-jam:free": {
1073
1123
  label: "FREE",
1074
1124
  free: true
1075
1125
  },
@@ -1825,8 +1875,8 @@ const MODEL_PRICE_ROWS = [
1825
1875
  ],
1826
1876
  allowance: {
1827
1877
  go: 6,
1828
- goat: 70,
1829
- pro: 80
1878
+ goat: 20,
1879
+ pro: 30
1830
1880
  },
1831
1881
  contextTiers: [{
1832
1882
  "maxContext": 272e3,
@@ -2146,8 +2196,8 @@ const MODEL_PRICE_ROWS = [
2146
2196
  ],
2147
2197
  allowance: {
2148
2198
  go: 6,
2149
- goat: 20,
2150
- pro: 30
2199
+ goat: 100,
2200
+ pro: 110
2151
2201
  }
2152
2202
  },
2153
2203
  {
@@ -2360,6 +2410,19 @@ const MODEL_PRICE_ROWS = [
2360
2410
  pro: 30
2361
2411
  }
2362
2412
  },
2413
+ {
2414
+ id: "muse-spark-open",
2415
+ rates: [
2416
+ 1.25,
2417
+ 4.25,
2418
+ .15
2419
+ ],
2420
+ allowance: {
2421
+ go: 6,
2422
+ goat: 20,
2423
+ pro: 30
2424
+ }
2425
+ },
2363
2426
  {
2364
2427
  id: "muse-wonder-1.0",
2365
2428
  rates: [
@@ -2386,6 +2449,19 @@ const MODEL_PRICE_ROWS = [
2386
2449
  pro: 30
2387
2450
  }
2388
2451
  },
2452
+ {
2453
+ id: "nemotron-3.5-super-vl",
2454
+ rates: [
2455
+ .2,
2456
+ .5,
2457
+ .04
2458
+ ],
2459
+ allowance: {
2460
+ go: 6,
2461
+ goat: 20,
2462
+ pro: 30
2463
+ }
2464
+ },
2389
2465
  {
2390
2466
  id: "qwen-3.6-max",
2391
2467
  rates: [
@@ -2664,6 +2740,19 @@ const MODEL_PRICE_ROWS = [
2664
2740
  goat: 20,
2665
2741
  pro: 30
2666
2742
  }
2743
+ },
2744
+ {
2745
+ id: "typesafe/jev-1.14.0",
2746
+ rates: [
2747
+ .042,
2748
+ 0,
2749
+ 0
2750
+ ],
2751
+ allowance: {
2752
+ go: 6,
2753
+ goat: 20,
2754
+ pro: 30
2755
+ }
2667
2756
  }
2668
2757
  ];
2669
2758
  /**
@@ -3552,6 +3641,175 @@ function modelIsVisible(id, allowlist, overrides) {
3552
3641
  const flag = overrides !== null && typeof overrides === "object" && !Array.isArray(overrides) ? overrides[id] : void 0;
3553
3642
  return typeof flag === "boolean" ? flag : allowlist.length === 0 || allowlist.includes(id);
3554
3643
  }
3644
+ /** 自定义上下文长度接受的上界(token);覆盖任何已知模型窗口仍留有余量。 */
3645
+ const CONTEXT_WINDOW_MAX = 1e7;
3646
+ /**
3647
+ * 判定一个值能否作为自定义上下文长度:边界内的安全整数,符合宿主模型元数据约束。
3648
+ *
3649
+ * 程序化构造的连接可能绕过 schemastery 归一化,所以 `resolveAdapterOptions`
3650
+ * 也要重新判定——越界或非数值直接丢弃,回落到目录公布值,而不是让请求带着
3651
+ * 一个网关会拒绝的窗口上线。
3652
+ */
3653
+ function isValidContextWindow(value) {
3654
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 1e7;
3655
+ }
3656
+ /**
3657
+ * 只接收合法条目的按模型覆盖表;空表、非对象与非法数值一律视为未设置。
3658
+ * 配置校验会拒绝非整数或越界值;本函数额外保护绕过配置校验的程序化调用,
3659
+ * 丢弃坏条目,不把非法窗口交给宿主。返回新对象;未设置时为 `undefined`。
3660
+ */
3661
+ function readContextWindowOverrides(raw) {
3662
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
3663
+ const entries = Object.entries(raw).filter((entry) => entry[0] !== "" && isValidContextWindow(entry[1]));
3664
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
3665
+ }
3666
+ /**
3667
+ * 一次模型解析的有效上下文长度:按模型覆盖 → 全局值 → 目录公布值。
3668
+ *
3669
+ * 适配器只在有目录条目时调用(未知模型保持既有的「无上下文宣告」形状),
3670
+ * 所以 `catalog` 是已知的正数;按模型覆盖与全局值在 `resolveAdapterOptions`
3671
+ * 已清洗,这里对程序化构造的连接再判一次,坏值静默回落到目录值。
3672
+ */
3673
+ function effectiveContextWindow(catalog, model, options) {
3674
+ const perModel = options.contextWindowOverrides?.[model];
3675
+ if (isValidContextWindow(perModel)) return perModel;
3676
+ if (isValidContextWindow(options.contextWindow)) return options.contextWindow;
3677
+ return catalog;
3678
+ }
3679
+ //#endregion
3680
+ //#region src/output-budget.ts
3681
+ /**
3682
+ * Output-budget bounds shared by the Host, the browser settings page and the
3683
+ * adapter (issue #74).
3684
+ *
3685
+ * The module stays pure (no node imports) so the client bundle can inline it,
3686
+ * exactly like `context-window.ts`. The report behind issue #74 asked for a
3687
+ * knob that stops the adapter from writing `max_tokens` at all; that is the
3688
+ * `0` sentinel here rather than a separate `null` spelling, because the value
3689
+ * travels through one numeric settings field on both surfaces.
3690
+ */
3691
+ /**
3692
+ * The output budget this bundle asks for, and the ceiling it will never exceed.
3693
+ *
3694
+ * 131 072, chosen against a measured provider limit rather than a round guess.
3695
+ * Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
3696
+ * 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
3697
+ * therefore caps `max_tokens` at 200 000, and it does so identically for a
3698
+ * 262 144-window model (`stealth/pixel-canary`, retired by Command Code on
3699
+ * 2026-10-01 and named here only as the probe's subject) and a 1 000 000-window
3700
+ * one (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
3701
+ * keeps a 35 % margin under it while covering the real output ceiling of the
3702
+ * models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
3703
+ *
3704
+ * The old 64 000 was never a provider requirement: it cost nothing in latency
3705
+ * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
3706
+ * it did cap what a model could ever say in one turn.
3707
+ *
3708
+ * No client-side clamp is applied on top of this: an earlier version pre-shrank
3709
+ * `max_tokens` from an estimate of the prompt already sent (`requestContextBudget()`,
3710
+ * issue #67), but that estimate over-corrected on ordinary long conversations —
3711
+ * exactly the "output gets cut short as the conversation grows" symptom issue #74
3712
+ * reported — while a live A/B probe (2026-09-30, `stealth/space-bunny-alpha`,
3713
+ * ~19.6% over its 1,000,000-token window with no client-side shrink) showed the
3714
+ * endpoint answers a genuine overflow with an unambiguous `400 … exceeds the
3715
+ * model's context window`, which `isContextOverflowDetail()` classifies as
3716
+ * `CONTEXT_WINDOW_EXCEEDED` independently of this constant.
3717
+ *
3718
+ * A user-set positive budget can only TIGHTEN this, never raise it — a figure
3719
+ * above the cap would be refused by the gateway anyway, so the settings field
3720
+ * refuses it at the input instead of spending a round trip to learn that.
3721
+ *
3722
+ * Lives here rather than in `adapter.ts` so the browser settings page can bound
3723
+ * its input by the same figure the Host enforces; `adapter.ts` re-exports it
3724
+ * under its historical name.
3725
+ */
3726
+ const MAX_OUTPUT_TOKEN_LIMIT = 131072;
3727
+ /**
3728
+ * Whether a value can serve as a user output budget: the omit sentinel, or a
3729
+ * whole token count inside the gateway's own ceiling.
3730
+ *
3731
+ * Programmatically built connections can bypass schemastery normalization, so
3732
+ * the resolve step re-judges every value here and drops an out-of-range one
3733
+ * back to "unset" instead of letting the request carry a budget the gateway
3734
+ * refuses.
3735
+ */
3736
+ function isValidMaxOutputTokens(value) {
3737
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 131072;
3738
+ }
3739
+ /**
3740
+ * Apply a user output budget to a derived one.
3741
+ *
3742
+ * `undefined` means the user set nothing, so the derived ceiling stands
3743
+ * unchanged — the default path, and the one every existing profile takes.
3744
+ * {@link OMIT_OUTPUT_TOKENS} is a wire instruction rather than a number, so it
3745
+ * also leaves the derived budget alone; {@link omitOutputTokens} is what turns
3746
+ * it into an absent field.
3747
+ */
3748
+ function applyMaxOutputTokens(derived, configured) {
3749
+ if (configured === void 0 || configured === 0) return derived;
3750
+ return Math.min(derived, configured);
3751
+ }
3752
+ /**
3753
+ * Whether this attempt must send no `max_tokens` field at all.
3754
+ *
3755
+ * Two guards narrow this beyond the user's own setting, and both are
3756
+ * correctness requirements rather than conveniences:
3757
+ *
3758
+ * - The Messages route is excluded deliberately: the endpoint rejects a body
3759
+ * without the field (`Invalid input: expected number, received undefined at
3760
+ * max_tokens`), so omitting it there would not lengthen an answer, it would
3761
+ * make the whole Claude family unusable. The CLI and OpenAI transports accept
3762
+ * an absent field, which is where the setting does what it promises.
3763
+ * - An explicit per-call budget from the host is an instruction, not a default,
3764
+ * so it wins over the plugin-wide setting. `session-title` is the concrete
3765
+ * case: it asks for 64 tokens precisely because its output must stay bounded,
3766
+ * and dropping the field there would let a title generation run long instead
3767
+ * of making a long answer fit (issue #88's fix depends on that bound).
3768
+ */
3769
+ function omitOutputTokens(configured, protocol, hostRequestedTokens) {
3770
+ if (configured !== 0) return false;
3771
+ if (protocol === "messages") return false;
3772
+ return hostRequestedTokens === void 0;
3773
+ }
3774
+ //#endregion
3775
+ //#region src/readable-error.ts
3776
+ /**
3777
+ * Readable error text for the settings page AND the Host-side failure paths.
3778
+ *
3779
+ * @module dsh-commandcode-provider/readable-error
3780
+ */
3781
+ /**
3782
+ * Turn a caught value into readable text.
3783
+ *
3784
+ * `error instanceof Error ? error.message : String(error)` degrades to the
3785
+ * literal string `[object Object]` for a plain object with no `Error`
3786
+ * prototype — which is exactly what a Remote call across the webview/host
3787
+ * boundary can hand back: `dsh-typert-protocol` serializes a rejection for
3788
+ * the wire, and a structurally-cloned value loses its prototype chain even
3789
+ * when the host threw a real `Error` (issue #74). Reading a `message` string
3790
+ * off the plain object first recovers the original text in that case; only a
3791
+ * value with neither an `Error` prototype nor a usable `message` field falls
3792
+ * through to a JSON dump.
3793
+ *
3794
+ * Shared by both halves on purpose. The browser client needs it for rejections
3795
+ * that crossed the RPC boundary, and the Host needs it for values that reached
3796
+ * it from a transport, a JSON body, or a user-thrown non-Error — the same
3797
+ * degradation, one owner.
3798
+ */
3799
+ function readableErrorText(error) {
3800
+ if (error instanceof Error) return error.message;
3801
+ if (typeof error === "string") return error;
3802
+ if (error !== null && typeof error === "object") {
3803
+ const message = error.message;
3804
+ if (typeof message === "string" && message !== "") return message;
3805
+ try {
3806
+ return JSON.stringify(error);
3807
+ } catch {
3808
+ return Object.prototype.toString.call(error);
3809
+ }
3810
+ }
3811
+ return String(error);
3812
+ }
3555
3813
  const THROTTLED_CODE = "THROTTLED";
3556
3814
  const boundedCodes = /* @__PURE__ */ new Set([
3557
3815
  "EMPTY_RESPONSE",
@@ -5858,6 +6116,13 @@ function learnedOutputLimit(apiBase, model) {
5858
6116
  function modelOutputTokenLimit(apiBase, model) {
5859
6117
  return Math.min(published.get(apiBase)?.get(model) ?? fallbackLimit(model), learnedOutputLimit(apiBase, model));
5860
6118
  }
6119
+ /**
6120
+ * 从本次目录记录的独立输出上限重新推导预算,不能复用已经被旧窗口压低的 maxTokens。
6121
+ * 公布值固定在该记录,不读取并发刷新后的共享目录;只继续采纳同网关更低的学习上限。
6122
+ */
6123
+ function catalogModelMaxTokens(apiBase, model, contextWindow = model.contextWindow) {
6124
+ return Math.min(contextWindow, model.publishedMaxTokens ?? fallbackLimit(model.id), learnedOutputLimit(apiBase, model.id));
6125
+ }
5861
6126
  /** 是否更新共享事实与是否值得重试是两个问题;以本次实发预算判断拒绝。 */
5862
6127
  function outputCeilingRefusal(apiBase, model, sent, error) {
5863
6128
  if (!(error instanceof LlmError) || !OUTPUT_LIMIT_REJECTION.test(error.message)) return void 0;
@@ -6013,37 +6278,15 @@ var GatewayFacts = class {
6013
6278
  * per-request options thunk and an API-key resolver from the plugin entry
6014
6279
  * (src/index.ts), so a settings change reaches the very next request.
6015
6280
  */
6016
- const COMMAND_CODE_CLI_VERSION = "1.82.2";
6281
+ const COMMAND_CODE_CLI_VERSION = "1.82.5";
6017
6282
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
6018
6283
  /**
6019
6284
  * The output budget this bundle asks for, and the ceiling it will never exceed.
6020
- *
6021
- * 131 072, chosen against a measured provider limit rather than a round guess.
6022
- * Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
6023
- * 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
6024
- * therefore caps `max_tokens` at 200 000, and it does so identically for a
6025
- * 262 144-window model (`stealth/pixel-canary`, retired by Command Code on
6026
- * 2026-10-01 and named here only as the probe's subject) and a 1 000 000-window
6027
- * one (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
6028
- * keeps a 35 % margin under it while covering the real output ceiling of the
6029
- * models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
6030
- *
6031
- * The old 64 000 was never a provider requirement: it cost nothing in latency
6032
- * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
6033
- * it did cap what a model could ever say in one turn.
6034
- *
6035
- * No client-side clamp is applied on top of this: an earlier version pre-shrank
6036
- * `max_tokens` from an estimate of the prompt already sent (`requestContextBudget()`,
6037
- * issue #67), but that estimate over-corrected on ordinary long conversations —
6038
- * exactly the "output gets cut short as the conversation grows" symptom issue #74
6039
- * reported — while a live A/B probe (2026-09-30, `stealth/space-bunny-alpha`,
6040
- * ~19.6% over its 1,000,000-token window with no client-side shrink) showed the
6041
- * endpoint answers a genuine overflow with an unambiguous `400 … exceeds the
6042
- * model's context window`, which `isContextOverflowDetail()` below already
6043
- * classifies as `CONTEXT_WINDOW_EXCEEDED` independently of this constant. See
6044
- * [决策记录](../docs/决策记录.md).
6285
+ * The figure and its probe evidence live in `./output-budget.ts` — the module
6286
+ * the browser settings page also bounds its input by — and are re-exported here
6287
+ * under the historical name so existing importers are unaffected.
6045
6288
  */
6046
- const DEFAULT_GENERATE_MAX_TOKENS = 131072;
6289
+ const DEFAULT_GENERATE_MAX_TOKENS = MAX_OUTPUT_TOKEN_LIMIT;
6047
6290
  const MODELS_TIMEOUT_MS = 1e4;
6048
6291
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
6049
6292
  const BILLING_ACCESS_TTL_MS = 3e5;
@@ -7274,7 +7517,7 @@ async function buildCliBody(options, connection, facts) {
7274
7517
  input_schema: toolParametersSchema(tool.parameters)
7275
7518
  })),
7276
7519
  system: cliSystem(facts.systemText),
7277
- max_tokens: facts.maxTokens,
7520
+ ...facts.omitMaxTokens ? {} : { max_tokens: facts.maxTokens },
7278
7521
  temperature: options.temperature ?? .3,
7279
7522
  stream: true,
7280
7523
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
@@ -7300,7 +7543,7 @@ async function buildOpenAIBody(options, facts) {
7300
7543
  model: options.model,
7301
7544
  messages: openAiMessages,
7302
7545
  ...openAiTools.length > 0 ? { tools: openAiTools } : {},
7303
- max_tokens: facts.maxTokens,
7546
+ ...facts.omitMaxTokens ? {} : { max_tokens: facts.maxTokens },
7304
7547
  temperature: options.temperature ?? .3,
7305
7548
  stream: true,
7306
7549
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
@@ -7363,6 +7606,16 @@ async function buildMessagesBody(options, facts) {
7363
7606
  return body;
7364
7607
  }
7365
7608
  /**
7609
+ * Write the output budget into an already-built body, each transport in its own
7610
+ * shape (the CLI route nests it under `params`). Used to keep a REBUILT body's
7611
+ * figure in step with the budget without duplicating the nesting knowledge, and
7612
+ * only called when the field is not being omitted (issue #74).
7613
+ */
7614
+ function assignMaxTokens(body, target, maxTokens) {
7615
+ if (target === "cli") recordOrEmpty(body.params).max_tokens = maxTokens;
7616
+ else body.max_tokens = maxTokens;
7617
+ }
7618
+ /**
7366
7619
  * One connect attempt's inputs: everything `connectGenerate` needs beyond
7367
7620
  * the per-attempt key and protocol. Passed explicitly (not closed over) so
7368
7621
  * the rotation loop's mutation of `protocol`/`body` stays visible at the
@@ -7676,7 +7929,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
7676
7929
  provider,
7677
7930
  id: model.id,
7678
7931
  name: `${model.name} (CC)`,
7679
- description: capabilityDescription(model.id, model.contextWindow),
7932
+ description: capabilityDescription(model.id, effectiveContextWindow(model.contextWindow, model.id, connection)),
7680
7933
  inputModalities: vision ? ["text", "image"] : ["text"]
7681
7934
  };
7682
7935
  };
@@ -7716,16 +7969,17 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
7716
7969
  const entry = this.gatewayFacts.knownModel(connection, model) ?? (await this.gatewayFacts.refresh(connection, signal)).find((m) => m.id === model);
7717
7970
  const efforts = KNOWN_EFFORTS[model];
7718
7971
  const vision = KNOWN_IMAGE_MODELS.has(model);
7972
+ const contextWindow = entry === void 0 ? void 0 : effectiveContextWindow(entry.contextWindow, model, connection);
7719
7973
  return {
7720
7974
  provider,
7721
7975
  id: model,
7722
7976
  name: entry ? `${entry.name} (CC)` : model,
7723
- description: capabilityDescription(model, entry?.contextWindow),
7977
+ description: capabilityDescription(model, contextWindow),
7724
7978
  inputModalities: vision ? ["text", "image"] : ["text"],
7725
7979
  toolUpdate: "addition-only",
7726
- ...entry ? {
7727
- context: { contextWindow: entry.contextWindow },
7728
- defaultMaxTokens: Math.min(entry.maxTokens, DEFAULT_GENERATE_MAX_TOKENS)
7980
+ ...entry !== void 0 && contextWindow !== void 0 ? {
7981
+ context: { contextWindow },
7982
+ defaultMaxTokens: applyMaxOutputTokens(Math.min(catalogModelMaxTokens(connection.apiBase, entry, contextWindow), DEFAULT_GENERATE_MAX_TOKENS), connection.maxOutputTokens)
7729
7983
  } : {},
7730
7984
  ...efforts ? { reasoning: { efforts: efforts.map((effort) => ({
7731
7985
  id: ReasoningEffortId(effort),
@@ -8005,7 +8259,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8005
8259
  }
8006
8260
  return record;
8007
8261
  } catch (error) {
8008
- failures.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
8262
+ failures.push(`${path}: ${readableErrorText(error)}`);
8009
8263
  failed.push({ kind: "transport" });
8010
8264
  return;
8011
8265
  }
@@ -8148,7 +8402,8 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8148
8402
  const catalog = await facts.catalog;
8149
8403
  options.signal?.throwIfAborted();
8150
8404
  const modelEntry = catalog.find((row) => row.id === options.model);
8151
- const initialMax = Math.min(options.maxTokens ?? Infinity, modelEntry?.maxTokens ?? facts.fallbackLimit, DEFAULT_GENERATE_MAX_TOKENS);
8405
+ const modelMax = modelEntry === void 0 ? facts.fallbackLimit : catalogModelMaxTokens(connection.apiBase, modelEntry, effectiveContextWindow(modelEntry.contextWindow, options.model, connection));
8406
+ const initialMax = applyMaxOutputTokens(Math.min(options.maxTokens ?? Infinity, modelMax, DEFAULT_GENERATE_MAX_TOKENS), connection.maxOutputTokens);
8152
8407
  const budget = {
8153
8408
  max: initialMax,
8154
8409
  sent: initialMax
@@ -8188,8 +8443,8 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8188
8443
  }
8189
8444
  options.signal?.throwIfAborted();
8190
8445
  let maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
8191
- const effort = options.reasoningEffort;
8192
8446
  const supported = KNOWN_EFFORTS[options.model];
8447
+ const effort = options.purpose === "session-title" && supported?.includes("off") ? "off" : options.reasoningEffort;
8193
8448
  const declared = effort !== void 0 && supported?.includes(effort) ? effort : void 0;
8194
8449
  const reasoningEffort = effort === "off" && declared === void 0 ? void 0 : declared;
8195
8450
  const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
@@ -8199,17 +8454,16 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8199
8454
  const buildBody = async (target) => {
8200
8455
  const finishBody = timing.phase("body");
8201
8456
  try {
8457
+ const omitMaxTokens = omitOutputTokens(connection.maxOutputTokens, target, options.maxTokens);
8202
8458
  const facts = {
8203
8459
  maxTokens,
8460
+ omitMaxTokens,
8204
8461
  reasoningEffort,
8205
8462
  selectableEffort: supported !== void 0 && supported.length > 0,
8206
8463
  systemText,
8207
8464
  readImage
8208
8465
  };
8209
- const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
8210
- if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
8211
- else built.max_tokens = maxTokens;
8212
- return built;
8466
+ return target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
8213
8467
  } finally {
8214
8468
  finishBody();
8215
8469
  }
@@ -8259,8 +8513,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8259
8513
  if (lower < maxTokens) {
8260
8514
  maxTokens = lower;
8261
8515
  budget.max = Math.min(budget.max, lower);
8262
- if (protocol === "cli") recordOrEmpty(body.params).max_tokens = lower;
8263
- else body.max_tokens = lower;
8516
+ if (!omitOutputTokens(connection.maxOutputTokens, protocol, options.maxTokens)) assignMaxTokens(body, protocol, lower);
8264
8517
  serializedBodies.delete(body);
8265
8518
  }
8266
8519
  budget.sent = maxTokens;
@@ -8447,12 +8700,33 @@ function formatMoney(value) {
8447
8700
  function formatMoneyExact(value) {
8448
8701
  return `$${value.toFixed(4)}`;
8449
8702
  }
8450
- function formatTokensCompact(value) {
8451
- if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
8452
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
8453
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
8703
+ /**
8704
+ * 紧凑数字:K/M/B 三级,一切档位决策的唯一实现。
8705
+ *
8706
+ * 档位必须**回头判断**:先除以档位再四舍五入会算出 `999_999 → "1000.0K"`、
8707
+ * `999_999_999 → "1000.0M"` 这类既超长、又落在并不存在的档位上的结果,
8708
+ * 因此每个档位算完后若已经够到下一档,就改用下一档重算一次。
8709
+ *
8710
+ * @param value - 非负计数或词元数。
8711
+ * @param keepZeroDecimal - 整数值是否保留一位小数。词元显示沿用 `3.0B` 的旧样式;
8712
+ * 计数显示去掉多余的 `.0`,见 {@link formatCompactCount}。
8713
+ * @returns 紧凑文本;不足 1000 时原样返回该整数。
8714
+ */
8715
+ function compact(value, keepZeroDecimal) {
8716
+ const rounded = (scaled) => Math.round(scaled * 10) / 10;
8717
+ const text = (scaled) => {
8718
+ const fixed = rounded(scaled);
8719
+ return keepZeroDecimal ? fixed.toFixed(1) : String(fixed);
8720
+ };
8721
+ if (value >= 1e9) return `${text(value / 1e9)}B`;
8722
+ if (value >= 1e6) return rounded(value / 1e6) >= 1e3 ? `${text(value / 1e9)}B` : `${text(value / 1e6)}M`;
8723
+ if (value >= 1e3) return rounded(value / 1e3) >= 1e3 ? `${text(value / 1e6)}M` : `${text(value / 1e3)}K`;
8454
8724
  return String(value);
8455
8725
  }
8726
+ /** 紧凑词元数:与 {@link formatCompactCount} 共用档位,但整数值保留一位小数(`3.0B`)。 */
8727
+ function formatTokensCompact(value) {
8728
+ return compact(value, true);
8729
+ }
8456
8730
  /** The input is already in percent units; callers add the percent sign. */
8457
8731
  function formatSuccessRate(value) {
8458
8732
  return String(Number(value.toFixed(2)));
@@ -8553,7 +8827,7 @@ function commandDefinition(deps) {
8553
8827
  text: renderReport(await adapter.getUsage(), locale)
8554
8828
  };
8555
8829
  } catch (error) {
8556
- const message = error instanceof Error ? error.message : String(error);
8830
+ const message = readableErrorText(error);
8557
8831
  return {
8558
8832
  kind: "error",
8559
8833
  text: commandCopy(locale, "errorText").replace("{message}", message)
@@ -8871,6 +9145,30 @@ const LOGIN_HOST_CONTRIBUTION = {
8871
9145
  };
8872
9146
  //#endregion
8873
9147
  //#region src/enrollment-wire.ts
9148
+ const ENROLLMENT_MESSAGE_KEYS = [
9149
+ "enrollmentCreating",
9150
+ "enrollmentWaiting",
9151
+ "enrollmentWriting",
9152
+ "enrollmentNaming",
9153
+ "enrollmentNameFailed",
9154
+ "enrollmentFinished",
9155
+ "enrollmentCancelling",
9156
+ "enrollmentCancelled",
9157
+ "enrollmentCancelledCleaned",
9158
+ "enrollmentFailed",
9159
+ "enrollmentFailedCleaned",
9160
+ "enrollmentCleanupPending",
9161
+ "enrollmentCleanupFailed",
9162
+ "enrollmentRecordNaming",
9163
+ "enrollmentRecordCleanup",
9164
+ "enrollmentBeginFailed",
9165
+ "enrollmentCancelUnconfirmed",
9166
+ "enrollmentNameRequestFailed",
9167
+ "enrollmentCleanupRequestFailed",
9168
+ "enrollmentDisconnected",
9169
+ "enrollmentHostUnavailable",
9170
+ "enrollmentStatusUnavailable"
9171
+ ];
8874
9172
  const guard = makeBoundaryValidator("commandcode/enrollment");
8875
9173
  const phases = [
8876
9174
  "creating",
@@ -8887,11 +9185,13 @@ function parseEnrollmentState(value) {
8887
9185
  const source = guard.record(value, "state");
8888
9186
  const phase = source.phase;
8889
9187
  if (!phases.includes(phase)) return guard.reject("phase");
9188
+ const messageKey = source.messageKey;
9189
+ if (!ENROLLMENT_MESSAGE_KEYS.includes(messageKey)) return guard.reject("messageKey");
8890
9190
  const state = {
8891
9191
  id: guard.stringField(source, "id", "id"),
8892
9192
  ref: guard.stringField(source, "ref", "ref"),
8893
9193
  phase,
8894
- message: guard.stringField(source, "message", "message")
9194
+ messageKey
8895
9195
  };
8896
9196
  for (const field of ["authUrl", "suggestedName"]) if (source[field] !== void 0) state[field] = guard.stringField(source, field, field);
8897
9197
  return state;
@@ -9583,7 +9883,7 @@ var CommandCodeLoginFlow = class {
9583
9883
  this.setStatus({
9584
9884
  state: "failed",
9585
9885
  reason: "unavailable",
9586
- message: error instanceof Error ? error.message : String(error)
9886
+ message: readableErrorText(error)
9587
9887
  });
9588
9888
  return;
9589
9889
  }
@@ -9692,7 +9992,7 @@ var AccountEnrollmentManager = class {
9692
9992
  id: input.id,
9693
9993
  ref: "",
9694
9994
  phase: "creating",
9695
- message: "正在创建账号"
9995
+ messageKey: "enrollmentCreating"
9696
9996
  }
9697
9997
  };
9698
9998
  this.tasks.set(input.id, task);
@@ -9700,7 +10000,7 @@ var AccountEnrollmentManager = class {
9700
10000
  task.state = {
9701
10001
  ...task.state,
9702
10002
  phase: "cleanup-needed",
9703
- message: "收尾未完成,请重试"
10003
+ messageKey: "enrollmentCleanupPending"
9704
10004
  };
9705
10005
  });
9706
10006
  return task.state;
@@ -9730,7 +10030,7 @@ var AccountEnrollmentManager = class {
9730
10030
  id,
9731
10031
  ref: "",
9732
10032
  phase: "cancelled",
9733
- message: "已取消"
10033
+ messageKey: "enrollmentCancelled"
9734
10034
  }
9735
10035
  };
9736
10036
  this.tasks.set(id, task);
@@ -9743,7 +10043,7 @@ var AccountEnrollmentManager = class {
9743
10043
  task.state = {
9744
10044
  ...task.state,
9745
10045
  phase: "cancelling",
9746
- message: "正在等待写入结束并清理"
10046
+ messageKey: "enrollmentCancelling"
9747
10047
  };
9748
10048
  await task.flow?.cancelAndDrain();
9749
10049
  await task.work;
@@ -9780,14 +10080,14 @@ var AccountEnrollmentManager = class {
9780
10080
  id,
9781
10081
  ref: record.ref,
9782
10082
  phase: "finished",
9783
- message: "账号开通完成"
10083
+ messageKey: "enrollmentFinished"
9784
10084
  });
9785
10085
  } catch {
9786
10086
  return this.publish(task, {
9787
10087
  id,
9788
10088
  ref: record.ref,
9789
10089
  phase: "naming",
9790
- message: "账号已登录,名称保存失败,请重试或接受已有名称",
10090
+ messageKey: "enrollmentNameFailed",
9791
10091
  ...name ? { suggestedName: name } : {}
9792
10092
  });
9793
10093
  }
@@ -9813,7 +10113,7 @@ var AccountEnrollmentManager = class {
9813
10113
  task.state = {
9814
10114
  ...task.state,
9815
10115
  phase: "cleanup-needed",
9816
- message: "清理未完成,请重试"
10116
+ messageKey: "enrollmentCleanupFailed"
9817
10117
  };
9818
10118
  });
9819
10119
  await task.work;
@@ -9881,7 +10181,7 @@ var AccountEnrollmentManager = class {
9881
10181
  if (!task.cancelled && status.state === "waiting") task.state = {
9882
10182
  ...task.state,
9883
10183
  phase: "waiting",
9884
- message: "等待浏览器登录",
10184
+ messageKey: "enrollmentWaiting",
9885
10185
  ...status.authUrl ? { authUrl: status.authUrl } : {}
9886
10186
  };
9887
10187
  if (task.cancelled) flow.cancelAndDrain();
@@ -9897,7 +10197,7 @@ var AccountEnrollmentManager = class {
9897
10197
  task.state = {
9898
10198
  ...task.state,
9899
10199
  phase: "naming",
9900
- message: "账号已登录,正在保存名称",
10200
+ messageKey: "enrollmentNaming",
9901
10201
  ...suggested ? { suggestedName: suggested } : {}
9902
10202
  };
9903
10203
  try {
@@ -9910,7 +10210,7 @@ var AccountEnrollmentManager = class {
9910
10210
  if (task.state.phase === "finished") return;
9911
10211
  task.state = {
9912
10212
  ...task.state,
9913
- message: "账号已登录,名称保存失败,请重试或接受已有名称"
10213
+ messageKey: "enrollmentNameFailed"
9914
10214
  };
9915
10215
  return;
9916
10216
  }
@@ -9922,7 +10222,7 @@ var AccountEnrollmentManager = class {
9922
10222
  else task.state = {
9923
10223
  ...task.state,
9924
10224
  phase: task.cancelled ? "cancelled" : "failed",
9925
- message: task.cancelled ? "已取消" : "账号开通失败,未创建账号"
10225
+ messageKey: task.cancelled ? "enrollmentCancelled" : "enrollmentFailed"
9926
10226
  };
9927
10227
  } finally {
9928
10228
  task.flow?.dispose();
@@ -9933,7 +10233,7 @@ var AccountEnrollmentManager = class {
9933
10233
  task.state = {
9934
10234
  ...task.state,
9935
10235
  phase: "writing",
9936
- message: "正在保存凭据"
10236
+ messageKey: "enrollmentWriting"
9937
10237
  };
9938
10238
  task.writeStarted = true;
9939
10239
  await this.deps.set(task.state.ref, key);
@@ -9974,7 +10274,7 @@ var AccountEnrollmentManager = class {
9974
10274
  id,
9975
10275
  ref,
9976
10276
  phase: task.cancelled ? "cancelled" : "failed",
9977
- message: task.cancelled ? "已取消并清理" : "开通失败,已清理临时账号"
10277
+ messageKey: task.cancelled ? "enrollmentCancelledCleaned" : "enrollmentFailedCleaned"
9978
10278
  };
9979
10279
  }
9980
10280
  settings() {
@@ -10023,12 +10323,134 @@ var AccountEnrollmentManager = class {
10023
10323
  id: record.id,
10024
10324
  ref: record.ref,
10025
10325
  phase: record.phase === "naming" ? "naming" : "cleanup-needed",
10026
- message: record.phase === "naming" ? "已登录账号等待确认名称" : "账号开通收尾未完成,请重试",
10326
+ messageKey: record.phase === "naming" ? "enrollmentRecordNaming" : "enrollmentRecordCleanup",
10027
10327
  ...record.phase === "naming" ? { suggestedName: record.label } : {}
10028
10328
  };
10029
10329
  }
10030
10330
  };
10031
10331
  //#endregion
10332
+ //#region src/config-volatile.ts
10333
+ /**
10334
+ * Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
10335
+ *
10336
+ * 0.1.7's `settings.describe()` projects each entry's schema through
10337
+ * `volatileForm()`, so a form contains ONLY nodes carrying `meta.volatile`
10338
+ * and a form edit is refused unless its path lies beneath a marked node. The
10339
+ * loader then resolves each marked field into a frozen `{ get() }` reference
10340
+ * that is written IN PLACE (no remount) and announced with
10341
+ * `loader/volatile-update`. Two helpers keep the rest of the plugin reading a
10342
+ * PLAIN `Config`: `markVolatile()` applies the mark, and
10343
+ * `unwrapVolatileConfig()` reads every top-level field through its reference
10344
+ * when one is present — FRESH per call, because a reference's identity is
10345
+ * stable while its value changes.
10346
+ *
10347
+ * Only TOP-LEVEL fields are marked (a form write names a top-level path such
10348
+ * as `accounts` or `modelVisibility.<id>`), so one unwrap level is complete.
10349
+ * The literal `apiKey` secret stays unmarked on purpose: no settings surface
10350
+ * writes it — page and TUI both write keys through the credentials seam — so a
10351
+ * config-file edit to it reloading the fiber is correct for a secret literal.
10352
+ *
10353
+ * `volatileWrite()` is the third helper and the only WRITE in this module: it
10354
+ * commits into a reference that belongs to ANOTHER plugin entry (`ctx.web`'s
10355
+ * `searchProvider`), which no host API addresses directly. It is the degraded
10356
+ * path behind `ctx.settings.update()`; see its own note for why the disk-backed
10357
+ * path is preferred.
10358
+ *
10359
+ * @module dsh-commandcode-provider/config-volatile
10360
+ */
10361
+ /**
10362
+ * Mark one schema field volatile.
10363
+ *
10364
+ * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
10365
+ * supported engine ships `~3.18.5`, so the
10366
+ * method is always present. It is reached through a structural member so the caller's
10367
+ * declared field type survives — the mark changes what the schema PARSES to (a
10368
+ * live reference), not the plugin-facing `Config` shape.
10369
+ */
10370
+ function markVolatile(schema) {
10371
+ return schema.volatile();
10372
+ }
10373
+ /**
10374
+ * Mark every field of a Config field dict volatile, except the named
10375
+ * composition-only secrets (`apiKey` here, which no settings surface writes).
10376
+ */
10377
+ function markVolatileFields(fields, skip = []) {
10378
+ return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
10379
+ }
10380
+ /**
10381
+ * Whether a resolved config value is a volatile reference.
10382
+ *
10383
+ * Duck-typed on the two properties `createVolatile` produces (frozen object
10384
+ * with a `get` method) rather than cosmokit's private `write` symbol, because
10385
+ * cosmokit is not a peer of this bundle. Config values are plain parsed JSON
10386
+ * (or `!!jsExpr` wrappers), which are never frozen, so the test cannot
10387
+ * misfire on an ordinary field.
10388
+ */
10389
+ function isVolatileRef(value) {
10390
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.isFrozen(value) && typeof value.get === "function";
10391
+ }
10392
+ /**
10393
+ * The commit protocol cosmokit's `createVolatile` installs on every reference.
10394
+ *
10395
+ * `Symbol.for` — not a local `Symbol()` — because cosmokit registers it
10396
+ * globally precisely so copies of the shared library loaded twice (ESM/CJS, two
10397
+ * node_modules trees) still recognize each other's references. Reaching it
10398
+ * through the global registry is what lets this bundle commit a value without
10399
+ * taking `@deepseek-ai/cosmokit` as a dependency.
10400
+ */
10401
+ const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
10402
+ /**
10403
+ * Commit a value into a RUNNING volatile reference and verify it landed.
10404
+ *
10405
+ * This is the DEGRADED write path, and it is deliberately second choice: the
10406
+ * reference is the live config the owner reads per call, but the value never
10407
+ * reaches the profile document, so the next loader commit for that entry (any
10408
+ * `loader/volatile-update`, including one caused by an unrelated field of the
10409
+ * same entry) re-resolves the raw config and overwrites it. The official
10410
+ * `ctx.settings.update()` path exists to avoid exactly that; this one keeps the
10411
+ * CURRENT process coherent when settings is unavailable or refuses the write.
10412
+ *
10413
+ * Reading back is not optional: the write method is an internal protocol, so a
10414
+ * future cosmokit that renames the symbol, hides it, or changes the value shape
10415
+ * must degrade to "not applied" instead of silently pretending success.
10416
+ *
10417
+ * @param ref - Candidate volatile reference; anything else returns `false`.
10418
+ * @param value - Value to commit.
10419
+ * @returns Whether the reference now reads back as `value`.
10420
+ */
10421
+ function volatileWrite(ref, value) {
10422
+ if (!isVolatileRef(ref)) return false;
10423
+ const write = ref[VOLATILE_WRITE];
10424
+ if (typeof write !== "function") return false;
10425
+ try {
10426
+ write.call(ref, value);
10427
+ } catch {
10428
+ return false;
10429
+ }
10430
+ try {
10431
+ return Object.is(ref.get(), value);
10432
+ } catch {
10433
+ return false;
10434
+ }
10435
+ }
10436
+ /**
10437
+ * Read one config object as PLAIN values.
10438
+ *
10439
+ * Every top-level field found to be a volatile reference is read through
10440
+ * `.get()` — freshly on every call, so a live update lands on the very next
10441
+ * read (the loader resolves EVERY marked node into a reference, unset fields
10442
+ * included, so a loader-parsed config always yields a fresh object here). The
10443
+ * pass-through for a field that is not a reference is what keeps the unmarked
10444
+ * `apiKey` literal readable.
10445
+ */
10446
+ function unwrapVolatileConfig(config) {
10447
+ const source = config;
10448
+ if (typeof source !== "object" || source === null) return config;
10449
+ const plain = {};
10450
+ for (const [field, value] of Object.entries(source)) plain[field] = isVolatileRef(value) ? value.get() : value;
10451
+ return plain;
10452
+ }
10453
+ //#endregion
10032
10454
  //#region src/web-search.ts
10033
10455
  /**
10034
10456
  * Command Code web-search provider over `ctx.web`.
@@ -10041,6 +10463,12 @@ var AccountEnrollmentManager = class {
10041
10463
  * therefore reuses the plugin's existing credential chain and `apiBase`, so
10042
10464
  * the model-facing `web_search` tool needs no separate configuration.
10043
10465
  *
10466
+ * The second half of this module is the SELECTION seam: which registered search
10467
+ * backend `ctx.web` actually runs. dsh 0.2.1-rc.2 turned that choice into a
10468
+ * per-call `Volatile` field of the `web` profile entry, so it is changed
10469
+ * through the host settings service rather than by assigning a runtime field —
10470
+ * see {@link createCommandCodeSearchSelection}.
10471
+ *
10044
10472
  * @module dsh-commandcode-provider/web-search
10045
10473
  */
10046
10474
  /** Stable id this provider registers under in `ctx.web`. */
@@ -10052,54 +10480,273 @@ const COMMANDCODE_SEARCH_PROVIDER_ID = "commandcode";
10052
10480
  * PREVIOUSLY selected backend, never this default (issue #26).
10053
10481
  */
10054
10482
  const DEFAULT_WEB_SEARCH_PROVIDER_ID = "deepseek-official";
10483
+ /**
10484
+ * Profile entry id whose volatile `searchProvider` selects the search backend.
10485
+ *
10486
+ * The hosting profile declares it in `dsh-base`'s cordis patch as
10487
+ * `- id: web` / `name: '@deepseek-ai/dsh-web'`, and the shipped web-search
10488
+ * settings page addresses the very same namespace — that page and this plugin
10489
+ * are therefore two writers of ONE field (see
10490
+ * {@link createCommandCodeSearchSelection} for how they avoid fighting).
10491
+ */
10492
+ const WEB_SELECTION_NAMESPACE = "web";
10055
10493
  /** Fresh selection state: the plugin starts out not owning the selection. */
10056
10494
  function commandCodeSearchSelection() {
10057
10495
  return {
10058
10496
  owner: false,
10059
- displaced: void 0,
10060
- preexisting: false
10497
+ preexisting: false,
10498
+ foreign: void 0,
10499
+ displaced: void 0
10061
10500
  };
10062
10501
  }
10502
+ /** Read one member of a plain object or array without trusting its shape. */
10503
+ function member(value, key) {
10504
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
10505
+ return value[key];
10506
+ }
10507
+ /** A string member, or `undefined` for anything else (including `null`). */
10508
+ function text(value) {
10509
+ return typeof value === "string" ? value : void 0;
10510
+ }
10511
+ /** The settings row describing the `web` entry, when the host serves one. */
10512
+ function selectionRow(settings) {
10513
+ let rows;
10514
+ try {
10515
+ rows = settings.describe();
10516
+ } catch {
10517
+ return;
10518
+ }
10519
+ if (!Array.isArray(rows)) return void 0;
10520
+ return rows.find((row) => text(member(row, "ns")) === "web");
10521
+ }
10063
10522
  /**
10064
- * Reach one end of the `webSearch` toggle without trampling sibling search
10065
- * providers (issue #26).
10066
- *
10067
- * - Enabling writes `commandcode` and remembers whatever it displaced. When
10068
- * the plugin already owns the selection, the original `displaced` is kept —
10069
- * the field currently holds our own id, which must never be mistaken for the
10070
- * user's backend.
10071
- * - Disabling hands the selection back to the remembered backend only when
10072
- * this plugin actually took it over. A fresh boot straight into
10073
- * `webSearch: false`, or a `preexisting` field, leaves it ALONE: writing the
10074
- * empty memory back would clear the user's own `searchProvider: commandcode`
10075
- * pin and hand the selection to dsh-web's auto-select, where a second usable
10076
- * provider makes every later search throw `WEB_PROVIDER_AMBIGUOUS`.
10523
+ * The volatile reference backing `web.searchProvider`.
10077
10524
  *
10078
- * Never throws: like the low-level rewrite, a hardened runtime shape degrades
10079
- * to registered-but-unselected.
10525
+ * `config` is a TypeScript-private parameter property on `WebRuntime`, so at
10526
+ * runtime it is an ordinary own field — the only handle to a reference the host
10527
+ * exposes no public accessor for. Reading it structurally means a hardened
10528
+ * runtime degrades to "not writable" instead of throwing.
10080
10529
  */
10081
- function applyCommandCodeSearchSelection(web, state, enable) {
10530
+ function searchProviderRef(web) {
10531
+ return member(member(web, "config"), "searchProvider");
10532
+ }
10533
+ /** Observe the live selection; `undefined` when nothing can be observed. */
10534
+ function readView(deps) {
10535
+ const settings = deps.settings();
10536
+ if (settings !== void 0) {
10537
+ const row = selectionRow(settings);
10538
+ if (row !== void 0) {
10539
+ const user = member(row, "user");
10540
+ return {
10541
+ configured: text(member(member(row, "value"), "searchProvider")),
10542
+ pinned: text(member(user, "searchProvider")),
10543
+ base: text(member(member(row, "base"), "searchProvider")),
10544
+ user: typeof user === "object" && user !== null && !Array.isArray(user) ? { ...user } : {},
10545
+ persistent: true
10546
+ };
10547
+ }
10548
+ }
10549
+ const ref = searchProviderRef(deps.web);
10550
+ if (!isVolatileRef(ref)) return void 0;
10551
+ let current;
10082
10552
  try {
10083
- const field = web;
10084
- if (enable) {
10085
- if (state.owner) {
10086
- field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
10553
+ current = ref.get();
10554
+ } catch {
10555
+ return;
10556
+ }
10557
+ if (current !== void 0 && typeof current !== "string") return void 0;
10558
+ return {
10559
+ configured: current,
10560
+ pinned: void 0,
10561
+ base: void 0,
10562
+ user: {},
10563
+ persistent: false
10564
+ };
10565
+ }
10566
+ /**
10567
+ * The selection seam over `ctx.web`.
10568
+ *
10569
+ * ## Why settings rather than the runtime instance
10570
+ *
10571
+ * 0.2.1-rc.2's `WebRuntime` reads `this.config.searchProvider.get()` on EVERY
10572
+ * search and fetch, and the loader commits a settings write into that same
10573
+ * reference in place (`Entry._commitVolatile`) without remounting the entry.
10574
+ * Writing the reference directly therefore works, but leaves the value out of
10575
+ * the profile: the next commit for that entry — including one caused by an
10576
+ * unrelated field such as `fetchProvider` — re-resolves the raw config and
10577
+ * overwrites it. `ctx.settings.update()` / `replace()` is the documented path
10578
+ * and the one the shipped web-search settings page uses, so this plugin takes
10579
+ * it first and falls back to the reference only when settings is unavailable or
10580
+ * refuses (see `volatileWrite`).
10581
+ *
10582
+ * ## Two writers, one field
10583
+ *
10584
+ * The shipped web-search settings page writes `web.searchProvider` too
10585
+ * (`deepseek-search-api` / `deepseek-official`). This seam never displaces a
10586
+ * pin it did not write: a non-`commandcode` value in the USER LAYER stops the
10587
+ * takeover, the value is recorded on {@link CommandCodeSearchSelection.foreign}
10588
+ * and named in one `info` line, and the provider stays registered but
10589
+ * unselected. A value the user layer does not carry is the deployment's own
10590
+ * default, and taking it over is the toggle's whole purpose. A user-layer
10591
+ * `commandcode` is treated as ours: nothing else writes that id, so it can only
10592
+ * be a previous run of this plugin or a deliberate manual pin, and turning the
10593
+ * toggle off restores the composed default in both cases.
10594
+ *
10595
+ * @param deps - the running `web` service, a settings resolver, and a log sink.
10596
+ * @returns a synchronous handle; failures are logged, never thrown.
10597
+ */
10598
+ function createCommandCodeSearchSelection(deps) {
10599
+ const state = commandCodeSearchSelection();
10600
+ let pending;
10601
+ let activeKey;
10602
+ let running = false;
10603
+ let closed = false;
10604
+ let waiters = [];
10605
+ const warn = (message) => {
10606
+ try {
10607
+ deps.logger.warn(message);
10608
+ } catch {}
10609
+ };
10610
+ const info = (message) => {
10611
+ try {
10612
+ deps.logger.info(message);
10613
+ } catch {}
10614
+ };
10615
+ /** Commit `commandcode` in memory; the last resort before giving up. */
10616
+ const memoryWrite = (value) => {
10617
+ const written = volatileWrite(searchProviderRef(deps.web), value);
10618
+ if (!written) warn("llm-commandcode: web search selection could not be applied (settings refused the write and the running web config is not writable); the provider stays registered but unselected");
10619
+ return written;
10620
+ };
10621
+ /** Run one intent against a FRESH observation; never throws. */
10622
+ const perform = async (intent) => {
10623
+ let view;
10624
+ try {
10625
+ view = readView(deps);
10626
+ } catch {
10627
+ view = void 0;
10628
+ }
10629
+ if (view === void 0) return;
10630
+ const settings = view.persistent ? deps.settings() : void 0;
10631
+ if (intent.kind === "take") {
10632
+ if (view.pinned !== void 0 && view.pinned !== "commandcode") return;
10633
+ if (settings !== void 0) try {
10634
+ await settings.update("web", { searchProvider: COMMANDCODE_SEARCH_PROVIDER_ID });
10087
10635
  return;
10636
+ } catch (error) {
10637
+ warn(`llm-commandcode: writing web.searchProvider through settings failed (${error instanceof Error ? error.message : String(error)}); falling back to the running config`);
10088
10638
  }
10089
- const prior = field.searchProviderId;
10090
- state.preexisting = prior === COMMANDCODE_SEARCH_PROVIDER_ID;
10091
- state.displaced = state.preexisting ? void 0 : prior;
10092
- field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
10093
- state.owner = true;
10639
+ memoryWrite(COMMANDCODE_SEARCH_PROVIDER_ID);
10094
10640
  return;
10095
10641
  }
10096
- if (state.owner) {
10642
+ if (view.pinned !== void 0 && view.pinned !== "commandcode") return;
10643
+ if (settings !== void 0) {
10644
+ const section = { ...view.user };
10645
+ delete section.searchProvider;
10646
+ if (view.base !== void 0) section.searchProvider = view.base;
10647
+ try {
10648
+ await settings.replace("web", section);
10649
+ return;
10650
+ } catch (error) {
10651
+ warn(`llm-commandcode: restoring web.searchProvider through settings failed (${error instanceof Error ? error.message : String(error)}); falling back to the running config`);
10652
+ }
10653
+ }
10654
+ memoryWrite(intent.displaced);
10655
+ };
10656
+ const flush = () => {
10657
+ const done = waiters;
10658
+ waiters = [];
10659
+ for (const resolve of done) resolve();
10660
+ };
10661
+ const drain = async () => {
10662
+ running = true;
10663
+ try {
10664
+ while (pending !== void 0 && !closed) {
10665
+ const item = pending;
10666
+ pending = void 0;
10667
+ activeKey = item.key;
10668
+ await perform(item.intent);
10669
+ }
10670
+ } finally {
10671
+ running = false;
10672
+ activeKey = void 0;
10673
+ flush();
10674
+ }
10675
+ };
10676
+ /**
10677
+ * Queue one intent, skipping an identical one already queued or in flight. A
10678
+ * later DIFFERENT intent replaces the queued one: the toggle is a state, not a
10679
+ * log, so only the newest position has to land.
10680
+ */
10681
+ const submit = (key, intent) => {
10682
+ if (closed) return;
10683
+ if (key === activeKey && pending === void 0) return;
10684
+ if (pending?.key === key) return;
10685
+ pending = {
10686
+ key,
10687
+ intent
10688
+ };
10689
+ if (!running) drain();
10690
+ };
10691
+ const apply = (enable) => {
10692
+ try {
10693
+ const view = readView(deps);
10694
+ if (view === void 0) return;
10695
+ if (!enable) {
10696
+ if (!state.owner) return;
10697
+ state.owner = false;
10698
+ if (state.preexisting) {
10699
+ state.preexisting = false;
10700
+ return;
10701
+ }
10702
+ submit("restore", {
10703
+ kind: "restore",
10704
+ displaced: state.displaced
10705
+ });
10706
+ return;
10707
+ }
10708
+ if (view.pinned !== void 0 && view.pinned !== "commandcode") {
10709
+ if (state.foreign !== view.pinned) {
10710
+ state.foreign = view.pinned;
10711
+ info(`llm-commandcode: web search backend "${view.pinned}" was chosen in the web-search settings page; leaving that explicit choice in place, so the commandcode provider stays registered but unselected`);
10712
+ }
10713
+ state.owner = false;
10714
+ state.preexisting = false;
10715
+ return;
10716
+ }
10717
+ state.foreign = void 0;
10718
+ if (view.configured === "commandcode") {
10719
+ state.owner = view.pinned === COMMANDCODE_SEARCH_PROVIDER_ID;
10720
+ state.preexisting = !state.owner;
10721
+ if (state.owner && state.displaced === void 0) state.displaced = view.base;
10722
+ return;
10723
+ }
10724
+ state.displaced = view.configured;
10725
+ state.preexisting = false;
10726
+ state.owner = true;
10727
+ submit("take", {
10728
+ kind: "take",
10729
+ displaced: view.configured
10730
+ });
10731
+ } catch (error) {
10732
+ warn(`llm-commandcode: aligning the web search selection failed (${error instanceof Error ? error.message : String(error)})`);
10733
+ }
10734
+ };
10735
+ return {
10736
+ state,
10737
+ apply,
10738
+ settled: () => running || pending !== void 0 ? new Promise((resolve) => {
10739
+ waiters.push(resolve);
10740
+ }) : Promise.resolve(),
10741
+ release: () => {
10742
+ closed = true;
10743
+ pending = void 0;
10744
+ if (state.owner && !state.preexisting) memoryWrite(state.displaced);
10097
10745
  state.owner = false;
10098
- if (!state.preexisting) field.searchProviderId = state.displaced;
10099
10746
  state.preexisting = false;
10100
- return;
10747
+ if (!running) flush();
10101
10748
  }
10102
- } catch {}
10749
+ };
10103
10750
  }
10104
10751
  /** Command Code's lower/upper bound on `numResults` (from the CLI's `web_search` schema). */
10105
10752
  const MIN_NUM_RESULTS = 1;
@@ -10183,7 +10830,7 @@ var CommandCodeSearchProvider = class {
10183
10830
  });
10184
10831
  } catch (error) {
10185
10832
  if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
10186
- throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10833
+ throw new WebError(`Command Code web search request failed: ${readableErrorText(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10187
10834
  }
10188
10835
  if (!response.ok) {
10189
10836
  let message = `Command Code web search failed (HTTP ${response.status})`;
@@ -10237,7 +10884,7 @@ var CommandCodeSearchProvider = class {
10237
10884
  if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
10238
10885
  if (code === "INVALID_CREDENTIAL" || code === "RATE_LIMIT") throw new WebError(error.message, "WEB_PROVIDER_ERROR", { cause: error });
10239
10886
  }
10240
- throw new WebError(`Command Code web search credential resolution failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10887
+ throw new WebError(`Command Code web search credential resolution failed: ${readableErrorText(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10241
10888
  }
10242
10889
  if (key === void 0 || key.length === 0) throw new WebError("Command Code web search has no API key; store COMMANDCODE_API_KEY through the credentials service (the web Models page writes it), export it in the launching environment, set config.apiKey, or run `command-code login` to write ~/.commandcode/auth.json", "WEB_PROVIDER_CREDENTIAL_MISSING");
10243
10890
  return key;
@@ -10568,7 +11215,7 @@ function applyCommandCodeTuiSettings(ctx, deps) {
10568
11215
  dispose: service.register(section)
10569
11216
  };
10570
11217
  } catch (error) {
10571
- ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${error instanceof Error ? error.message : String(error)}`);
11218
+ ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${readableErrorText(error)}`);
10572
11219
  }
10573
11220
  };
10574
11221
  refresh();
@@ -10651,78 +11298,6 @@ function transportBudgetMessage(failureMessage, maxRetries) {
10651
11298
  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`;
10652
11299
  }
10653
11300
  //#endregion
10654
- //#region src/config-volatile.ts
10655
- /**
10656
- * Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
10657
- *
10658
- * 0.1.7's `settings.describe()` projects each entry's schema through
10659
- * `volatileForm()`, so a form contains ONLY nodes carrying `meta.volatile`
10660
- * and a form edit is refused unless its path lies beneath a marked node. The
10661
- * loader then resolves each marked field into a frozen `{ get() }` reference
10662
- * that is written IN PLACE (no remount) and announced with
10663
- * `loader/volatile-update`. Two helpers keep the rest of the plugin reading a
10664
- * PLAIN `Config`: `markVolatile()` applies the mark, and
10665
- * `unwrapVolatileConfig()` reads every top-level field through its reference
10666
- * when one is present — FRESH per call, because a reference's identity is
10667
- * stable while its value changes.
10668
- *
10669
- * Only TOP-LEVEL fields are marked (a form write names a top-level path such
10670
- * as `accounts` or `modelVisibility.<id>`), so one unwrap level is complete.
10671
- * The literal `apiKey` secret stays unmarked on purpose: no settings surface
10672
- * writes it — page and TUI both write keys through the credentials seam — so a
10673
- * config-file edit to it reloading the fiber is correct for a secret literal.
10674
- *
10675
- * @module dsh-commandcode-provider/config-volatile
10676
- */
10677
- /**
10678
- * Mark one schema field volatile.
10679
- *
10680
- * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and every
10681
- * supported engine ships `~3.18.4` or `~3.18.5-alpha.1`, so the
10682
- * method is always present. It is reached through a structural member so the caller's
10683
- * declared field type survives — the mark changes what the schema PARSES to (a
10684
- * live reference), not the plugin-facing `Config` shape.
10685
- */
10686
- function markVolatile(schema) {
10687
- return schema.volatile();
10688
- }
10689
- /**
10690
- * Mark every field of a Config field dict volatile, except the named
10691
- * composition-only secrets (`apiKey` here, which no settings surface writes).
10692
- */
10693
- function markVolatileFields(fields, skip = []) {
10694
- return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
10695
- }
10696
- /**
10697
- * Whether a resolved config value is a volatile reference.
10698
- *
10699
- * Duck-typed on the two properties `createVolatile` produces (frozen object
10700
- * with a `get` method) rather than cosmokit's private `write` symbol, because
10701
- * cosmokit is not a peer of this bundle. Config values are plain parsed JSON
10702
- * (or `!!jsExpr` wrappers), which are never frozen, so the test cannot
10703
- * misfire on an ordinary field.
10704
- */
10705
- function isVolatileRef(value) {
10706
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.isFrozen(value) && typeof value.get === "function";
10707
- }
10708
- /**
10709
- * Read one config object as PLAIN values.
10710
- *
10711
- * Every top-level field found to be a volatile reference is read through
10712
- * `.get()` — freshly on every call, so a live update lands on the very next
10713
- * read (the loader resolves EVERY marked node into a reference, unset fields
10714
- * included, so a loader-parsed config always yields a fresh object here). The
10715
- * pass-through for a field that is not a reference is what keeps the unmarked
10716
- * `apiKey` literal readable.
10717
- */
10718
- function unwrapVolatileConfig(config) {
10719
- const source = config;
10720
- if (typeof source !== "object" || source === null) return config;
10721
- const plain = {};
10722
- for (const [field, value] of Object.entries(source)) plain[field] = isVolatileRef(value) ? value.get() : value;
10723
- return plain;
10724
- }
10725
- //#endregion
10726
11301
  //#region src/index.ts
10727
11302
  /**
10728
11303
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -10780,6 +11355,9 @@ const Config = z.object(markVolatileFields({
10780
11355
  filterModelsByPlan: z.boolean(),
10781
11356
  visibleModels: z.array(z.string()),
10782
11357
  modelVisibility: z.dict(z.boolean()),
11358
+ contextWindow: z.number().min(1).max(CONTEXT_WINDOW_MAX).step(1),
11359
+ contextWindowOverrides: z.dict(z.number().min(1).max(CONTEXT_WINDOW_MAX).step(1)),
11360
+ maxOutputTokens: z.number().min(0).max(MAX_OUTPUT_TOKEN_LIMIT).step(1),
10783
11361
  webSearch: z.boolean().default(true),
10784
11362
  showSidebarQuota: z.boolean().default(false),
10785
11363
  accounts: z.array(z.object({
@@ -10825,6 +11403,9 @@ function resolveAdapterOptions(config) {
10825
11403
  filterModelsByPlan: config.filterModelsByPlan ?? true,
10826
11404
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
10827
11405
  modelVisibility: readModelVisibility(config.modelVisibility),
11406
+ contextWindow: isValidContextWindow(config.contextWindow) ? config.contextWindow : void 0,
11407
+ contextWindowOverrides: readContextWindowOverrides(config.contextWindowOverrides),
11408
+ maxOutputTokens: isValidMaxOutputTokens(config.maxOutputTokens) ? config.maxOutputTokens : void 0,
10828
11409
  zdr: config.zdr === true
10829
11410
  };
10830
11411
  }
@@ -10985,7 +11566,7 @@ function apply(ctx, config) {
10985
11566
  else try {
10986
11567
  report = await adapter.getUsage(account.key, connection);
10987
11568
  } catch (error) {
10988
- report = { failures: [error instanceof Error ? error.message : String(error)] };
11569
+ report = { failures: [readableErrorText(error)] };
10989
11570
  }
10990
11571
  const state = account?.state;
10991
11572
  const usable = accountUsable(state);
@@ -11067,13 +11648,24 @@ function apply(ctx, config) {
11067
11648
  listModels: catalogForEditors,
11068
11649
  analytics: createAnalyticsReader(ctx, adapter)
11069
11650
  });
11070
- const searchSelection = commandCodeSearchSelection();
11651
+ const selectionLogger = {
11652
+ warn: (message) => {
11653
+ ctx.logger.warn(message);
11654
+ },
11655
+ info: (message) => {
11656
+ ctx.logger.info(message);
11657
+ }
11658
+ };
11659
+ let searchSelection;
11071
11660
  const applySearchSelection = (enabled) => {
11072
- if (webRuntime !== void 0) applyCommandCodeSearchSelection(webRuntime, searchSelection, enabled);
11661
+ searchSelection?.apply(enabled);
11073
11662
  };
11074
- let webRuntime;
11075
11663
  ctx.inject(["web"], (webCtx) => {
11076
- webRuntime = webCtx.web;
11664
+ searchSelection = createCommandCodeSearchSelection({
11665
+ web: webCtx.web,
11666
+ settings: () => ctx.get("settings"),
11667
+ logger: selectionLogger
11668
+ });
11077
11669
  webCtx.web.registerSearchProvider(new CommandCodeSearchProvider({
11078
11670
  resolveKey: async (apiBase) => {
11079
11671
  const connection = options();
@@ -11085,8 +11677,8 @@ function apply(ctx, config) {
11085
11677
  }));
11086
11678
  applySearchSelection(current().webSearch ?? true);
11087
11679
  webCtx.effect(() => () => {
11088
- webRuntime = void 0;
11089
- applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
11680
+ searchSelection?.release();
11681
+ searchSelection = void 0;
11090
11682
  }, "dsh-commandcode-provider: web search selection");
11091
11683
  });
11092
11684
  let refreshTuiSettings;
@@ -11131,6 +11723,7 @@ function apply(ctx, config) {
11131
11723
  dispose();
11132
11724
  };
11133
11725
  }, "dsh-commandcode-provider: settings auto-form policy");
11726
+ applySearchSelection(current().webSearch ?? true);
11134
11727
  });
11135
11728
  const loaderEvents = ctx;
11136
11729
  ctx.effect(() => {
@@ -11144,6 +11737,6 @@ function apply(ctx, config) {
11144
11737
  }, "dsh-commandcode-provider: volatile config updates");
11145
11738
  }
11146
11739
  //#endregion
11147
- export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_NON_ZDR_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, studioBaseForApiBase, subscriptionPlanInfo, supportsZeroDataRetention, usageReportSchema, validateCommandApiKey };
11740
+ export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_NON_ZDR_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, WEB_SELECTION_NAMESPACE, accountUsable, apply, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, createCommandCodeSearchSelection, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, studioBaseForApiBase, subscriptionPlanInfo, supportsZeroDataRetention, usageReportSchema, validateCommandApiKey };
11148
11741
 
11149
11742
  //# sourceMappingURL=index.js.map