@mars-sea/dsh-commandcode-provider 0.13.4 → 0.14.1

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.5) and
106
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.84.0) 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
@@ -315,6 +315,13 @@ const KNOWN_EFFORTS = {
315
315
  "medium",
316
316
  "high"
317
317
  ],
318
+ "reflection/beam:free": [
319
+ "low",
320
+ "medium",
321
+ "high",
322
+ "xhigh",
323
+ "max"
324
+ ],
318
325
  "sakana/fugu-ultra": ["high", "xhigh"],
319
326
  "stepfun/Step-5-Preview": [
320
327
  "low",
@@ -622,9 +629,13 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
622
629
  * what the current CLI says.
623
630
  *
624
631
  * 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.
632
+ * the `stealth/cat-jam:free` preview, taking this set to 24 members. The 1.84.0
633
+ * sync (2026-10-16) moves it to 25 with no verdict flip: LongCat's exclusion
634
+ * followed its rename to the paid id, and the free `reflection/beam:free`
635
+ * preview joined on catalog evidence alone. The authoritative membership is the
636
+ * UNION of the artifact's exclusion set and the catalog's `zdr:false` list —
637
+ * neither source alone is complete, which is why this set is not simply "what
638
+ * the CLI says".
628
639
  */
629
640
  const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
630
641
  "MiniMaxAI/MiniMax-M3",
@@ -636,9 +647,10 @@ const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
636
647
  "meta/muse-spark-1.3",
637
648
  "meta/muse-spark-1.3-contributor",
638
649
  "meta/muse-wonder-1.0",
639
- "meituan/LongCat-2.0:free",
650
+ "meituan/LongCat-2.0",
640
651
  "minimax/minimax-m3-free",
641
652
  "poolside/laguna-s-2.1-free",
653
+ "reflection/beam:free",
642
654
  "sakana/fugu-ultra",
643
655
  "stealth/glyph-cluster:free",
644
656
  "stealth/cat-jam:free",
@@ -792,6 +804,7 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
792
804
  ["claude-sonnet-5", 128e3],
793
805
  ["claude-sonnet-5-5", 128e3],
794
806
  ["deepseek/deepseek-v4-flash", 393216],
807
+ ["deepseek/deepseek-v4-flash-fast", 384e3],
795
808
  ["deepseek/deepseek-v4-flash-vision-exp", 393216],
796
809
  ["deepseek/deepseek-v4-pro", 393216],
797
810
  ["google/gemini-3.1-flash-lite", 65536],
@@ -835,11 +848,13 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
835
848
  ["Qwen/Qwen3.7-Plus", 131072],
836
849
  ["Qwen/Qwen3.8-Flash", 131072],
837
850
  ["Qwen/Qwen3.8-Max", 131072],
851
+ ["Qwen/Qwen3.8-Max-0902", 131072],
838
852
  ["Qwen/Qwen3.8-Omni-Flash", 131072],
839
853
  ["sakana/fugu-ultra", 1e6],
840
854
  ["stepfun/Step-3.5-Flash", 256e3],
841
855
  ["stepfun/Step-3.7-Flash", 256e3],
842
856
  ["stepfun/Step-5-Preview", 65536],
857
+ ["tencent/hy3-paid", 128e3],
843
858
  ["thinkingmachines/inkling", 65536],
844
859
  ["xai/grok-4.5", 5e5],
845
860
  ["xai/grok-4.6", 5e5],
@@ -923,6 +938,7 @@ const KNOWN_PLANS = {
923
938
  "nvidia/nemotron-3-ultra-550b-a55b": "go",
924
939
  "nvidia/nemotron-3.5-super-vl": "go",
925
940
  "poolside/laguna-s-2.1-free": "go",
941
+ "reflection/beam:free": "go",
926
942
  "stepfun/Step-3.5-Flash": "go",
927
943
  "stepfun/Step-3.7-Flash": "go",
928
944
  "stepfun/Step-5-Preview": "go",
@@ -1119,6 +1135,10 @@ const KNOWN_DEALS = {
1119
1135
  label: "FREE",
1120
1136
  free: true
1121
1137
  },
1138
+ "reflection/beam:free": {
1139
+ label: "FREE",
1140
+ free: true
1141
+ },
1122
1142
  "stealth/cat-jam:free": {
1123
1143
  label: "FREE",
1124
1144
  free: true
@@ -1563,8 +1583,8 @@ const MODEL_PRICE_ROWS = [
1563
1583
  ],
1564
1584
  allowance: {
1565
1585
  go: 10,
1566
- goat: 100,
1567
- pro: 100
1586
+ goat: 80,
1587
+ pro: 90
1568
1588
  }
1569
1589
  },
1570
1590
  {
@@ -1764,8 +1784,8 @@ const MODEL_PRICE_ROWS = [
1764
1784
  ],
1765
1785
  allowance: {
1766
1786
  go: 10,
1767
- goat: 100,
1768
- pro: 100
1787
+ goat: 80,
1788
+ pro: 90
1769
1789
  }
1770
1790
  },
1771
1791
  {
@@ -3676,6 +3696,140 @@ function effectiveContextWindow(catalog, model, options) {
3676
3696
  if (isValidContextWindow(options.contextWindow)) return options.contextWindow;
3677
3697
  return catalog;
3678
3698
  }
3699
+ //#endregion
3700
+ //#region src/output-budget.ts
3701
+ /**
3702
+ * Output-budget bounds shared by the Host, the browser settings page and the
3703
+ * adapter (issue #74).
3704
+ *
3705
+ * The module stays pure (no node imports) so the client bundle can inline it,
3706
+ * exactly like `context-window.ts`. The report behind issue #74 asked for a
3707
+ * knob that stops the adapter from writing `max_tokens` at all; that is the
3708
+ * `0` sentinel here rather than a separate `null` spelling, because the value
3709
+ * travels through one numeric settings field on both surfaces.
3710
+ */
3711
+ /**
3712
+ * The output budget this bundle asks for, and the ceiling it will never exceed.
3713
+ *
3714
+ * 131 072, chosen against a measured provider limit rather than a round guess.
3715
+ * Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
3716
+ * 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
3717
+ * therefore caps `max_tokens` at 200 000, and it does so identically for a
3718
+ * 262 144-window model (`stealth/pixel-canary`, retired by Command Code on
3719
+ * 2026-10-01 and named here only as the probe's subject) and a 1 000 000-window
3720
+ * one (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
3721
+ * keeps a 35 % margin under it while covering the real output ceiling of the
3722
+ * models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
3723
+ *
3724
+ * The old 64 000 was never a provider requirement: it cost nothing in latency
3725
+ * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
3726
+ * it did cap what a model could ever say in one turn.
3727
+ *
3728
+ * No client-side clamp is applied on top of this: an earlier version pre-shrank
3729
+ * `max_tokens` from an estimate of the prompt already sent (`requestContextBudget()`,
3730
+ * issue #67), but that estimate over-corrected on ordinary long conversations —
3731
+ * exactly the "output gets cut short as the conversation grows" symptom issue #74
3732
+ * reported — while a live A/B probe (2026-09-30, `stealth/space-bunny-alpha`,
3733
+ * ~19.6% over its 1,000,000-token window with no client-side shrink) showed the
3734
+ * endpoint answers a genuine overflow with an unambiguous `400 … exceeds the
3735
+ * model's context window`, which `isContextOverflowDetail()` classifies as
3736
+ * `CONTEXT_WINDOW_EXCEEDED` independently of this constant.
3737
+ *
3738
+ * A user-set positive budget can only TIGHTEN this, never raise it — a figure
3739
+ * above the cap would be refused by the gateway anyway, so the settings field
3740
+ * refuses it at the input instead of spending a round trip to learn that.
3741
+ *
3742
+ * Lives here rather than in `adapter.ts` so the browser settings page can bound
3743
+ * its input by the same figure the Host enforces; `adapter.ts` re-exports it
3744
+ * under its historical name.
3745
+ */
3746
+ const MAX_OUTPUT_TOKEN_LIMIT = 131072;
3747
+ /**
3748
+ * Whether a value can serve as a user output budget: the omit sentinel, or a
3749
+ * whole token count inside the gateway's own ceiling.
3750
+ *
3751
+ * Programmatically built connections can bypass schemastery normalization, so
3752
+ * the resolve step re-judges every value here and drops an out-of-range one
3753
+ * back to "unset" instead of letting the request carry a budget the gateway
3754
+ * refuses.
3755
+ */
3756
+ function isValidMaxOutputTokens(value) {
3757
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 131072;
3758
+ }
3759
+ /**
3760
+ * Apply a user output budget to a derived one.
3761
+ *
3762
+ * `undefined` means the user set nothing, so the derived ceiling stands
3763
+ * unchanged — the default path, and the one every existing profile takes.
3764
+ * {@link OMIT_OUTPUT_TOKENS} is a wire instruction rather than a number, so it
3765
+ * also leaves the derived budget alone; {@link omitOutputTokens} is what turns
3766
+ * it into an absent field.
3767
+ */
3768
+ function applyMaxOutputTokens(derived, configured) {
3769
+ if (configured === void 0 || configured === 0) return derived;
3770
+ return Math.min(derived, configured);
3771
+ }
3772
+ /**
3773
+ * Whether this attempt must send no `max_tokens` field at all.
3774
+ *
3775
+ * Two guards narrow this beyond the user's own setting, and both are
3776
+ * correctness requirements rather than conveniences:
3777
+ *
3778
+ * - The Messages route is excluded deliberately: the endpoint rejects a body
3779
+ * without the field (`Invalid input: expected number, received undefined at
3780
+ * max_tokens`), so omitting it there would not lengthen an answer, it would
3781
+ * make the whole Claude family unusable. The CLI and OpenAI transports accept
3782
+ * an absent field, which is where the setting does what it promises.
3783
+ * - An explicit per-call budget from the host is an instruction, not a default,
3784
+ * so it wins over the plugin-wide setting. `session-title` is the concrete
3785
+ * case: it asks for 64 tokens precisely because its output must stay bounded,
3786
+ * and dropping the field there would let a title generation run long instead
3787
+ * of making a long answer fit (issue #88's fix depends on that bound).
3788
+ */
3789
+ function omitOutputTokens(configured, protocol, hostRequestedTokens) {
3790
+ if (configured !== 0) return false;
3791
+ if (protocol === "messages") return false;
3792
+ return hostRequestedTokens === void 0;
3793
+ }
3794
+ //#endregion
3795
+ //#region src/readable-error.ts
3796
+ /**
3797
+ * Readable error text for the settings page AND the Host-side failure paths.
3798
+ *
3799
+ * @module dsh-commandcode-provider/readable-error
3800
+ */
3801
+ /**
3802
+ * Turn a caught value into readable text.
3803
+ *
3804
+ * `error instanceof Error ? error.message : String(error)` degrades to the
3805
+ * literal string `[object Object]` for a plain object with no `Error`
3806
+ * prototype — which is exactly what a Remote call across the webview/host
3807
+ * boundary can hand back: `dsh-typert-protocol` serializes a rejection for
3808
+ * the wire, and a structurally-cloned value loses its prototype chain even
3809
+ * when the host threw a real `Error` (issue #74). Reading a `message` string
3810
+ * off the plain object first recovers the original text in that case; only a
3811
+ * value with neither an `Error` prototype nor a usable `message` field falls
3812
+ * through to a JSON dump.
3813
+ *
3814
+ * Shared by both halves on purpose. The browser client needs it for rejections
3815
+ * that crossed the RPC boundary, and the Host needs it for values that reached
3816
+ * it from a transport, a JSON body, or a user-thrown non-Error — the same
3817
+ * degradation, one owner.
3818
+ */
3819
+ function readableErrorText(error) {
3820
+ if (error instanceof Error) return error.message;
3821
+ if (typeof error === "string") return error;
3822
+ if (error !== null && typeof error === "object") {
3823
+ const message = error.message;
3824
+ if (typeof message === "string" && message !== "") return message;
3825
+ try {
3826
+ return JSON.stringify(error);
3827
+ } catch {
3828
+ return Object.prototype.toString.call(error);
3829
+ }
3830
+ }
3831
+ return String(error);
3832
+ }
3679
3833
  const THROTTLED_CODE = "THROTTLED";
3680
3834
  const boundedCodes = /* @__PURE__ */ new Set([
3681
3835
  "EMPTY_RESPONSE",
@@ -6144,37 +6298,15 @@ var GatewayFacts = class {
6144
6298
  * per-request options thunk and an API-key resolver from the plugin entry
6145
6299
  * (src/index.ts), so a settings change reaches the very next request.
6146
6300
  */
6147
- const COMMAND_CODE_CLI_VERSION = "1.82.5";
6301
+ const COMMAND_CODE_CLI_VERSION = "1.84.0";
6148
6302
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
6149
6303
  /**
6150
6304
  * The output budget this bundle asks for, and the ceiling it will never exceed.
6151
- *
6152
- * 131 072, chosen against a measured provider limit rather than a round guess.
6153
- * Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
6154
- * 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
6155
- * therefore caps `max_tokens` at 200 000, and it does so identically for a
6156
- * 262 144-window model (`stealth/pixel-canary`, retired by Command Code on
6157
- * 2026-10-01 and named here only as the probe's subject) and a 1 000 000-window
6158
- * one (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
6159
- * keeps a 35 % margin under it while covering the real output ceiling of the
6160
- * models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
6161
- *
6162
- * The old 64 000 was never a provider requirement: it cost nothing in latency
6163
- * (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
6164
- * it did cap what a model could ever say in one turn.
6165
- *
6166
- * No client-side clamp is applied on top of this: an earlier version pre-shrank
6167
- * `max_tokens` from an estimate of the prompt already sent (`requestContextBudget()`,
6168
- * issue #67), but that estimate over-corrected on ordinary long conversations —
6169
- * exactly the "output gets cut short as the conversation grows" symptom issue #74
6170
- * reported — while a live A/B probe (2026-09-30, `stealth/space-bunny-alpha`,
6171
- * ~19.6% over its 1,000,000-token window with no client-side shrink) showed the
6172
- * endpoint answers a genuine overflow with an unambiguous `400 … exceeds the
6173
- * model's context window`, which `isContextOverflowDetail()` below already
6174
- * classifies as `CONTEXT_WINDOW_EXCEEDED` independently of this constant. See
6175
- * [决策记录](../docs/决策记录.md).
6305
+ * The figure and its probe evidence live in `./output-budget.ts` — the module
6306
+ * the browser settings page also bounds its input by — and are re-exported here
6307
+ * under the historical name so existing importers are unaffected.
6176
6308
  */
6177
- const DEFAULT_GENERATE_MAX_TOKENS = 131072;
6309
+ const DEFAULT_GENERATE_MAX_TOKENS = MAX_OUTPUT_TOKEN_LIMIT;
6178
6310
  const MODELS_TIMEOUT_MS = 1e4;
6179
6311
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
6180
6312
  const BILLING_ACCESS_TTL_MS = 3e5;
@@ -7405,7 +7537,7 @@ async function buildCliBody(options, connection, facts) {
7405
7537
  input_schema: toolParametersSchema(tool.parameters)
7406
7538
  })),
7407
7539
  system: cliSystem(facts.systemText),
7408
- max_tokens: facts.maxTokens,
7540
+ ...facts.omitMaxTokens ? {} : { max_tokens: facts.maxTokens },
7409
7541
  temperature: options.temperature ?? .3,
7410
7542
  stream: true,
7411
7543
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
@@ -7431,7 +7563,7 @@ async function buildOpenAIBody(options, facts) {
7431
7563
  model: options.model,
7432
7564
  messages: openAiMessages,
7433
7565
  ...openAiTools.length > 0 ? { tools: openAiTools } : {},
7434
- max_tokens: facts.maxTokens,
7566
+ ...facts.omitMaxTokens ? {} : { max_tokens: facts.maxTokens },
7435
7567
  temperature: options.temperature ?? .3,
7436
7568
  stream: true,
7437
7569
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
@@ -7494,6 +7626,16 @@ async function buildMessagesBody(options, facts) {
7494
7626
  return body;
7495
7627
  }
7496
7628
  /**
7629
+ * Write the output budget into an already-built body, each transport in its own
7630
+ * shape (the CLI route nests it under `params`). Used to keep a REBUILT body's
7631
+ * figure in step with the budget without duplicating the nesting knowledge, and
7632
+ * only called when the field is not being omitted (issue #74).
7633
+ */
7634
+ function assignMaxTokens(body, target, maxTokens) {
7635
+ if (target === "cli") recordOrEmpty(body.params).max_tokens = maxTokens;
7636
+ else body.max_tokens = maxTokens;
7637
+ }
7638
+ /**
7497
7639
  * One connect attempt's inputs: everything `connectGenerate` needs beyond
7498
7640
  * the per-attempt key and protocol. Passed explicitly (not closed over) so
7499
7641
  * the rotation loop's mutation of `protocol`/`body` stays visible at the
@@ -7857,7 +7999,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
7857
7999
  toolUpdate: "addition-only",
7858
8000
  ...entry !== void 0 && contextWindow !== void 0 ? {
7859
8001
  context: { contextWindow },
7860
- defaultMaxTokens: Math.min(catalogModelMaxTokens(connection.apiBase, entry, contextWindow), DEFAULT_GENERATE_MAX_TOKENS)
8002
+ defaultMaxTokens: applyMaxOutputTokens(Math.min(catalogModelMaxTokens(connection.apiBase, entry, contextWindow), DEFAULT_GENERATE_MAX_TOKENS), connection.maxOutputTokens)
7861
8003
  } : {},
7862
8004
  ...efforts ? { reasoning: { efforts: efforts.map((effort) => ({
7863
8005
  id: ReasoningEffortId(effort),
@@ -8137,7 +8279,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8137
8279
  }
8138
8280
  return record;
8139
8281
  } catch (error) {
8140
- failures.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
8282
+ failures.push(`${path}: ${readableErrorText(error)}`);
8141
8283
  failed.push({ kind: "transport" });
8142
8284
  return;
8143
8285
  }
@@ -8281,7 +8423,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8281
8423
  options.signal?.throwIfAborted();
8282
8424
  const modelEntry = catalog.find((row) => row.id === options.model);
8283
8425
  const modelMax = modelEntry === void 0 ? facts.fallbackLimit : catalogModelMaxTokens(connection.apiBase, modelEntry, effectiveContextWindow(modelEntry.contextWindow, options.model, connection));
8284
- const initialMax = Math.min(options.maxTokens ?? Infinity, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
8426
+ const initialMax = applyMaxOutputTokens(Math.min(options.maxTokens ?? Infinity, modelMax, DEFAULT_GENERATE_MAX_TOKENS), connection.maxOutputTokens);
8285
8427
  const budget = {
8286
8428
  max: initialMax,
8287
8429
  sent: initialMax
@@ -8332,17 +8474,16 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8332
8474
  const buildBody = async (target) => {
8333
8475
  const finishBody = timing.phase("body");
8334
8476
  try {
8477
+ const omitMaxTokens = omitOutputTokens(connection.maxOutputTokens, target, options.maxTokens);
8335
8478
  const facts = {
8336
8479
  maxTokens,
8480
+ omitMaxTokens,
8337
8481
  reasoningEffort,
8338
8482
  selectableEffort: supported !== void 0 && supported.length > 0,
8339
8483
  systemText,
8340
8484
  readImage
8341
8485
  };
8342
- const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
8343
- if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
8344
- else built.max_tokens = maxTokens;
8345
- return built;
8486
+ return target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
8346
8487
  } finally {
8347
8488
  finishBody();
8348
8489
  }
@@ -8392,8 +8533,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8392
8533
  if (lower < maxTokens) {
8393
8534
  maxTokens = lower;
8394
8535
  budget.max = Math.min(budget.max, lower);
8395
- if (protocol === "cli") recordOrEmpty(body.params).max_tokens = lower;
8396
- else body.max_tokens = lower;
8536
+ if (!omitOutputTokens(connection.maxOutputTokens, protocol, options.maxTokens)) assignMaxTokens(body, protocol, lower);
8397
8537
  serializedBodies.delete(body);
8398
8538
  }
8399
8539
  budget.sent = maxTokens;
@@ -8707,7 +8847,7 @@ function commandDefinition(deps) {
8707
8847
  text: renderReport(await adapter.getUsage(), locale)
8708
8848
  };
8709
8849
  } catch (error) {
8710
- const message = error instanceof Error ? error.message : String(error);
8850
+ const message = readableErrorText(error);
8711
8851
  return {
8712
8852
  kind: "error",
8713
8853
  text: commandCopy(locale, "errorText").replace("{message}", message)
@@ -9025,6 +9165,30 @@ const LOGIN_HOST_CONTRIBUTION = {
9025
9165
  };
9026
9166
  //#endregion
9027
9167
  //#region src/enrollment-wire.ts
9168
+ const ENROLLMENT_MESSAGE_KEYS = [
9169
+ "enrollmentCreating",
9170
+ "enrollmentWaiting",
9171
+ "enrollmentWriting",
9172
+ "enrollmentNaming",
9173
+ "enrollmentNameFailed",
9174
+ "enrollmentFinished",
9175
+ "enrollmentCancelling",
9176
+ "enrollmentCancelled",
9177
+ "enrollmentCancelledCleaned",
9178
+ "enrollmentFailed",
9179
+ "enrollmentFailedCleaned",
9180
+ "enrollmentCleanupPending",
9181
+ "enrollmentCleanupFailed",
9182
+ "enrollmentRecordNaming",
9183
+ "enrollmentRecordCleanup",
9184
+ "enrollmentBeginFailed",
9185
+ "enrollmentCancelUnconfirmed",
9186
+ "enrollmentNameRequestFailed",
9187
+ "enrollmentCleanupRequestFailed",
9188
+ "enrollmentDisconnected",
9189
+ "enrollmentHostUnavailable",
9190
+ "enrollmentStatusUnavailable"
9191
+ ];
9028
9192
  const guard = makeBoundaryValidator("commandcode/enrollment");
9029
9193
  const phases = [
9030
9194
  "creating",
@@ -9041,11 +9205,13 @@ function parseEnrollmentState(value) {
9041
9205
  const source = guard.record(value, "state");
9042
9206
  const phase = source.phase;
9043
9207
  if (!phases.includes(phase)) return guard.reject("phase");
9208
+ const messageKey = source.messageKey;
9209
+ if (!ENROLLMENT_MESSAGE_KEYS.includes(messageKey)) return guard.reject("messageKey");
9044
9210
  const state = {
9045
9211
  id: guard.stringField(source, "id", "id"),
9046
9212
  ref: guard.stringField(source, "ref", "ref"),
9047
9213
  phase,
9048
- message: guard.stringField(source, "message", "message")
9214
+ messageKey
9049
9215
  };
9050
9216
  for (const field of ["authUrl", "suggestedName"]) if (source[field] !== void 0) state[field] = guard.stringField(source, field, field);
9051
9217
  return state;
@@ -9737,7 +9903,7 @@ var CommandCodeLoginFlow = class {
9737
9903
  this.setStatus({
9738
9904
  state: "failed",
9739
9905
  reason: "unavailable",
9740
- message: error instanceof Error ? error.message : String(error)
9906
+ message: readableErrorText(error)
9741
9907
  });
9742
9908
  return;
9743
9909
  }
@@ -9846,7 +10012,7 @@ var AccountEnrollmentManager = class {
9846
10012
  id: input.id,
9847
10013
  ref: "",
9848
10014
  phase: "creating",
9849
- message: "正在创建账号"
10015
+ messageKey: "enrollmentCreating"
9850
10016
  }
9851
10017
  };
9852
10018
  this.tasks.set(input.id, task);
@@ -9854,7 +10020,7 @@ var AccountEnrollmentManager = class {
9854
10020
  task.state = {
9855
10021
  ...task.state,
9856
10022
  phase: "cleanup-needed",
9857
- message: "收尾未完成,请重试"
10023
+ messageKey: "enrollmentCleanupPending"
9858
10024
  };
9859
10025
  });
9860
10026
  return task.state;
@@ -9884,7 +10050,7 @@ var AccountEnrollmentManager = class {
9884
10050
  id,
9885
10051
  ref: "",
9886
10052
  phase: "cancelled",
9887
- message: "已取消"
10053
+ messageKey: "enrollmentCancelled"
9888
10054
  }
9889
10055
  };
9890
10056
  this.tasks.set(id, task);
@@ -9897,7 +10063,7 @@ var AccountEnrollmentManager = class {
9897
10063
  task.state = {
9898
10064
  ...task.state,
9899
10065
  phase: "cancelling",
9900
- message: "正在等待写入结束并清理"
10066
+ messageKey: "enrollmentCancelling"
9901
10067
  };
9902
10068
  await task.flow?.cancelAndDrain();
9903
10069
  await task.work;
@@ -9934,14 +10100,14 @@ var AccountEnrollmentManager = class {
9934
10100
  id,
9935
10101
  ref: record.ref,
9936
10102
  phase: "finished",
9937
- message: "账号开通完成"
10103
+ messageKey: "enrollmentFinished"
9938
10104
  });
9939
10105
  } catch {
9940
10106
  return this.publish(task, {
9941
10107
  id,
9942
10108
  ref: record.ref,
9943
10109
  phase: "naming",
9944
- message: "账号已登录,名称保存失败,请重试或接受已有名称",
10110
+ messageKey: "enrollmentNameFailed",
9945
10111
  ...name ? { suggestedName: name } : {}
9946
10112
  });
9947
10113
  }
@@ -9967,7 +10133,7 @@ var AccountEnrollmentManager = class {
9967
10133
  task.state = {
9968
10134
  ...task.state,
9969
10135
  phase: "cleanup-needed",
9970
- message: "清理未完成,请重试"
10136
+ messageKey: "enrollmentCleanupFailed"
9971
10137
  };
9972
10138
  });
9973
10139
  await task.work;
@@ -10035,7 +10201,7 @@ var AccountEnrollmentManager = class {
10035
10201
  if (!task.cancelled && status.state === "waiting") task.state = {
10036
10202
  ...task.state,
10037
10203
  phase: "waiting",
10038
- message: "等待浏览器登录",
10204
+ messageKey: "enrollmentWaiting",
10039
10205
  ...status.authUrl ? { authUrl: status.authUrl } : {}
10040
10206
  };
10041
10207
  if (task.cancelled) flow.cancelAndDrain();
@@ -10051,7 +10217,7 @@ var AccountEnrollmentManager = class {
10051
10217
  task.state = {
10052
10218
  ...task.state,
10053
10219
  phase: "naming",
10054
- message: "账号已登录,正在保存名称",
10220
+ messageKey: "enrollmentNaming",
10055
10221
  ...suggested ? { suggestedName: suggested } : {}
10056
10222
  };
10057
10223
  try {
@@ -10064,7 +10230,7 @@ var AccountEnrollmentManager = class {
10064
10230
  if (task.state.phase === "finished") return;
10065
10231
  task.state = {
10066
10232
  ...task.state,
10067
- message: "账号已登录,名称保存失败,请重试或接受已有名称"
10233
+ messageKey: "enrollmentNameFailed"
10068
10234
  };
10069
10235
  return;
10070
10236
  }
@@ -10076,7 +10242,7 @@ var AccountEnrollmentManager = class {
10076
10242
  else task.state = {
10077
10243
  ...task.state,
10078
10244
  phase: task.cancelled ? "cancelled" : "failed",
10079
- message: task.cancelled ? "已取消" : "账号开通失败,未创建账号"
10245
+ messageKey: task.cancelled ? "enrollmentCancelled" : "enrollmentFailed"
10080
10246
  };
10081
10247
  } finally {
10082
10248
  task.flow?.dispose();
@@ -10087,7 +10253,7 @@ var AccountEnrollmentManager = class {
10087
10253
  task.state = {
10088
10254
  ...task.state,
10089
10255
  phase: "writing",
10090
- message: "正在保存凭据"
10256
+ messageKey: "enrollmentWriting"
10091
10257
  };
10092
10258
  task.writeStarted = true;
10093
10259
  await this.deps.set(task.state.ref, key);
@@ -10128,7 +10294,7 @@ var AccountEnrollmentManager = class {
10128
10294
  id,
10129
10295
  ref,
10130
10296
  phase: task.cancelled ? "cancelled" : "failed",
10131
- message: task.cancelled ? "已取消并清理" : "开通失败,已清理临时账号"
10297
+ messageKey: task.cancelled ? "enrollmentCancelledCleaned" : "enrollmentFailedCleaned"
10132
10298
  };
10133
10299
  }
10134
10300
  settings() {
@@ -10177,12 +10343,134 @@ var AccountEnrollmentManager = class {
10177
10343
  id: record.id,
10178
10344
  ref: record.ref,
10179
10345
  phase: record.phase === "naming" ? "naming" : "cleanup-needed",
10180
- message: record.phase === "naming" ? "已登录账号等待确认名称" : "账号开通收尾未完成,请重试",
10346
+ messageKey: record.phase === "naming" ? "enrollmentRecordNaming" : "enrollmentRecordCleanup",
10181
10347
  ...record.phase === "naming" ? { suggestedName: record.label } : {}
10182
10348
  };
10183
10349
  }
10184
10350
  };
10185
10351
  //#endregion
10352
+ //#region src/config-volatile.ts
10353
+ /**
10354
+ * Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
10355
+ *
10356
+ * 0.1.7's `settings.describe()` projects each entry's schema through
10357
+ * `volatileForm()`, so a form contains ONLY nodes carrying `meta.volatile`
10358
+ * and a form edit is refused unless its path lies beneath a marked node. The
10359
+ * loader then resolves each marked field into a frozen `{ get() }` reference
10360
+ * that is written IN PLACE (no remount) and announced with
10361
+ * `loader/volatile-update`. Two helpers keep the rest of the plugin reading a
10362
+ * PLAIN `Config`: `markVolatile()` applies the mark, and
10363
+ * `unwrapVolatileConfig()` reads every top-level field through its reference
10364
+ * when one is present — FRESH per call, because a reference's identity is
10365
+ * stable while its value changes.
10366
+ *
10367
+ * Only TOP-LEVEL fields are marked (a form write names a top-level path such
10368
+ * as `accounts` or `modelVisibility.<id>`), so one unwrap level is complete.
10369
+ * The literal `apiKey` secret stays unmarked on purpose: no settings surface
10370
+ * writes it — page and TUI both write keys through the credentials seam — so a
10371
+ * config-file edit to it reloading the fiber is correct for a secret literal.
10372
+ *
10373
+ * `volatileWrite()` is the third helper and the only WRITE in this module: it
10374
+ * commits into a reference that belongs to ANOTHER plugin entry (`ctx.web`'s
10375
+ * `searchProvider`), which no host API addresses directly. It is the degraded
10376
+ * path behind `ctx.settings.update()`; see its own note for why the disk-backed
10377
+ * path is preferred.
10378
+ *
10379
+ * @module dsh-commandcode-provider/config-volatile
10380
+ */
10381
+ /**
10382
+ * Mark one schema field volatile.
10383
+ *
10384
+ * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
10385
+ * supported engine ships `~3.18.5`, so the
10386
+ * method is always present. It is reached through a structural member so the caller's
10387
+ * declared field type survives — the mark changes what the schema PARSES to (a
10388
+ * live reference), not the plugin-facing `Config` shape.
10389
+ */
10390
+ function markVolatile(schema) {
10391
+ return schema.volatile();
10392
+ }
10393
+ /**
10394
+ * Mark every field of a Config field dict volatile, except the named
10395
+ * composition-only secrets (`apiKey` here, which no settings surface writes).
10396
+ */
10397
+ function markVolatileFields(fields, skip = []) {
10398
+ return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
10399
+ }
10400
+ /**
10401
+ * Whether a resolved config value is a volatile reference.
10402
+ *
10403
+ * Duck-typed on the two properties `createVolatile` produces (frozen object
10404
+ * with a `get` method) rather than cosmokit's private `write` symbol, because
10405
+ * cosmokit is not a peer of this bundle. Config values are plain parsed JSON
10406
+ * (or `!!jsExpr` wrappers), which are never frozen, so the test cannot
10407
+ * misfire on an ordinary field.
10408
+ */
10409
+ function isVolatileRef(value) {
10410
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.isFrozen(value) && typeof value.get === "function";
10411
+ }
10412
+ /**
10413
+ * The commit protocol cosmokit's `createVolatile` installs on every reference.
10414
+ *
10415
+ * `Symbol.for` — not a local `Symbol()` — because cosmokit registers it
10416
+ * globally precisely so copies of the shared library loaded twice (ESM/CJS, two
10417
+ * node_modules trees) still recognize each other's references. Reaching it
10418
+ * through the global registry is what lets this bundle commit a value without
10419
+ * taking `@deepseek-ai/cosmokit` as a dependency.
10420
+ */
10421
+ const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
10422
+ /**
10423
+ * Commit a value into a RUNNING volatile reference and verify it landed.
10424
+ *
10425
+ * This is the DEGRADED write path, and it is deliberately second choice: the
10426
+ * reference is the live config the owner reads per call, but the value never
10427
+ * reaches the profile document, so the next loader commit for that entry (any
10428
+ * `loader/volatile-update`, including one caused by an unrelated field of the
10429
+ * same entry) re-resolves the raw config and overwrites it. The official
10430
+ * `ctx.settings.update()` path exists to avoid exactly that; this one keeps the
10431
+ * CURRENT process coherent when settings is unavailable or refuses the write.
10432
+ *
10433
+ * Reading back is not optional: the write method is an internal protocol, so a
10434
+ * future cosmokit that renames the symbol, hides it, or changes the value shape
10435
+ * must degrade to "not applied" instead of silently pretending success.
10436
+ *
10437
+ * @param ref - Candidate volatile reference; anything else returns `false`.
10438
+ * @param value - Value to commit.
10439
+ * @returns Whether the reference now reads back as `value`.
10440
+ */
10441
+ function volatileWrite(ref, value) {
10442
+ if (!isVolatileRef(ref)) return false;
10443
+ const write = ref[VOLATILE_WRITE];
10444
+ if (typeof write !== "function") return false;
10445
+ try {
10446
+ write.call(ref, value);
10447
+ } catch {
10448
+ return false;
10449
+ }
10450
+ try {
10451
+ return Object.is(ref.get(), value);
10452
+ } catch {
10453
+ return false;
10454
+ }
10455
+ }
10456
+ /**
10457
+ * Read one config object as PLAIN values.
10458
+ *
10459
+ * Every top-level field found to be a volatile reference is read through
10460
+ * `.get()` — freshly on every call, so a live update lands on the very next
10461
+ * read (the loader resolves EVERY marked node into a reference, unset fields
10462
+ * included, so a loader-parsed config always yields a fresh object here). The
10463
+ * pass-through for a field that is not a reference is what keeps the unmarked
10464
+ * `apiKey` literal readable.
10465
+ */
10466
+ function unwrapVolatileConfig(config) {
10467
+ const source = config;
10468
+ if (typeof source !== "object" || source === null) return config;
10469
+ const plain = {};
10470
+ for (const [field, value] of Object.entries(source)) plain[field] = isVolatileRef(value) ? value.get() : value;
10471
+ return plain;
10472
+ }
10473
+ //#endregion
10186
10474
  //#region src/web-search.ts
10187
10475
  /**
10188
10476
  * Command Code web-search provider over `ctx.web`.
@@ -10195,6 +10483,15 @@ var AccountEnrollmentManager = class {
10195
10483
  * therefore reuses the plugin's existing credential chain and `apiBase`, so
10196
10484
  * the model-facing `web_search` tool needs no separate configuration.
10197
10485
  *
10486
+ * The second half of this module is the SELECTION seam: which registered search
10487
+ * backend `ctx.web` actually runs. That seam has TWO engine shapes, and both are
10488
+ * supported. dsh 0.2.1-rc.2 turned the choice into a per-call `Volatile` field of
10489
+ * the `web` profile entry, so it is changed through the host settings service
10490
+ * rather than by assigning a runtime field. Engines up to and including
10491
+ * 0.2.1-alpha.4 kept it in the private instance field `searchProviderId`, which
10492
+ * has no settings surface and can only be assigned in memory — see
10493
+ * {@link createCommandCodeSearchSelection}.
10494
+ *
10198
10495
  * @module dsh-commandcode-provider/web-search
10199
10496
  */
10200
10497
  /** Stable id this provider registers under in `ctx.web`. */
@@ -10206,54 +10503,326 @@ const COMMANDCODE_SEARCH_PROVIDER_ID = "commandcode";
10206
10503
  * PREVIOUSLY selected backend, never this default (issue #26).
10207
10504
  */
10208
10505
  const DEFAULT_WEB_SEARCH_PROVIDER_ID = "deepseek-official";
10506
+ /**
10507
+ * Profile entry id whose volatile `searchProvider` selects the search backend.
10508
+ *
10509
+ * The hosting profile declares it in `dsh-base`'s cordis patch as
10510
+ * `- id: web` / `name: '@deepseek-ai/dsh-web'`, and the shipped web-search
10511
+ * settings page addresses the very same namespace — that page and this plugin
10512
+ * are therefore two writers of ONE field (see
10513
+ * {@link createCommandCodeSearchSelection} for how they avoid fighting).
10514
+ */
10515
+ const WEB_SELECTION_NAMESPACE = "web";
10209
10516
  /** Fresh selection state: the plugin starts out not owning the selection. */
10210
10517
  function commandCodeSearchSelection() {
10211
10518
  return {
10212
10519
  owner: false,
10213
- displaced: void 0,
10214
- preexisting: false
10520
+ preexisting: false,
10521
+ foreign: void 0,
10522
+ displaced: void 0
10215
10523
  };
10216
10524
  }
10525
+ /** Read one member of a plain object or array without trusting its shape. */
10526
+ function member(value, key) {
10527
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
10528
+ return value[key];
10529
+ }
10530
+ /** A string member, or `undefined` for anything else (including `null`). */
10531
+ function text(value) {
10532
+ return typeof value === "string" ? value : void 0;
10533
+ }
10534
+ /** The settings row describing the `web` entry, when the host serves one. */
10535
+ function selectionRow(settings) {
10536
+ let rows;
10537
+ try {
10538
+ rows = settings.describe();
10539
+ } catch {
10540
+ return;
10541
+ }
10542
+ if (!Array.isArray(rows)) return void 0;
10543
+ return rows.find((row) => text(member(row, "ns")) === "web");
10544
+ }
10217
10545
  /**
10218
- * Reach one end of the `webSearch` toggle without trampling sibling search
10219
- * providers (issue #26).
10546
+ * The volatile reference backing `web.searchProvider`.
10220
10547
  *
10221
- * - Enabling writes `commandcode` and remembers whatever it displaced. When
10222
- * the plugin already owns the selection, the original `displaced` is kept —
10223
- * the field currently holds our own id, which must never be mistaken for the
10224
- * user's backend.
10225
- * - Disabling hands the selection back to the remembered backend only when
10226
- * this plugin actually took it over. A fresh boot straight into
10227
- * `webSearch: false`, or a `preexisting` field, leaves it ALONE: writing the
10228
- * empty memory back would clear the user's own `searchProvider: commandcode`
10229
- * pin and hand the selection to dsh-web's auto-select, where a second usable
10230
- * provider makes every later search throw `WEB_PROVIDER_AMBIGUOUS`.
10548
+ * `config` is a TypeScript-private parameter property on `WebRuntime`, so at
10549
+ * runtime it is an ordinary own field — the only handle to a reference the host
10550
+ * exposes no public accessor for. Reading it structurally means a hardened
10551
+ * runtime degrades to "not writable" instead of throwing.
10552
+ */
10553
+ function searchProviderRef(web) {
10554
+ return member(member(web, "config"), "searchProvider");
10555
+ }
10556
+ /**
10557
+ * The pre-rc.2 instance field, or `undefined` when this runtime does not carry
10558
+ * it.
10231
10559
  *
10232
- * Never throws: like the low-level rewrite, a hardened runtime shape degrades
10233
- * to registered-but-unselected.
10560
+ * An OWN property is the discriminator, not a value test: a runtime of the
10561
+ * reference shape never has `searchProviderId` at all, so the two shapes cannot
10562
+ * both answer and neither needs to be preferred over the other.
10234
10563
  */
10235
- function applyCommandCodeSearchSelection(web, state, enable) {
10236
- try {
10237
- const field = web;
10238
- if (enable) {
10239
- if (state.owner) {
10240
- field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
10564
+ function legacySearchField(web) {
10565
+ if (!Object.prototype.hasOwnProperty.call(web, "searchProviderId")) return void 0;
10566
+ const shaped = web;
10567
+ return {
10568
+ read: () => text(shaped.searchProviderId),
10569
+ write: (value) => {
10570
+ try {
10571
+ shaped.searchProviderId = value;
10572
+ return true;
10573
+ } catch {
10574
+ return false;
10575
+ }
10576
+ }
10577
+ };
10578
+ }
10579
+ /** Observe the live selection; `undefined` when nothing can be observed. */
10580
+ function readView(deps) {
10581
+ const settings = deps.settings();
10582
+ if (settings !== void 0) {
10583
+ const row = selectionRow(settings);
10584
+ if (row !== void 0) {
10585
+ const user = member(row, "user");
10586
+ return {
10587
+ configured: text(member(member(row, "value"), "searchProvider")),
10588
+ pinned: text(member(user, "searchProvider")),
10589
+ base: text(member(member(row, "base"), "searchProvider")),
10590
+ user: typeof user === "object" && user !== null && !Array.isArray(user) ? { ...user } : {},
10591
+ persistent: true
10592
+ };
10593
+ }
10594
+ }
10595
+ const ref = searchProviderRef(deps.web);
10596
+ if (isVolatileRef(ref)) {
10597
+ let current;
10598
+ try {
10599
+ current = ref.get();
10600
+ } catch {
10601
+ return;
10602
+ }
10603
+ if (current !== void 0 && typeof current !== "string") return void 0;
10604
+ return {
10605
+ configured: current,
10606
+ pinned: void 0,
10607
+ base: void 0,
10608
+ user: {},
10609
+ persistent: false
10610
+ };
10611
+ }
10612
+ const legacy = legacySearchField(deps.web);
10613
+ if (legacy === void 0) return void 0;
10614
+ return {
10615
+ configured: legacy.read(),
10616
+ pinned: void 0,
10617
+ base: void 0,
10618
+ user: {},
10619
+ persistent: false
10620
+ };
10621
+ }
10622
+ /**
10623
+ * The selection seam over `ctx.web`.
10624
+ *
10625
+ * ## Why settings rather than the runtime instance
10626
+ *
10627
+ * 0.2.1-rc.2's `WebRuntime` reads `this.config.searchProvider.get()` on EVERY
10628
+ * search and fetch, and the loader commits a settings write into that same
10629
+ * reference in place (`Entry._commitVolatile`) without remounting the entry.
10630
+ * Writing the reference directly therefore works, but leaves the value out of
10631
+ * the profile: the next commit for that entry — including one caused by an
10632
+ * unrelated field such as `fetchProvider` — re-resolves the raw config and
10633
+ * overwrites it. `ctx.settings.update()` / `replace()` is the documented path
10634
+ * and the one the shipped web-search settings page uses, so this plugin takes
10635
+ * it first and falls back to the reference only when settings is unavailable or
10636
+ * refuses (see `volatileWrite`).
10637
+ *
10638
+ * ## Two engine shapes
10639
+ *
10640
+ * Everything above describes an rc.2+ runtime, where the selection is a
10641
+ * reference. PRE-rc.2 engines (0.2.0-rc.2 and the 0.2.1 alphas) have neither
10642
+ * that reference nor a settings row for `web`: there the selection is the
10643
+ * private instance field `searchProviderId`, memory-only by nature. Those
10644
+ * engines therefore run the degraded path BY CONSTRUCTION — no profile write
10645
+ * exists for it and the user layer is unreadable, so a foreign pin cannot be
10646
+ * seen and the takeover/restore semantics are the ones this plugin shipped
10647
+ * before rc.2: remember the displaced backend, hand back only what we took. The
10648
+ * shape is resolved per observation, so one build serves both.
10649
+ *
10650
+ * ## Two writers, one field
10651
+ *
10652
+ * The shipped web-search settings page writes `web.searchProvider` too
10653
+ * (`deepseek-search-api` / `deepseek-official`). This seam never displaces a
10654
+ * pin it did not write: a non-`commandcode` value in the USER LAYER stops the
10655
+ * takeover, the value is recorded on {@link CommandCodeSearchSelection.foreign}
10656
+ * and named in one `info` line, and the provider stays registered but
10657
+ * unselected. A value the user layer does not carry is the deployment's own
10658
+ * default, and taking it over is the toggle's whole purpose. A user-layer
10659
+ * `commandcode` is treated as ours: nothing else writes that id, so it can only
10660
+ * be a previous run of this plugin or a deliberate manual pin, and turning the
10661
+ * toggle off restores the composed default in both cases.
10662
+ *
10663
+ * @param deps - the running `web` service, a settings resolver, and a log sink.
10664
+ * @returns a synchronous handle; failures are logged, never thrown.
10665
+ */
10666
+ function createCommandCodeSearchSelection(deps) {
10667
+ const state = commandCodeSearchSelection();
10668
+ let pending;
10669
+ let activeKey;
10670
+ let running = false;
10671
+ let closed = false;
10672
+ let waiters = [];
10673
+ const warn = (message) => {
10674
+ try {
10675
+ deps.logger.warn(message);
10676
+ } catch {}
10677
+ };
10678
+ const info = (message) => {
10679
+ try {
10680
+ deps.logger.info(message);
10681
+ } catch {}
10682
+ };
10683
+ /**
10684
+ * Commit the selection in memory; the last resort before giving up.
10685
+ *
10686
+ * The volatile reference is tried first because on an rc.2+ runtime that is
10687
+ * what `search()` reads. A PRE-rc.2 runtime carries no reference at all, and
10688
+ * there the private instance field IS the seam — the fallback is a different
10689
+ * engine shape, not a second-best copy of the same write.
10690
+ */
10691
+ const memoryWrite = (value) => {
10692
+ if (volatileWrite(searchProviderRef(deps.web), value)) return true;
10693
+ if (legacySearchField(deps.web)?.write(value) === true) return true;
10694
+ warn("llm-commandcode: web search selection could not be applied (settings refused the write and the running web runtime is not writable); the provider stays registered but unselected");
10695
+ return false;
10696
+ };
10697
+ /** Run one intent against a FRESH observation; never throws. */
10698
+ const perform = async (intent) => {
10699
+ let view;
10700
+ try {
10701
+ view = readView(deps);
10702
+ } catch {
10703
+ view = void 0;
10704
+ }
10705
+ if (view === void 0) return;
10706
+ const settings = view.persistent ? deps.settings() : void 0;
10707
+ if (intent.kind === "take") {
10708
+ if (view.pinned !== void 0 && view.pinned !== "commandcode") return;
10709
+ if (settings !== void 0) try {
10710
+ await settings.update("web", { searchProvider: COMMANDCODE_SEARCH_PROVIDER_ID });
10241
10711
  return;
10712
+ } catch (error) {
10713
+ warn(`llm-commandcode: writing web.searchProvider through settings failed (${error instanceof Error ? error.message : String(error)}); falling back to the running config`);
10242
10714
  }
10243
- const prior = field.searchProviderId;
10244
- state.preexisting = prior === COMMANDCODE_SEARCH_PROVIDER_ID;
10245
- state.displaced = state.preexisting ? void 0 : prior;
10246
- field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
10247
- state.owner = true;
10715
+ memoryWrite(COMMANDCODE_SEARCH_PROVIDER_ID);
10248
10716
  return;
10249
10717
  }
10250
- if (state.owner) {
10718
+ if (view.pinned !== void 0 && view.pinned !== "commandcode") return;
10719
+ if (settings !== void 0) {
10720
+ const section = { ...view.user };
10721
+ delete section.searchProvider;
10722
+ if (view.base !== void 0) section.searchProvider = view.base;
10723
+ try {
10724
+ await settings.replace("web", section);
10725
+ return;
10726
+ } catch (error) {
10727
+ warn(`llm-commandcode: restoring web.searchProvider through settings failed (${error instanceof Error ? error.message : String(error)}); falling back to the running config`);
10728
+ }
10729
+ }
10730
+ memoryWrite(intent.displaced);
10731
+ };
10732
+ const flush = () => {
10733
+ const done = waiters;
10734
+ waiters = [];
10735
+ for (const resolve of done) resolve();
10736
+ };
10737
+ const drain = async () => {
10738
+ running = true;
10739
+ try {
10740
+ while (pending !== void 0 && !closed) {
10741
+ const item = pending;
10742
+ pending = void 0;
10743
+ activeKey = item.key;
10744
+ await perform(item.intent);
10745
+ }
10746
+ } finally {
10747
+ running = false;
10748
+ activeKey = void 0;
10749
+ flush();
10750
+ }
10751
+ };
10752
+ /**
10753
+ * Queue one intent, skipping an identical one already queued or in flight. A
10754
+ * later DIFFERENT intent replaces the queued one: the toggle is a state, not a
10755
+ * log, so only the newest position has to land.
10756
+ */
10757
+ const submit = (key, intent) => {
10758
+ if (closed) return;
10759
+ if (key === activeKey && pending === void 0) return;
10760
+ if (pending?.key === key) return;
10761
+ pending = {
10762
+ key,
10763
+ intent
10764
+ };
10765
+ if (!running) drain();
10766
+ };
10767
+ const apply = (enable) => {
10768
+ try {
10769
+ const view = readView(deps);
10770
+ if (view === void 0) return;
10771
+ if (!enable) {
10772
+ if (!state.owner) return;
10773
+ state.owner = false;
10774
+ if (state.preexisting) {
10775
+ state.preexisting = false;
10776
+ return;
10777
+ }
10778
+ submit("restore", {
10779
+ kind: "restore",
10780
+ displaced: state.displaced
10781
+ });
10782
+ return;
10783
+ }
10784
+ if (view.pinned !== void 0 && view.pinned !== "commandcode") {
10785
+ if (state.foreign !== view.pinned) {
10786
+ state.foreign = view.pinned;
10787
+ 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`);
10788
+ }
10789
+ state.owner = false;
10790
+ state.preexisting = false;
10791
+ return;
10792
+ }
10793
+ state.foreign = void 0;
10794
+ if (view.configured === "commandcode") {
10795
+ state.owner = view.pinned === "commandcode" || !view.persistent && state.owner;
10796
+ state.preexisting = !state.owner;
10797
+ if (state.owner && state.displaced === void 0) state.displaced = view.base;
10798
+ return;
10799
+ }
10800
+ state.displaced = view.configured;
10801
+ state.preexisting = false;
10802
+ state.owner = true;
10803
+ submit("take", {
10804
+ kind: "take",
10805
+ displaced: view.configured
10806
+ });
10807
+ } catch (error) {
10808
+ warn(`llm-commandcode: aligning the web search selection failed (${error instanceof Error ? error.message : String(error)})`);
10809
+ }
10810
+ };
10811
+ return {
10812
+ state,
10813
+ apply,
10814
+ settled: () => running || pending !== void 0 ? new Promise((resolve) => {
10815
+ waiters.push(resolve);
10816
+ }) : Promise.resolve(),
10817
+ release: () => {
10818
+ closed = true;
10819
+ pending = void 0;
10820
+ if (state.owner && !state.preexisting) memoryWrite(state.displaced);
10251
10821
  state.owner = false;
10252
- if (!state.preexisting) field.searchProviderId = state.displaced;
10253
10822
  state.preexisting = false;
10254
- return;
10823
+ if (!running) flush();
10255
10824
  }
10256
- } catch {}
10825
+ };
10257
10826
  }
10258
10827
  /** Command Code's lower/upper bound on `numResults` (from the CLI's `web_search` schema). */
10259
10828
  const MIN_NUM_RESULTS = 1;
@@ -10337,7 +10906,7 @@ var CommandCodeSearchProvider = class {
10337
10906
  });
10338
10907
  } catch (error) {
10339
10908
  if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
10340
- throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10909
+ throw new WebError(`Command Code web search request failed: ${readableErrorText(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10341
10910
  }
10342
10911
  if (!response.ok) {
10343
10912
  let message = `Command Code web search failed (HTTP ${response.status})`;
@@ -10391,7 +10960,7 @@ var CommandCodeSearchProvider = class {
10391
10960
  if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
10392
10961
  if (code === "INVALID_CREDENTIAL" || code === "RATE_LIMIT") throw new WebError(error.message, "WEB_PROVIDER_ERROR", { cause: error });
10393
10962
  }
10394
- throw new WebError(`Command Code web search credential resolution failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10963
+ throw new WebError(`Command Code web search credential resolution failed: ${readableErrorText(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10395
10964
  }
10396
10965
  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");
10397
10966
  return key;
@@ -10722,7 +11291,7 @@ function applyCommandCodeTuiSettings(ctx, deps) {
10722
11291
  dispose: service.register(section)
10723
11292
  };
10724
11293
  } catch (error) {
10725
- ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${error instanceof Error ? error.message : String(error)}`);
11294
+ ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${readableErrorText(error)}`);
10726
11295
  }
10727
11296
  };
10728
11297
  refresh();
@@ -10805,78 +11374,6 @@ function transportBudgetMessage(failureMessage, maxRetries) {
10805
11374
  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`;
10806
11375
  }
10807
11376
  //#endregion
10808
- //#region src/config-volatile.ts
10809
- /**
10810
- * Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
10811
- *
10812
- * 0.1.7's `settings.describe()` projects each entry's schema through
10813
- * `volatileForm()`, so a form contains ONLY nodes carrying `meta.volatile`
10814
- * and a form edit is refused unless its path lies beneath a marked node. The
10815
- * loader then resolves each marked field into a frozen `{ get() }` reference
10816
- * that is written IN PLACE (no remount) and announced with
10817
- * `loader/volatile-update`. Two helpers keep the rest of the plugin reading a
10818
- * PLAIN `Config`: `markVolatile()` applies the mark, and
10819
- * `unwrapVolatileConfig()` reads every top-level field through its reference
10820
- * when one is present — FRESH per call, because a reference's identity is
10821
- * stable while its value changes.
10822
- *
10823
- * Only TOP-LEVEL fields are marked (a form write names a top-level path such
10824
- * as `accounts` or `modelVisibility.<id>`), so one unwrap level is complete.
10825
- * The literal `apiKey` secret stays unmarked on purpose: no settings surface
10826
- * writes it — page and TUI both write keys through the credentials seam — so a
10827
- * config-file edit to it reloading the fiber is correct for a secret literal.
10828
- *
10829
- * @module dsh-commandcode-provider/config-volatile
10830
- */
10831
- /**
10832
- * Mark one schema field volatile.
10833
- *
10834
- * `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and every
10835
- * supported engine ships `~3.18.4` or `~3.18.5-alpha.1`, so the
10836
- * method is always present. It is reached through a structural member so the caller's
10837
- * declared field type survives — the mark changes what the schema PARSES to (a
10838
- * live reference), not the plugin-facing `Config` shape.
10839
- */
10840
- function markVolatile(schema) {
10841
- return schema.volatile();
10842
- }
10843
- /**
10844
- * Mark every field of a Config field dict volatile, except the named
10845
- * composition-only secrets (`apiKey` here, which no settings surface writes).
10846
- */
10847
- function markVolatileFields(fields, skip = []) {
10848
- return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
10849
- }
10850
- /**
10851
- * Whether a resolved config value is a volatile reference.
10852
- *
10853
- * Duck-typed on the two properties `createVolatile` produces (frozen object
10854
- * with a `get` method) rather than cosmokit's private `write` symbol, because
10855
- * cosmokit is not a peer of this bundle. Config values are plain parsed JSON
10856
- * (or `!!jsExpr` wrappers), which are never frozen, so the test cannot
10857
- * misfire on an ordinary field.
10858
- */
10859
- function isVolatileRef(value) {
10860
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.isFrozen(value) && typeof value.get === "function";
10861
- }
10862
- /**
10863
- * Read one config object as PLAIN values.
10864
- *
10865
- * Every top-level field found to be a volatile reference is read through
10866
- * `.get()` — freshly on every call, so a live update lands on the very next
10867
- * read (the loader resolves EVERY marked node into a reference, unset fields
10868
- * included, so a loader-parsed config always yields a fresh object here). The
10869
- * pass-through for a field that is not a reference is what keeps the unmarked
10870
- * `apiKey` literal readable.
10871
- */
10872
- function unwrapVolatileConfig(config) {
10873
- const source = config;
10874
- if (typeof source !== "object" || source === null) return config;
10875
- const plain = {};
10876
- for (const [field, value] of Object.entries(source)) plain[field] = isVolatileRef(value) ? value.get() : value;
10877
- return plain;
10878
- }
10879
- //#endregion
10880
11377
  //#region src/index.ts
10881
11378
  /**
10882
11379
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -10936,6 +11433,7 @@ const Config = z.object(markVolatileFields({
10936
11433
  modelVisibility: z.dict(z.boolean()),
10937
11434
  contextWindow: z.number().min(1).max(CONTEXT_WINDOW_MAX).step(1),
10938
11435
  contextWindowOverrides: z.dict(z.number().min(1).max(CONTEXT_WINDOW_MAX).step(1)),
11436
+ maxOutputTokens: z.number().min(0).max(MAX_OUTPUT_TOKEN_LIMIT).step(1),
10939
11437
  webSearch: z.boolean().default(true),
10940
11438
  showSidebarQuota: z.boolean().default(false),
10941
11439
  accounts: z.array(z.object({
@@ -10983,6 +11481,7 @@ function resolveAdapterOptions(config) {
10983
11481
  modelVisibility: readModelVisibility(config.modelVisibility),
10984
11482
  contextWindow: isValidContextWindow(config.contextWindow) ? config.contextWindow : void 0,
10985
11483
  contextWindowOverrides: readContextWindowOverrides(config.contextWindowOverrides),
11484
+ maxOutputTokens: isValidMaxOutputTokens(config.maxOutputTokens) ? config.maxOutputTokens : void 0,
10986
11485
  zdr: config.zdr === true
10987
11486
  };
10988
11487
  }
@@ -11143,7 +11642,7 @@ function apply(ctx, config) {
11143
11642
  else try {
11144
11643
  report = await adapter.getUsage(account.key, connection);
11145
11644
  } catch (error) {
11146
- report = { failures: [error instanceof Error ? error.message : String(error)] };
11645
+ report = { failures: [readableErrorText(error)] };
11147
11646
  }
11148
11647
  const state = account?.state;
11149
11648
  const usable = accountUsable(state);
@@ -11225,13 +11724,24 @@ function apply(ctx, config) {
11225
11724
  listModels: catalogForEditors,
11226
11725
  analytics: createAnalyticsReader(ctx, adapter)
11227
11726
  });
11228
- const searchSelection = commandCodeSearchSelection();
11727
+ const selectionLogger = {
11728
+ warn: (message) => {
11729
+ ctx.logger.warn(message);
11730
+ },
11731
+ info: (message) => {
11732
+ ctx.logger.info(message);
11733
+ }
11734
+ };
11735
+ let searchSelection;
11229
11736
  const applySearchSelection = (enabled) => {
11230
- if (webRuntime !== void 0) applyCommandCodeSearchSelection(webRuntime, searchSelection, enabled);
11737
+ searchSelection?.apply(enabled);
11231
11738
  };
11232
- let webRuntime;
11233
11739
  ctx.inject(["web"], (webCtx) => {
11234
- webRuntime = webCtx.web;
11740
+ searchSelection = createCommandCodeSearchSelection({
11741
+ web: webCtx.web,
11742
+ settings: () => ctx.get("settings"),
11743
+ logger: selectionLogger
11744
+ });
11235
11745
  webCtx.web.registerSearchProvider(new CommandCodeSearchProvider({
11236
11746
  resolveKey: async (apiBase) => {
11237
11747
  const connection = options();
@@ -11243,8 +11753,8 @@ function apply(ctx, config) {
11243
11753
  }));
11244
11754
  applySearchSelection(current().webSearch ?? true);
11245
11755
  webCtx.effect(() => () => {
11246
- webRuntime = void 0;
11247
- applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
11756
+ searchSelection?.release();
11757
+ searchSelection = void 0;
11248
11758
  }, "dsh-commandcode-provider: web search selection");
11249
11759
  });
11250
11760
  let refreshTuiSettings;
@@ -11289,6 +11799,7 @@ function apply(ctx, config) {
11289
11799
  dispose();
11290
11800
  };
11291
11801
  }, "dsh-commandcode-provider: settings auto-form policy");
11802
+ applySearchSelection(current().webSearch ?? true);
11292
11803
  });
11293
11804
  const loaderEvents = ctx;
11294
11805
  ctx.effect(() => {
@@ -11302,6 +11813,6 @@ function apply(ctx, config) {
11302
11813
  }, "dsh-commandcode-provider: volatile config updates");
11303
11814
  }
11304
11815
  //#endregion
11305
- 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 };
11816
+ 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 };
11306
11817
 
11307
11818
  //# sourceMappingURL=index.js.map