@mars-sea/dsh-commandcode-provider 0.13.4 → 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
@@ -3676,6 +3676,140 @@ function effectiveContextWindow(catalog, model, options) {
3676
3676
  if (isValidContextWindow(options.contextWindow)) return options.contextWindow;
3677
3677
  return catalog;
3678
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
+ }
3679
3813
  const THROTTLED_CODE = "THROTTLED";
3680
3814
  const boundedCodes = /* @__PURE__ */ new Set([
3681
3815
  "EMPTY_RESPONSE",
@@ -6148,33 +6282,11 @@ const COMMAND_CODE_CLI_VERSION = "1.82.5";
6148
6282
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
6149
6283
  /**
6150
6284
  * 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).
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.
6176
6288
  */
6177
- const DEFAULT_GENERATE_MAX_TOKENS = 131072;
6289
+ const DEFAULT_GENERATE_MAX_TOKENS = MAX_OUTPUT_TOKEN_LIMIT;
6178
6290
  const MODELS_TIMEOUT_MS = 1e4;
6179
6291
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
6180
6292
  const BILLING_ACCESS_TTL_MS = 3e5;
@@ -7405,7 +7517,7 @@ async function buildCliBody(options, connection, facts) {
7405
7517
  input_schema: toolParametersSchema(tool.parameters)
7406
7518
  })),
7407
7519
  system: cliSystem(facts.systemText),
7408
- max_tokens: facts.maxTokens,
7520
+ ...facts.omitMaxTokens ? {} : { max_tokens: facts.maxTokens },
7409
7521
  temperature: options.temperature ?? .3,
7410
7522
  stream: true,
7411
7523
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
@@ -7431,7 +7543,7 @@ async function buildOpenAIBody(options, facts) {
7431
7543
  model: options.model,
7432
7544
  messages: openAiMessages,
7433
7545
  ...openAiTools.length > 0 ? { tools: openAiTools } : {},
7434
- max_tokens: facts.maxTokens,
7546
+ ...facts.omitMaxTokens ? {} : { max_tokens: facts.maxTokens },
7435
7547
  temperature: options.temperature ?? .3,
7436
7548
  stream: true,
7437
7549
  ...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
@@ -7494,6 +7606,16 @@ async function buildMessagesBody(options, facts) {
7494
7606
  return body;
7495
7607
  }
7496
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
+ /**
7497
7619
  * One connect attempt's inputs: everything `connectGenerate` needs beyond
7498
7620
  * the per-attempt key and protocol. Passed explicitly (not closed over) so
7499
7621
  * the rotation loop's mutation of `protocol`/`body` stays visible at the
@@ -7857,7 +7979,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
7857
7979
  toolUpdate: "addition-only",
7858
7980
  ...entry !== void 0 && contextWindow !== void 0 ? {
7859
7981
  context: { contextWindow },
7860
- defaultMaxTokens: Math.min(catalogModelMaxTokens(connection.apiBase, entry, contextWindow), DEFAULT_GENERATE_MAX_TOKENS)
7982
+ defaultMaxTokens: applyMaxOutputTokens(Math.min(catalogModelMaxTokens(connection.apiBase, entry, contextWindow), DEFAULT_GENERATE_MAX_TOKENS), connection.maxOutputTokens)
7861
7983
  } : {},
7862
7984
  ...efforts ? { reasoning: { efforts: efforts.map((effort) => ({
7863
7985
  id: ReasoningEffortId(effort),
@@ -8137,7 +8259,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8137
8259
  }
8138
8260
  return record;
8139
8261
  } catch (error) {
8140
- failures.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
8262
+ failures.push(`${path}: ${readableErrorText(error)}`);
8141
8263
  failed.push({ kind: "transport" });
8142
8264
  return;
8143
8265
  }
@@ -8281,7 +8403,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8281
8403
  options.signal?.throwIfAborted();
8282
8404
  const modelEntry = catalog.find((row) => row.id === options.model);
8283
8405
  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);
8406
+ const initialMax = applyMaxOutputTokens(Math.min(options.maxTokens ?? Infinity, modelMax, DEFAULT_GENERATE_MAX_TOKENS), connection.maxOutputTokens);
8285
8407
  const budget = {
8286
8408
  max: initialMax,
8287
8409
  sent: initialMax
@@ -8332,17 +8454,16 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8332
8454
  const buildBody = async (target) => {
8333
8455
  const finishBody = timing.phase("body");
8334
8456
  try {
8457
+ const omitMaxTokens = omitOutputTokens(connection.maxOutputTokens, target, options.maxTokens);
8335
8458
  const facts = {
8336
8459
  maxTokens,
8460
+ omitMaxTokens,
8337
8461
  reasoningEffort,
8338
8462
  selectableEffort: supported !== void 0 && supported.length > 0,
8339
8463
  systemText,
8340
8464
  readImage
8341
8465
  };
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;
8466
+ return target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
8346
8467
  } finally {
8347
8468
  finishBody();
8348
8469
  }
@@ -8392,8 +8513,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
8392
8513
  if (lower < maxTokens) {
8393
8514
  maxTokens = lower;
8394
8515
  budget.max = Math.min(budget.max, lower);
8395
- if (protocol === "cli") recordOrEmpty(body.params).max_tokens = lower;
8396
- else body.max_tokens = lower;
8516
+ if (!omitOutputTokens(connection.maxOutputTokens, protocol, options.maxTokens)) assignMaxTokens(body, protocol, lower);
8397
8517
  serializedBodies.delete(body);
8398
8518
  }
8399
8519
  budget.sent = maxTokens;
@@ -8707,7 +8827,7 @@ function commandDefinition(deps) {
8707
8827
  text: renderReport(await adapter.getUsage(), locale)
8708
8828
  };
8709
8829
  } catch (error) {
8710
- const message = error instanceof Error ? error.message : String(error);
8830
+ const message = readableErrorText(error);
8711
8831
  return {
8712
8832
  kind: "error",
8713
8833
  text: commandCopy(locale, "errorText").replace("{message}", message)
@@ -9025,6 +9145,30 @@ const LOGIN_HOST_CONTRIBUTION = {
9025
9145
  };
9026
9146
  //#endregion
9027
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
+ ];
9028
9172
  const guard = makeBoundaryValidator("commandcode/enrollment");
9029
9173
  const phases = [
9030
9174
  "creating",
@@ -9041,11 +9185,13 @@ function parseEnrollmentState(value) {
9041
9185
  const source = guard.record(value, "state");
9042
9186
  const phase = source.phase;
9043
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");
9044
9190
  const state = {
9045
9191
  id: guard.stringField(source, "id", "id"),
9046
9192
  ref: guard.stringField(source, "ref", "ref"),
9047
9193
  phase,
9048
- message: guard.stringField(source, "message", "message")
9194
+ messageKey
9049
9195
  };
9050
9196
  for (const field of ["authUrl", "suggestedName"]) if (source[field] !== void 0) state[field] = guard.stringField(source, field, field);
9051
9197
  return state;
@@ -9737,7 +9883,7 @@ var CommandCodeLoginFlow = class {
9737
9883
  this.setStatus({
9738
9884
  state: "failed",
9739
9885
  reason: "unavailable",
9740
- message: error instanceof Error ? error.message : String(error)
9886
+ message: readableErrorText(error)
9741
9887
  });
9742
9888
  return;
9743
9889
  }
@@ -9846,7 +9992,7 @@ var AccountEnrollmentManager = class {
9846
9992
  id: input.id,
9847
9993
  ref: "",
9848
9994
  phase: "creating",
9849
- message: "正在创建账号"
9995
+ messageKey: "enrollmentCreating"
9850
9996
  }
9851
9997
  };
9852
9998
  this.tasks.set(input.id, task);
@@ -9854,7 +10000,7 @@ var AccountEnrollmentManager = class {
9854
10000
  task.state = {
9855
10001
  ...task.state,
9856
10002
  phase: "cleanup-needed",
9857
- message: "收尾未完成,请重试"
10003
+ messageKey: "enrollmentCleanupPending"
9858
10004
  };
9859
10005
  });
9860
10006
  return task.state;
@@ -9884,7 +10030,7 @@ var AccountEnrollmentManager = class {
9884
10030
  id,
9885
10031
  ref: "",
9886
10032
  phase: "cancelled",
9887
- message: "已取消"
10033
+ messageKey: "enrollmentCancelled"
9888
10034
  }
9889
10035
  };
9890
10036
  this.tasks.set(id, task);
@@ -9897,7 +10043,7 @@ var AccountEnrollmentManager = class {
9897
10043
  task.state = {
9898
10044
  ...task.state,
9899
10045
  phase: "cancelling",
9900
- message: "正在等待写入结束并清理"
10046
+ messageKey: "enrollmentCancelling"
9901
10047
  };
9902
10048
  await task.flow?.cancelAndDrain();
9903
10049
  await task.work;
@@ -9934,14 +10080,14 @@ var AccountEnrollmentManager = class {
9934
10080
  id,
9935
10081
  ref: record.ref,
9936
10082
  phase: "finished",
9937
- message: "账号开通完成"
10083
+ messageKey: "enrollmentFinished"
9938
10084
  });
9939
10085
  } catch {
9940
10086
  return this.publish(task, {
9941
10087
  id,
9942
10088
  ref: record.ref,
9943
10089
  phase: "naming",
9944
- message: "账号已登录,名称保存失败,请重试或接受已有名称",
10090
+ messageKey: "enrollmentNameFailed",
9945
10091
  ...name ? { suggestedName: name } : {}
9946
10092
  });
9947
10093
  }
@@ -9967,7 +10113,7 @@ var AccountEnrollmentManager = class {
9967
10113
  task.state = {
9968
10114
  ...task.state,
9969
10115
  phase: "cleanup-needed",
9970
- message: "清理未完成,请重试"
10116
+ messageKey: "enrollmentCleanupFailed"
9971
10117
  };
9972
10118
  });
9973
10119
  await task.work;
@@ -10035,7 +10181,7 @@ var AccountEnrollmentManager = class {
10035
10181
  if (!task.cancelled && status.state === "waiting") task.state = {
10036
10182
  ...task.state,
10037
10183
  phase: "waiting",
10038
- message: "等待浏览器登录",
10184
+ messageKey: "enrollmentWaiting",
10039
10185
  ...status.authUrl ? { authUrl: status.authUrl } : {}
10040
10186
  };
10041
10187
  if (task.cancelled) flow.cancelAndDrain();
@@ -10051,7 +10197,7 @@ var AccountEnrollmentManager = class {
10051
10197
  task.state = {
10052
10198
  ...task.state,
10053
10199
  phase: "naming",
10054
- message: "账号已登录,正在保存名称",
10200
+ messageKey: "enrollmentNaming",
10055
10201
  ...suggested ? { suggestedName: suggested } : {}
10056
10202
  };
10057
10203
  try {
@@ -10064,7 +10210,7 @@ var AccountEnrollmentManager = class {
10064
10210
  if (task.state.phase === "finished") return;
10065
10211
  task.state = {
10066
10212
  ...task.state,
10067
- message: "账号已登录,名称保存失败,请重试或接受已有名称"
10213
+ messageKey: "enrollmentNameFailed"
10068
10214
  };
10069
10215
  return;
10070
10216
  }
@@ -10076,7 +10222,7 @@ var AccountEnrollmentManager = class {
10076
10222
  else task.state = {
10077
10223
  ...task.state,
10078
10224
  phase: task.cancelled ? "cancelled" : "failed",
10079
- message: task.cancelled ? "已取消" : "账号开通失败,未创建账号"
10225
+ messageKey: task.cancelled ? "enrollmentCancelled" : "enrollmentFailed"
10080
10226
  };
10081
10227
  } finally {
10082
10228
  task.flow?.dispose();
@@ -10087,7 +10233,7 @@ var AccountEnrollmentManager = class {
10087
10233
  task.state = {
10088
10234
  ...task.state,
10089
10235
  phase: "writing",
10090
- message: "正在保存凭据"
10236
+ messageKey: "enrollmentWriting"
10091
10237
  };
10092
10238
  task.writeStarted = true;
10093
10239
  await this.deps.set(task.state.ref, key);
@@ -10128,7 +10274,7 @@ var AccountEnrollmentManager = class {
10128
10274
  id,
10129
10275
  ref,
10130
10276
  phase: task.cancelled ? "cancelled" : "failed",
10131
- message: task.cancelled ? "已取消并清理" : "开通失败,已清理临时账号"
10277
+ messageKey: task.cancelled ? "enrollmentCancelledCleaned" : "enrollmentFailedCleaned"
10132
10278
  };
10133
10279
  }
10134
10280
  settings() {
@@ -10177,12 +10323,134 @@ var AccountEnrollmentManager = class {
10177
10323
  id: record.id,
10178
10324
  ref: record.ref,
10179
10325
  phase: record.phase === "naming" ? "naming" : "cleanup-needed",
10180
- message: record.phase === "naming" ? "已登录账号等待确认名称" : "账号开通收尾未完成,请重试",
10326
+ messageKey: record.phase === "naming" ? "enrollmentRecordNaming" : "enrollmentRecordCleanup",
10181
10327
  ...record.phase === "naming" ? { suggestedName: record.label } : {}
10182
10328
  };
10183
10329
  }
10184
10330
  };
10185
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
10186
10454
  //#region src/web-search.ts
10187
10455
  /**
10188
10456
  * Command Code web-search provider over `ctx.web`.
@@ -10195,6 +10463,12 @@ var AccountEnrollmentManager = class {
10195
10463
  * therefore reuses the plugin's existing credential chain and `apiBase`, so
10196
10464
  * the model-facing `web_search` tool needs no separate configuration.
10197
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
+ *
10198
10472
  * @module dsh-commandcode-provider/web-search
10199
10473
  */
10200
10474
  /** Stable id this provider registers under in `ctx.web`. */
@@ -10206,54 +10480,273 @@ const COMMANDCODE_SEARCH_PROVIDER_ID = "commandcode";
10206
10480
  * PREVIOUSLY selected backend, never this default (issue #26).
10207
10481
  */
10208
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";
10209
10493
  /** Fresh selection state: the plugin starts out not owning the selection. */
10210
10494
  function commandCodeSearchSelection() {
10211
10495
  return {
10212
10496
  owner: false,
10213
- displaced: void 0,
10214
- preexisting: false
10497
+ preexisting: false,
10498
+ foreign: void 0,
10499
+ displaced: void 0
10215
10500
  };
10216
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
+ }
10217
10522
  /**
10218
- * Reach one end of the `webSearch` toggle without trampling sibling search
10219
- * providers (issue #26).
10220
- *
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`.
10523
+ * The volatile reference backing `web.searchProvider`.
10231
10524
  *
10232
- * Never throws: like the low-level rewrite, a hardened runtime shape degrades
10233
- * 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.
10234
10529
  */
10235
- 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;
10236
10552
  try {
10237
- const field = web;
10238
- if (enable) {
10239
- if (state.owner) {
10240
- 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 });
10241
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`);
10242
10638
  }
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;
10639
+ memoryWrite(COMMANDCODE_SEARCH_PROVIDER_ID);
10248
10640
  return;
10249
10641
  }
10250
- 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);
10251
10745
  state.owner = false;
10252
- if (!state.preexisting) field.searchProviderId = state.displaced;
10253
10746
  state.preexisting = false;
10254
- return;
10747
+ if (!running) flush();
10255
10748
  }
10256
- } catch {}
10749
+ };
10257
10750
  }
10258
10751
  /** Command Code's lower/upper bound on `numResults` (from the CLI's `web_search` schema). */
10259
10752
  const MIN_NUM_RESULTS = 1;
@@ -10337,7 +10830,7 @@ var CommandCodeSearchProvider = class {
10337
10830
  });
10338
10831
  } catch (error) {
10339
10832
  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 });
10833
+ throw new WebError(`Command Code web search request failed: ${readableErrorText(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
10341
10834
  }
10342
10835
  if (!response.ok) {
10343
10836
  let message = `Command Code web search failed (HTTP ${response.status})`;
@@ -10391,7 +10884,7 @@ var CommandCodeSearchProvider = class {
10391
10884
  if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
10392
10885
  if (code === "INVALID_CREDENTIAL" || code === "RATE_LIMIT") throw new WebError(error.message, "WEB_PROVIDER_ERROR", { cause: error });
10393
10886
  }
10394
- 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 });
10395
10888
  }
10396
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");
10397
10890
  return key;
@@ -10722,7 +11215,7 @@ function applyCommandCodeTuiSettings(ctx, deps) {
10722
11215
  dispose: service.register(section)
10723
11216
  };
10724
11217
  } catch (error) {
10725
- 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)}`);
10726
11219
  }
10727
11220
  };
10728
11221
  refresh();
@@ -10805,78 +11298,6 @@ function transportBudgetMessage(failureMessage, maxRetries) {
10805
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`;
10806
11299
  }
10807
11300
  //#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
11301
  //#region src/index.ts
10881
11302
  /**
10882
11303
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -10936,6 +11357,7 @@ const Config = z.object(markVolatileFields({
10936
11357
  modelVisibility: z.dict(z.boolean()),
10937
11358
  contextWindow: z.number().min(1).max(CONTEXT_WINDOW_MAX).step(1),
10938
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),
10939
11361
  webSearch: z.boolean().default(true),
10940
11362
  showSidebarQuota: z.boolean().default(false),
10941
11363
  accounts: z.array(z.object({
@@ -10983,6 +11405,7 @@ function resolveAdapterOptions(config) {
10983
11405
  modelVisibility: readModelVisibility(config.modelVisibility),
10984
11406
  contextWindow: isValidContextWindow(config.contextWindow) ? config.contextWindow : void 0,
10985
11407
  contextWindowOverrides: readContextWindowOverrides(config.contextWindowOverrides),
11408
+ maxOutputTokens: isValidMaxOutputTokens(config.maxOutputTokens) ? config.maxOutputTokens : void 0,
10986
11409
  zdr: config.zdr === true
10987
11410
  };
10988
11411
  }
@@ -11143,7 +11566,7 @@ function apply(ctx, config) {
11143
11566
  else try {
11144
11567
  report = await adapter.getUsage(account.key, connection);
11145
11568
  } catch (error) {
11146
- report = { failures: [error instanceof Error ? error.message : String(error)] };
11569
+ report = { failures: [readableErrorText(error)] };
11147
11570
  }
11148
11571
  const state = account?.state;
11149
11572
  const usable = accountUsable(state);
@@ -11225,13 +11648,24 @@ function apply(ctx, config) {
11225
11648
  listModels: catalogForEditors,
11226
11649
  analytics: createAnalyticsReader(ctx, adapter)
11227
11650
  });
11228
- 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;
11229
11660
  const applySearchSelection = (enabled) => {
11230
- if (webRuntime !== void 0) applyCommandCodeSearchSelection(webRuntime, searchSelection, enabled);
11661
+ searchSelection?.apply(enabled);
11231
11662
  };
11232
- let webRuntime;
11233
11663
  ctx.inject(["web"], (webCtx) => {
11234
- webRuntime = webCtx.web;
11664
+ searchSelection = createCommandCodeSearchSelection({
11665
+ web: webCtx.web,
11666
+ settings: () => ctx.get("settings"),
11667
+ logger: selectionLogger
11668
+ });
11235
11669
  webCtx.web.registerSearchProvider(new CommandCodeSearchProvider({
11236
11670
  resolveKey: async (apiBase) => {
11237
11671
  const connection = options();
@@ -11243,8 +11677,8 @@ function apply(ctx, config) {
11243
11677
  }));
11244
11678
  applySearchSelection(current().webSearch ?? true);
11245
11679
  webCtx.effect(() => () => {
11246
- webRuntime = void 0;
11247
- applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
11680
+ searchSelection?.release();
11681
+ searchSelection = void 0;
11248
11682
  }, "dsh-commandcode-provider: web search selection");
11249
11683
  });
11250
11684
  let refreshTuiSettings;
@@ -11289,6 +11723,7 @@ function apply(ctx, config) {
11289
11723
  dispose();
11290
11724
  };
11291
11725
  }, "dsh-commandcode-provider: settings auto-form policy");
11726
+ applySearchSelection(current().webSearch ?? true);
11292
11727
  });
11293
11728
  const loaderEvents = ctx;
11294
11729
  ctx.effect(() => {
@@ -11302,6 +11737,6 @@ function apply(ctx, config) {
11302
11737
  }, "dsh-commandcode-provider: volatile config updates");
11303
11738
  }
11304
11739
  //#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 };
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 };
11306
11741
 
11307
11742
  //# sourceMappingURL=index.js.map