@eddyskywalker/dsh-chatgpt-subscription 0.24.0 → 0.24.2

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
@@ -4955,6 +4955,27 @@ var FileModelSettingsStore$2 = class {
4955
4955
  */
4956
4956
  /** Default loopback port the Antigravity CLI's own flow uses. */
4957
4957
  const DEFAULT_CALLBACK_PORT$2 = 51121;
4958
+ /**
4959
+ * How many consecutive ports one probe covers, starting at the default.
4960
+ *
4961
+ * An excluded range is not a block of fixed width. Windows reserves whatever
4962
+ * span Hyper-V/WinNAT/its own services hold, it moves on every reboot, and a
4963
+ * Windows 11 host was measured handing this line's default port 277 consecutive
4964
+ * excluded ports (51121-51397). A window narrower than the run the default
4965
+ * starts inside ends *inside* that run and then reports "no usable port" on a
4966
+ * machine where hundreds of free ports sit immediately behind it — so the width
4967
+ * of this window, not the width the probe assumes, is what keeps the flow
4968
+ * working.
4969
+ *
4970
+ * 1024 is roughly 3.7x the widest run measured, which is the margin that stops
4971
+ * this depending on any single host's layout, and it still ends at 52144, far
4972
+ * below the 65535 ceiling. The window is cheap because it is walked one failed
4973
+ * bind at a time: a rejected listen measured ~0.35ms here (277 probes in 96ms),
4974
+ * so even the full 1024-probe worst case — only reached when every port in the
4975
+ * window is unusable, immediately before the actionable error — costs ~0.36s
4976
+ * once per sign-in. The ordinary case stops at the first usable port.
4977
+ */
4978
+ const CALLBACK_PORT_ATTEMPTS$2 = 1024;
4958
4979
  let cachedQuota;
4959
4980
  let quotaFetchInFlight = null;
4960
4981
  /** Bumped by every cache clear so an in-flight fetch cannot publish stale state. */
@@ -5290,11 +5311,19 @@ function callbackPort() {
5290
5311
  * URI carries whatever port this returns, so any free loopback port works. A
5291
5312
  * configured `DSH_ANTIGRAVITY_CALLBACK_PORT` still wins — callers who set it
5292
5313
  * accept that the port must be usable.
5314
+ *
5315
+ * How wide such a reserved run is not something this function can look up — it
5316
+ * is the host's own bookkeeping — so the probe only has to be wider than the
5317
+ * widest run it can meet, and it walks a window sized for that (see
5318
+ * {@link CALLBACK_PORT_ATTEMPTS}) instead of guessing a block size. `start` and
5319
+ * `probe` are the seams that keep that claim testable: production probes from
5320
+ * the line's own default and judges each port by really binding it, and a test
5321
+ * can point the walk at a run whose width it controls.
5293
5322
  */
5294
- async function resolveCallbackPort$1(attempts = 128, host = resolveCallbackHost()) {
5323
+ async function resolveCallbackPort$1(attempts = CALLBACK_PORT_ATTEMPTS$2, host = resolveCallbackHost(), start = DEFAULT_CALLBACK_PORT$2, probe = (port) => isPortUsable$1(port, host)) {
5295
5324
  if (antigravityEnv("CALLBACK_PORT") !== void 0) return callbackPort();
5296
- for (let offset = 0; offset < attempts; offset += 1) if (await isPortUsable$1(51121 + offset, host)) return DEFAULT_CALLBACK_PORT$2 + offset;
5297
- throw new Error(`No usable local port for the Antigravity sign-in callback (tried ${attempts} ports from ${DEFAULT_CALLBACK_PORT$2} on ${host}). A Windows excluded-port range may cover them; set DSH_ANTIGRAVITY_CALLBACK_PORT to a free port.`);
5325
+ for (let offset = 0; offset < attempts; offset += 1) if (await probe(start + offset)) return start + offset;
5326
+ throw new Error(`No usable local port for the Antigravity sign-in callback (tried ${attempts} ports from ${start} on ${host}). A Windows excluded-port range may cover them; set DSH_ANTIGRAVITY_CALLBACK_PORT to a free port.`);
5298
5327
  }
5299
5328
  /**
5300
5329
  * Whether one port can be bound on the interface the listener will use.
@@ -5377,7 +5406,7 @@ async function getUserEmail(token, fetchFn = fetch) {
5377
5406
  }
5378
5407
  async function startCallbackServer(expectedState) {
5379
5408
  const host = resolveCallbackHost();
5380
- const port = await resolveCallbackPort$1(128, host);
5409
+ const port = await resolveCallbackPort$1(CALLBACK_PORT_ATTEMPTS$2, host);
5381
5410
  return new Promise((resolve, reject) => {
5382
5411
  let settled = false;
5383
5412
  let timeout;
@@ -13395,6 +13424,28 @@ const BILLING_SUBSCRIPTIONS_PATH = "/alpha/billing/subscriptions";
13395
13424
  const USAGE_SUMMARY_PATH = "/alpha/usage/summary";
13396
13425
  /** First loopback port tried for the browser callback; matches the official CLI. */
13397
13426
  const DEFAULT_CALLBACK_PORT$1 = 5959;
13427
+ /**
13428
+ * Consecutive ports probed when the default is taken.
13429
+ *
13430
+ * The CLI opens its own search at 5959, which {@link DEFAULT_CALLBACK_PORT}
13431
+ * keeps, so on an ordinary machine this line still lands on the port the CLI
13432
+ * would. The WIDTH is recovery budget, not compatibility: the studio is handed
13433
+ * whatever port this returns in the callback URL, so ports past the CLI's own
13434
+ * range were already reachable here before the window was widened, and starting
13435
+ * at the CLI default is what preserves the "behaves like the CLI" property if the
13436
+ * studio ever does pin an allowlist.
13437
+ *
13438
+ * The failure shape makes the width matter more than it does for a line that has
13439
+ * a manual flow to fall back to: exhausting this window is a thrown error and a
13440
+ * dead sign-in, not a downgrade. Windows (Hyper-V/WinNAT) reserves TCP port
13441
+ * ranges of no fixed width that change on every reboot — one host was measured
13442
+ * holding 277 consecutive ports — so a ten-port window is one reboot away from
13443
+ * failing outright and a wider one is not. 1024 matches the other two sign-in
13444
+ * lines here and still ends at 6983: clear of the privileged ports, clear of the
13445
+ * OS dynamic range, and walked one failed bind at a time (~0.35ms each), so the
13446
+ * worst case costs a fraction of a second once per sign-in.
13447
+ */
13448
+ const CALLBACK_PORT_ATTEMPTS$1 = 1024;
13398
13449
  const CALLBACK_PATH$1 = "/callback";
13399
13450
  /** Page the studio's browser tab lands on after a successful POST; CLI-compatible. */
13400
13451
  const CALLBACK_COMPLETE_PATH = "/callback/complete";
@@ -17169,16 +17220,19 @@ function checkPortAvailable(port) {
17169
17220
  });
17170
17221
  }
17171
17222
  /**
17172
- * First free loopback port in the CLI's probe range.
17223
+ * First free loopback port in the probe range.
17173
17224
  *
17174
17225
  * The studio receives the port in the callback URL, so any free port works;
17175
17226
  * starting at the CLI's own default keeps behavior identical on a machine where
17176
- * a server-side allowlist is ever introduced.
17227
+ * a server-side allowlist is ever introduced. `probe` is the seam the tests walk
17228
+ * a reserved run through, which is what lets the width of the window be asserted
17229
+ * without binding a thousand real sockets on a machine whose own reservations
17230
+ * move underneath the test.
17177
17231
  */
17178
- async function findAvailablePort(start = DEFAULT_CALLBACK_PORT$1, attempts = 10) {
17232
+ async function findAvailablePort(start = DEFAULT_CALLBACK_PORT$1, attempts = CALLBACK_PORT_ATTEMPTS$1, probe = checkPortAvailable) {
17179
17233
  for (let offset = 0; offset < attempts; offset += 1) {
17180
17234
  const port = start + offset;
17181
- if (await checkPortAvailable(port)) return port;
17235
+ if (await probe(port)) return port;
17182
17236
  }
17183
17237
  throw new Error(`No free local port for the Command Code sign-in callback (tried ${attempts} ports from ${start}).`);
17184
17238
  }
@@ -24868,10 +24922,14 @@ function countOmitted(current, baseline, resolved) {
24868
24922
  *
24869
24923
  * The retryable set is therefore:
24870
24924
  *
24871
- * - `SERVER` — any 5xx, including that upstream-unavailable 502;
24872
- * - `RATE_LIMIT` — a 429 that is genuine back-pressure ("too many requests",
24873
- * "the engine is currently overloaded"), which the documentation describes as
24874
- * transient;
24925
+ * - `SERVER` — any 5xx, including that upstream-unavailable 502, and the
24926
+ * overloaded 429 (`UPSTREAM_OVERLOAD_PATTERNS`). Overload shares the 429
24927
+ * status with real back-pressure but not its meaning: it describes the state
24928
+ * of the upstream engine, which every account behind this endpoint shares, so
24929
+ * it is filed as the server condition it is and the pool is left alone;
24930
+ * - `RATE_LIMIT` — a 429 that is genuine back-pressure ("too many requests"),
24931
+ * which the documentation describes as transient and which does belong to the
24932
+ * account that hit it, so rotation on it still earns its keep;
24875
24933
  * - `TRANSPORT` — a connection that produced no response at all;
24876
24934
  * - `TIMEOUT` — a stalled stream, handled by the idle watchdog.
24877
24935
  *
@@ -24917,6 +24975,35 @@ const ENTITLEMENT_PATTERNS = [
24917
24975
  /recognized as other/i,
24918
24976
  /currently plan supports only/i
24919
24977
  ];
24978
+ /**
24979
+ * 正文里说明「上游引擎过载」的措辞,与上面两组按**归属**分界,而不是按严重程度。
24980
+ *
24981
+ * - `ENTITLEMENT_PATTERNS` 说的是套餐:换号、换时间都不会变,是请求/套餐的属性;
24982
+ * - `QUOTA_EXHAUSTED_PATTERNS` 说的是这个账号自己的余额/额度:别人可能还好,
24983
+ * 但这个账号在窗口重置前一定起不来;
24984
+ * - 本组说的是**上游引擎的繁忙**:这是所有账号共用的同一台引擎,谁来问都是这句
24985
+ * 话,本地没有任何动作能改变它。
24986
+ *
24987
+ * 第三组必须单独识别,因为它和真正的限流共用同一个 429 状态码,却不共享结论。
24988
+ * 服务端用 429 表达「引擎过载」时,本线路此前把它归进 RATE_LIMIT,号池随即按
24989
+ * 限流语义给**唯一那个账号**写 15 分钟冷却(`POOL_COOLDOWN_MS`),之后同一
24990
+ * 会话的每一次重试都被 `isEligible` 直接挡在门外——重试根本没发出网络请求,
24991
+ * 3 次退避(3.2s/6.1s,纯本地耗时)打在一个登录正常、额度正常的账号上,最后以
24992
+ * 「全部 1 个 Kimi Code 账号均处于配额限制或冷却中 (429)。最短预计在 15 分钟后
24993
+ * 解除冷却。」收场。真正被限流的服务端状态不该有账号级的副作用。
24994
+ *
24995
+ * 这里只按正文判定,绝不按状态码判定:同一个 429 既可能是过载也可能是真限流,
24996
+ * 唯一能区分的证据就是服务自己写下的那句话。措辞取自本线路文档与实际响应,
24997
+ * 刻意保留 "the engine is …" 的限定,避免把任何提到 "overload" 的正文一律
24998
+ * 吞掉——将来若出现「本账号请求量过载」这类真限流措辞,它不该落进这一组。
24999
+ */
25000
+ const UPSTREAM_OVERLOAD_PATTERNS = [
25001
+ /\bengine is (currently )?overloaded\b/i,
25002
+ /\bservers? (?:is|are) (?:currently )?overloaded\b/i,
25003
+ /\bservice is (?:currently )?overloaded\b/i,
25004
+ /\bupstream model provider is temporarily unavailable\b/i,
25005
+ /\bcurrently overloaded\b/i
25006
+ ];
24920
25007
  /** Body text the service uses for a 429 that must NOT be retried. */
24921
25008
  const QUOTA_EXHAUSTED_PATTERNS$1 = [
24922
25009
  /exceeded_current_quota_error/i,
@@ -24968,12 +25055,14 @@ function classifyKimiFailure(status, bodyText) {
24968
25055
  code: CONTEXT_OVERFLOW_CODE,
24969
25056
  retryable: false,
24970
25057
  accountScoped: false,
25058
+ upstreamOverload: false,
24971
25059
  message: `${PROVIDER_NAME$5} context window exceeded: ${detail}`
24972
25060
  };
24973
25061
  if (status === 402) return {
24974
25062
  code: "SERVER",
24975
25063
  retryable: true,
24976
25064
  accountScoped: false,
25065
+ upstreamOverload: false,
24977
25066
  message: `${PROVIDER_NAME$5} could not verify the subscription tier (402). Retrying; if it persists, confirm the membership is active.${detail ? ` ${detail}` : ""}`
24978
25067
  };
24979
25068
  if (status === 401 || status === 403) {
@@ -24981,6 +25070,7 @@ function classifyKimiFailure(status, bodyText) {
24981
25070
  code: "PROVIDER_ERROR",
24982
25071
  retryable: false,
24983
25072
  accountScoped: false,
25073
+ upstreamOverload: false,
24984
25074
  message: `${PROVIDER_NAME$5} refused this request for the current plan: ${detail || "the requested model or context is not included"}. Switch to a model the plan includes, lower the context-window override, or upgrade the subscription.`
24985
25075
  };
24986
25076
  if (status === 403) {
@@ -24989,6 +25079,7 @@ function classifyKimiFailure(status, bodyText) {
24989
25079
  code: "PROVIDER_ERROR",
24990
25080
  retryable: false,
24991
25081
  accountScoped: true,
25082
+ upstreamOverload: false,
24992
25083
  message: `${PROVIDER_NAME$5} blocked the request on an account limit (403): ${detail || (limitReached ? "the account limit was reached" : "the account refused the request")}. The quota refreshes on its own schedule — check the Kimi Code card in Settings for the reset time.`
24993
25084
  };
24994
25085
  }
@@ -24996,6 +25087,7 @@ function classifyKimiFailure(status, bodyText) {
24996
25087
  code: "INVALID_CREDENTIAL",
24997
25088
  retryable: false,
24998
25089
  accountScoped: false,
25090
+ upstreamOverload: false,
24999
25091
  message: `${PROVIDER_NAME$5} rejected the stored credential (401). Sign in again from Settings > Kimi Code.${detail ? ` ${detail}` : ""}`
25000
25092
  };
25001
25093
  }
@@ -25004,31 +25096,43 @@ function classifyKimiFailure(status, bodyText) {
25004
25096
  code: "PROVIDER_ERROR",
25005
25097
  retryable: false,
25006
25098
  accountScoped: true,
25099
+ upstreamOverload: false,
25007
25100
  message: `${PROVIDER_NAME$5} reports the account quota is exhausted: ${detail || "no remaining quota"}. Top up or wait for the window to reset.`
25008
25101
  };
25102
+ if (matchesAny$1(detail, UPSTREAM_OVERLOAD_PATTERNS)) return {
25103
+ code: "SERVER",
25104
+ retryable: true,
25105
+ accountScoped: false,
25106
+ upstreamOverload: true,
25107
+ message: `${PROVIDER_NAME$5} upstream engine is overloaded (429): ${detail || "the engine is currently overloaded"}. This is a temporary upstream condition shared by every account, so no account is taken out of rotation; retrying with backoff.`
25108
+ };
25009
25109
  return {
25010
25110
  code: "RATE_LIMIT",
25011
25111
  retryable: true,
25012
25112
  accountScoped: false,
25013
- message: `${PROVIDER_NAME$5} is rate limited or overloaded (429): ${detail || "too many requests"}. Retrying with backoff.`
25113
+ upstreamOverload: false,
25114
+ message: `${PROVIDER_NAME$5} is rate limited (429): ${detail || "too many requests"}. Retrying with backoff.`
25014
25115
  };
25015
25116
  }
25016
25117
  if (status >= 500) return {
25017
25118
  code: "SERVER",
25018
25119
  retryable: true,
25019
25120
  accountScoped: false,
25121
+ upstreamOverload: matchesAny$1(detail, UPSTREAM_OVERLOAD_PATTERNS),
25020
25122
  message: `${PROVIDER_NAME$5} upstream server error (${status}): ${detail || "the model provider is temporarily unavailable"}. Retrying with backoff.`
25021
25123
  };
25022
25124
  if (status === 400) return {
25023
25125
  code: "PROVIDER_ERROR",
25024
25126
  retryable: false,
25025
25127
  accountScoped: false,
25128
+ upstreamOverload: false,
25026
25129
  message: `${PROVIDER_NAME$5} rejected the request (400): ${detail || "the request was not accepted"}`
25027
25130
  };
25028
25131
  return {
25029
25132
  code: "PROVIDER_ERROR",
25030
25133
  retryable: false,
25031
25134
  accountScoped: false,
25135
+ upstreamOverload: false,
25032
25136
  message: `${PROVIDER_NAME$5} API error (${status}): ${detail || "No response"}`
25033
25137
  };
25034
25138
  }
@@ -25061,7 +25165,13 @@ function accountLimitCooldownMs(bodyText, now = Date.now()) {
25061
25165
  if (resetsAt !== null && Number.isFinite(resetsAt) && resetsAt > now) return Math.min(Math.max(resetsAt - now, POOL_COOLDOWN_MS$3), MAX_ACCOUNT_LIMIT_COOLDOWN_MS);
25062
25166
  return BILLING_CYCLE_PATTERN.test(bodyText) ? BILLING_CYCLE_COOLDOWN_MS : ACCOUNT_LIMIT_COOLDOWN_MS;
25063
25167
  }
25064
- /** Cooldown one rate-limited account takes when the provider states no delay. */
25168
+ /**
25169
+ * Cooldown one rate-limited account takes when the provider states no delay.
25170
+ *
25171
+ * 只在**真正的**限流上使用:上游引擎过载走 SERVER,既不落这条窗口,也不消耗轮转
25172
+ * 名额。它是 15 分钟而不是服务自己说的时长,正是因为拿不准——拿不准的那次猜测
25173
+ * 落在一个只有单个账号的号池上,代价是整个号池在 15 分钟里彻底不可用。
25174
+ */
25065
25175
  const POOL_COOLDOWN_MS$3 = 15 * 6e4;
25066
25176
  /**
25067
25177
  * Fallback cooldown for an account whose usage window is spent.
@@ -25288,7 +25398,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
25288
25398
  const after = response.status === 429 ? retryAfterMs$1(response.headers) : void 0;
25289
25399
  if (pool !== null && accountId !== void 0) {
25290
25400
  const planScoped = response.status === 429 && matchesAny$1(detail, ENTITLEMENT_PATTERNS);
25291
- if (response.status === 429 && !planScoped) {
25401
+ if (response.status === 429 && !planScoped && !failure.upstreamOverload) {
25292
25402
  const cooldownMs = failure.accountScoped ? accountLimitCooldownMs(detail) : after ?? POOL_COOLDOWN_MS$3;
25293
25403
  await pool.markCooldown(accountId, cooldownMs, `${PROVIDER_NAME$5} 429`).catch(() => void 0);
25294
25404
  if (await pool.hasAnotherAvailableAccount(tried)) continue;
@@ -25384,7 +25494,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
25384
25494
  if (status === null) return reclassified;
25385
25495
  const failure = classifyKimiFailure(status, bodyText);
25386
25496
  const planScoped = status === 429 && matchesAny$1(bodyText, ENTITLEMENT_PATTERNS);
25387
- if (status === 429 && !planScoped) await this.coolInBandRateLimit(pool, accountId, failure.accountScoped, bodyText);
25497
+ if (status === 429 && !planScoped && !failure.upstreamOverload) await this.coolInBandRateLimit(pool, accountId, failure.accountScoped, bodyText);
25388
25498
  else if (failure.code === "INVALID_CREDENTIAL") await pool.markAuthFailed(accountId, failure.message).catch(() => void 0);
25389
25499
  else if (failure.accountScoped) await pool.markCooldown(accountId, accountLimitCooldownMs(bodyText), `${PROVIDER_NAME$5} in-stream 403`).catch(() => void 0);
25390
25500
  return reclassified;
@@ -37086,6 +37196,32 @@ const CLAUDE_CLI_VERSION = "2.1.293";
37086
37196
  * see {@link CALLBACK_PORT_ATTEMPTS}.
37087
37197
  */
37088
37198
  const DEFAULT_CALLBACK_PORT = 53692;
37199
+ /**
37200
+ * How many consecutive ports one probe covers, starting at the default.
37201
+ *
37202
+ * Windows (Hyper-V/WinNAT) dynamically reserves TCP port ranges that change on
37203
+ * every reboot. A fixed callback port can land inside one and `listen` fails
37204
+ * with `EACCES`, so browser authorization never completes.
37205
+ *
37206
+ * An excluded run is not a block of a width anyone can look up. One Windows 11
37207
+ * host was measured holding 277 consecutive unusable ports (51121-51397) plus
37208
+ * scattered single ones that appear in no static table, and the picture changes
37209
+ * across reboots — so a window justified as "covers any single block" is covering
37210
+ * a block size that does not exist. A window narrower than the run the default
37211
+ * starts inside ends INSIDE that run, and the consequence here is worse than an
37212
+ * error: resolveCallbackPort returns null, which beginLogin reads as "fall back
37213
+ * to manual". The user is then pushed into pasting a code with no visible cause
37214
+ * on a machine that has hundreds of free ports immediately behind the run.
37215
+ *
37216
+ * 1024 is ~3.7x the widest run measured and still ends at 54716, far below the
37217
+ * 65535 ceiling. The window is walked one failed bind at a time (~0.35ms per
37218
+ * rejected listen here), so the worst case costs a fraction of a second once per
37219
+ * sign-in and only when every port in it is unusable; the ordinary case stops at
37220
+ * the first usable port. Widening changes neither which port is tried first nor
37221
+ * the default status as the one port known to be registered for this client
37222
+ * (see {@link DEFAULT_CALLBACK_PORT}).
37223
+ */
37224
+ const CALLBACK_PORT_ATTEMPTS = 1024;
37089
37225
  /** Path of the loopback callback the browser is redirected to. */
37090
37226
  const CALLBACK_PATH = "/callback";
37091
37227
  /** Idle ceiling for one streaming response before it is declared stalled. */
@@ -41950,12 +42086,16 @@ function isPortUsable(port) {
41950
42086
  * First usable loopback port, or null when the probe finds none.
41951
42087
  *
41952
42088
  * null rather than a throw is deliberate (see beginLogin): a failed probe is
41953
- * NOT a hard error, it is the signal to fall back to the manual flow. Windows
41954
- * (Hyper-V/WinNAT) reserves TCP port ranges that move on every reboot, and
41955
- * CALLBACK_PORT_ATTEMPTS covers any single 100-port excluded block the default
41956
- * port can start inside.
41957
- */
41958
- async function resolveCallbackPort(probe = isPortUsable, attempts = 128) {
42089
+ * NOT a hard error, it is the signal to fall back to the manual flow. That is
42090
+ * exactly why the window has to be wide rather than merely generous: a run of
42091
+ * excluded ports wider than CALLBACK_PORT_ATTEMPTS ends this walk early and drops
42092
+ * the user into a paste-a-code flow for a reason nothing on screen explains,
42093
+ * while the ports immediately behind that run sit free. Windows
42094
+ * (Hyper-V/WinNAT) reserves ranges of no fixed width that move on every reboot,
42095
+ * so the width of this window, not any assumed block size, is what keeps the
42096
+ * sign-in in loopback.
42097
+ */
42098
+ async function resolveCallbackPort(probe = isPortUsable, attempts = CALLBACK_PORT_ATTEMPTS) {
41959
42099
  for (let offset = 0; offset < attempts; offset += 1) {
41960
42100
  const port = DEFAULT_CALLBACK_PORT + offset;
41961
42101
  if (port > 65535) return null;
@@ -42355,7 +42495,7 @@ async function beginLogin(store, options = {}) {
42355
42495
  port = await resolveCallbackPort(probe);
42356
42496
  if (port === null) {
42357
42497
  resolvedMode = "manual";
42358
- fallbackReason = "No free loopback port in the 128-port range starting at 53692.";
42498
+ fallbackReason = "No free loopback port in the 1024-port range starting at 53692.";
42359
42499
  } else try {
42360
42500
  server = await listen(port, LOOPBACK_HOST);
42361
42501
  } catch (error) {
@@ -57540,6 +57680,129 @@ function delegationModeOf(policies, toolName) {
57540
57680
  for (const policy of policies) if (policy.name === toolName) return policy.mode;
57541
57681
  }
57542
57682
  /**
57683
+ * Resolve the effective allowlist that governs delegations from this agent:
57684
+ * empty when the Host preference explicitly disables enforcement, otherwise
57685
+ * the Session's (or nearest subagent ancestor's) recorded policy, falling
57686
+ * back to the live preference only under `preference` scope.
57687
+ * @param agent - Calling agent.
57688
+ * @param preference - Current Host preference, when the settings service exists.
57689
+ * @param sessions - Session registry used for ancestor lookup.
57690
+ * @param scope - Whether an unrecorded Session falls back to the preference.
57691
+ * @returns the authorized routes in force for this agent, or an empty list when unrestricted.
57692
+ */
57693
+ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session") {
57694
+ if (preference !== void 0 && !preference.enabled) return [];
57695
+ return authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
57696
+ }
57697
+ /**
57698
+ * Append the Session's authorized `{ provider, model }` pairs to a successful
57699
+ * `list_subagent_models` result when the calling Session is governed by an
57700
+ * allowlist.
57701
+ *
57702
+ * Calling `list_subagent_models({})` upstream returns only provider names
57703
+ * (`minimax-code — MiniMax Code`), and calling it with `provider` prints
57704
+ * `<provider>/<model>` without stating that the output is already filtered to
57705
+ * the Session allowlist. Appending the exact split `{ provider, model }` pairs
57706
+ * here lets even a single `list_subagent_models({})` call hand the model every
57707
+ * authorized route in the form `subagent` expects, eliminating any reason to
57708
+ * search local config files or source code for the allowlist.
57709
+ * @param output - Original text returned by `list_subagent_models`.
57710
+ * @param allowed - Routes the calling Session authorizes.
57711
+ * @returns the output with the Session allowlist summary appended.
57712
+ */
57713
+ function enrichListSubagentModelsOutput(output, allowed) {
57714
+ if (allowed.length === 0) return output;
57715
+ const header = "Authorized child routes for this Session (already filtered by the Session allowlist; only these routes are accepted, and `provider` and `model` must be passed as separate fields):";
57716
+ if (output.includes(header)) return output;
57717
+ const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
57718
+ return output.trim().length === 0 ? suffix : `${output}\n\n${suffix}`;
57719
+ }
57720
+ /**
57721
+ * Rewrite the upstream `list_subagent_models` and `subagent` tool/parameter
57722
+ * descriptions when the calling Session enforces an allowlist.
57723
+ *
57724
+ * Upstream `dsh-tool-subagent` ships static descriptions stating that
57725
+ * `"Catalog membership is advisory: an adapter may accept an unlisted model id."`
57726
+ * and `"Child LLM selection is optional. Omit provider, model, and reasoning_effort..."`.
57727
+ * In an allowlist-governed Session both statements are false and actively
57728
+ * mislead the dispatcher into either omitting `provider`/`model` or distrusting
57729
+ * `list_subagent_models` and hunting through local files for the "real" allowlist.
57730
+ * @param text - Tool description, parameter description, or rendered `tools:sdk` section text.
57731
+ * @param allowed - Routes the calling Session authorizes.
57732
+ * @returns the text with the misleading upstream sentences replaced by the Session's exact policy.
57733
+ */
57734
+ function rewriteAuthorizedToolDescriptions(text, allowed) {
57735
+ if (allowed.length === 0 || text.length === 0) return text;
57736
+ const routes = authorizedRoutesText(allowed);
57737
+ return text.replaceAll("Catalog membership is advisory: an adapter may accept an unlisted model id.", `Output is already filtered to this Session's allowlist (${routes}); only these authorized provider/model routes are accepted.`).replace(/Child LLM selection is optional\.\s*Omit `?provider`?, `?model`?, and `?reasoning_effort`? to use configured child defaults and (?:inherit compatible missing values from the parent Agent|this provider's route defaults)\./g, `Child LLM selection is required in this Session: pass an authorized \`provider\` and \`model\` from \`list_subagent_models\` (${routes}) as separate fields; omitting either field is rejected.`).replace(/Supply together with model; omit both to use configured child defaults or (?:inherit the parent route|this provider's route defaults)\.|Omit to use the configured child default or inherit from the parent Agent\./g, `Required in this Session: pass an authorized provider id from list_subagent_models (${routes}).`).replace(/Supply together with provider; omit both to use configured child defaults or (?:inherit the parent route|this provider's route defaults)\.|Omit to use the configured child default or inherit when the child provider matches the parent provider\./g, "Required in this Session: pass the authorized model id (without the provider/ prefix) for the selected provider.");
57738
+ }
57739
+ /**
57740
+ * Rewrite one assembled prompt's `tools:sdk` section and native tool schemas
57741
+ * when the target Agent's Session enforces a subagent model allowlist.
57742
+ * @param assembly - Assembled prompt from `system-prompt/assemble`.
57743
+ * @param allowed - Routes the calling Session authorizes.
57744
+ * @param toolNames - Explicit-route delegation tool names.
57745
+ * @returns the updated assembly, or the original object when nothing changed.
57746
+ */
57747
+ function rewriteAuthorizedPromptAssembly(assembly, allowed, toolNames = DEFAULT_DELEGATION_TOOLS) {
57748
+ if (allowed.length === 0) return assembly;
57749
+ let sectionsChanged = false;
57750
+ const sections = Array.isArray(assembly.sections) ? assembly.sections.map((section) => {
57751
+ if (section?.name !== "tools:sdk" || typeof section.text !== "string") return section;
57752
+ const rewritten = rewriteAuthorizedToolDescriptions(section.text, allowed);
57753
+ if (rewritten === section.text) return section;
57754
+ sectionsChanged = true;
57755
+ return {
57756
+ ...section,
57757
+ text: rewritten
57758
+ };
57759
+ }) : assembly.sections;
57760
+ let toolsChanged = false;
57761
+ const tools = Array.isArray(assembly.tools) ? assembly.tools.map((tool) => {
57762
+ const name = asString$1(tool?.name);
57763
+ if (name === void 0 || name !== "list_subagent_models" && !toolNames.includes(name)) return tool;
57764
+ const description = typeof tool.description === "string" ? rewriteAuthorizedToolDescriptions(tool.description, allowed) : tool.description;
57765
+ let params = tool.parameters;
57766
+ const paramsRecord = asRecord$1(params);
57767
+ const propsRecord = asRecord$1(paramsRecord?.["properties"]);
57768
+ if (paramsRecord !== void 0 && propsRecord !== void 0) {
57769
+ let propsMutated = false;
57770
+ const nextProps = { ...propsRecord };
57771
+ for (const field of ["provider", "model"]) {
57772
+ const prop = asRecord$1(propsRecord[field]);
57773
+ const propDesc = asString$1(prop?.["description"]);
57774
+ if (prop !== void 0 && propDesc !== void 0) {
57775
+ const nextDesc = rewriteAuthorizedToolDescriptions(propDesc, allowed);
57776
+ if (nextDesc !== propDesc) {
57777
+ nextProps[field] = {
57778
+ ...prop,
57779
+ description: nextDesc
57780
+ };
57781
+ propsMutated = true;
57782
+ }
57783
+ }
57784
+ }
57785
+ if (propsMutated) params = {
57786
+ ...paramsRecord,
57787
+ properties: nextProps
57788
+ };
57789
+ }
57790
+ if (description === tool.description && params === tool.parameters) return tool;
57791
+ toolsChanged = true;
57792
+ return {
57793
+ ...tool,
57794
+ ...description !== tool.description ? { description } : {},
57795
+ ...params !== tool.parameters ? { parameters: params } : {}
57796
+ };
57797
+ }) : assembly.tools;
57798
+ if (!sectionsChanged && !toolsChanged) return assembly;
57799
+ return {
57800
+ ...assembly,
57801
+ ...sectionsChanged ? { sections } : {},
57802
+ ...toolsChanged ? { tools } : {}
57803
+ };
57804
+ }
57805
+ /**
57543
57806
  * Decide whether one delegation call may start its child.
57544
57807
  * @param agent - Calling agent.
57545
57808
  * @param toolName - Tool being dispatched.
@@ -57560,8 +57823,7 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
57560
57823
  mode: "inherit"
57561
57824
  }))], toolName);
57562
57825
  if (mode === void 0) return void 0;
57563
- if (preference !== void 0 && !preference.enabled) return void 0;
57564
- const allowed = authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
57826
+ const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope);
57565
57827
  if (allowed.length === 0) return void 0;
57566
57828
  const request = asRecord$1(args) ?? {};
57567
57829
  const requestedProvider = asString$1(request["provider"]);
@@ -57628,11 +57890,14 @@ function validateDelegationToolNames(toolNames) {
57628
57890
  return [...toolNames];
57629
57891
  }
57630
57892
  /**
57631
- * Register the monotonic guard on the Host tool registry.
57632
- * @param ctx - Host context carrying `tools` (and optionally `settings`).
57893
+ * Register the monotonic guard on the Host tool registry, and — when the Host
57894
+ * context exposes Cordis events — enrich `list_subagent_models` output with the
57895
+ * Session's split `{ provider, model }` routes and rewrite the misleading
57896
+ * upstream optional/advisory wording in assembled prompts.
57897
+ * @param ctx - Host context carrying `tools` (and optionally `settings` / `on`).
57633
57898
  * @param sessions - Session registry used for ancestor lookup.
57634
57899
  * @param config - Enforcement toggle and delegation tool names.
57635
- * @returns the exact disposer that unregisters the guard.
57900
+ * @returns the exact disposer that unregisters the guard and listeners.
57636
57901
  */
57637
57902
  function installSubagentModelAuthorization(ctx, sessions, config = {}) {
57638
57903
  const toolNames = validateDelegationToolNames(config.toolNames ?? [...DEFAULT_DELEGATION_TOOLS]);
@@ -57647,7 +57912,47 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
57647
57912
  inheritToolNames,
57648
57913
  scope
57649
57914
  });
57650
- return ctx.tools.guard((exec) => authorize(exec.agent, exec.name, exec.arguments));
57915
+ const disposeGuard = ctx.tools.guard((exec) => authorize(exec.agent, exec.name, exec.arguments));
57916
+ const eventHost = ctx;
57917
+ const disposePostExecute = eventHost.on?.("tools/post-execute", async (exec, result, next) => {
57918
+ const decision = await next();
57919
+ if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
57920
+ const allowed = effectiveAllowedRoutes(exec.agent, subagentModelSelectionPreference(settings), sessions, scope);
57921
+ if (allowed.length === 0) return decision;
57922
+ const currentValue = decision.value !== void 0 ? decision.value : result.value;
57923
+ if (typeof currentValue === "string") {
57924
+ const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
57925
+ if (enriched === currentValue) return decision;
57926
+ return {
57927
+ kind: "accept",
57928
+ value: enriched,
57929
+ ...decision.additionalContexts !== void 0 ? { additionalContexts: decision.additionalContexts } : {}
57930
+ };
57931
+ }
57932
+ const currentContent = decision.content ?? result.content;
57933
+ const firstBlock = Array.isArray(currentContent) ? currentContent[0] : void 0;
57934
+ if (firstBlock?.type === "text" && typeof firstBlock.text === "string") {
57935
+ const enriched = enrichListSubagentModelsOutput(firstBlock.text, allowed);
57936
+ if (enriched === firstBlock.text) return decision;
57937
+ return {
57938
+ kind: "accept",
57939
+ content: [{
57940
+ type: "text",
57941
+ text: enriched
57942
+ }, ...currentContent.slice(1)],
57943
+ ...decision.additionalContexts !== void 0 ? { additionalContexts: decision.additionalContexts } : {}
57944
+ };
57945
+ }
57946
+ return decision;
57947
+ });
57948
+ const disposeAssemble = eventHost.on?.("system-prompt/assemble", async (_assembly, context, next) => {
57949
+ return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope, subagentModelSelectionPreference(settings), sessions, scope), toolNames);
57950
+ });
57951
+ return () => {
57952
+ disposeGuard();
57953
+ disposePostExecute?.();
57954
+ disposeAssemble?.();
57955
+ };
57651
57956
  }
57652
57957
  /**
57653
57958
  * Validate the configured authorization scope.
@@ -58170,125 +58475,126 @@ const DISPATCH_PRESET = {
58170
58475
  name: "@deepseek-ai/dsh-persona",
58171
58476
  config: {
58172
58477
  suffix: "Your working directory is {{cwd}}.",
58173
- prefix: `你由 {{model}} 驱动,工作目录是 {{cwd}}。
58478
+ prefix: `You are powered by {{model}}. Your working directory is {{cwd}}.
58174
58479
 
58175
- 【角色识别】若你的第一条用户消息是一份任务书(而不是用户本人的直接请求),说明你是子代理:R0、R0.5、R-G、R1、R2、R4、R5、R-W 中面向调度者的规则不适用于你。你是执行者,只在任务边界内自行规划、执行、自检,并把证据回报给派发者;R3 仍然有效。
58480
+ [Role Identification] If your first user message is a task brief (rather than a direct request from the human user), you are a subagent: the coordinator-facing rules in R0, R0.5, R-G, R1, R2, R4, R5, and R-W do not apply to you. You are the executor—plan, execute, and self-verify strictly within your task boundary, and report concrete evidence back to the dispatcher; R3 remains in effect.
58176
58481
 
58177
- 【调度者身份】你是本会话的调度 Agent。你的产出是计划、任务书、审查结论与交付说明——不是替别人写完的代码。
58482
+ [Coordinator Identity] You are the Dispatch Agent for this session. Your deliverables are plans, task briefs, review verdicts, and delivery summaries—not code written on someone else's behalf.
58483
+ [Execution Pipeline] R0.5 Compatibility Gate (once per new session) -> R0 Complexity Triage -> (L2) R-G Clarification Interview -> Planning -> R2 Model Routing -> R5 Task Brief Dispatch -> R-W Turn Boundary Wait -> R6 Review & Acceptance.
58178
58484
 
58179
- ## R0 · 复杂度分诊(先分诊,再决定流程)
58180
- 收到任务后先判定档位,用一行写出「档位 + 理由」,再决定后续阶段。
58181
- - L0 简单/单点:单文件或单点改动;需求无歧义;改动可逆;无并行价值;所需信息你已全部掌握。→ 不派子代理、不做访谈,直接做完并汇报。
58182
- - L1 中等:2–5 个文件;需求基本明确但有个别待定点;可拆但并行收益有限。→ 最多 1–2 个关键问题(不展开访谈)→ 简短规划 → 你执行或派 1 个子代理。
58183
- - L2 复杂/多步:跨模块或多交付物;存在歧义或隐含假设;需要探索未知;有可独立并行的子任务。→ 必须走完整流程:澄清访谈 → 完整规划 → 多子代理并行 → 分层验收。
58184
- 拿不准就先问清楚再升级;发现比预想复杂时,停下重新分诊并告知用户。绝不为 L0/L1 任务开启访谈或多代理编排。
58485
+ ## R0.5 · Compatibility Gate (Once per New Session, Before R0)
58185
58486
 
58186
- ## R0.5 · 兼容性闸门(每个新会话一次,早于 R0)
58487
+ Dispatch Mode is **incompatible** with Agent Teams and deliberately provides no fallback path. Agent Teams replaces send_message, list_agents, and interrupt_agent with its own team-scoped registrations: subagents can still be spawned, but they do not appear in list_agents and cannot be messaged or followed up—whereas review, rework, and follow-up in this mode rely entirely on those three tools. Switching to teammate vocabulary only turns an obvious failure ("cannot reach children") into a silent one.
58187
58488
 
58188
- 调度模式与 Agent Teams **不兼容**,并且刻意不做降级。team 用自己的 scoped 注册顶替 send_message / list_agents / interrupt_agent:子代理照样派得出去,却既不在 list_agents 的名册里、也没法被追问——而本模式的验收、返工、追问全建在这三个工具上。换一套词汇接着干,只会把「回访不了」从明摆着的问题变成看不见的问题。
58489
+ Criterion: whether spawn_teammate and wait_agent exist (in PTC mode: tools.spawn_teammate / tools.wait_agent).
58189
58490
 
58190
- 判据:spawn_teammate 与 wait_agent 是否存在(PTC 下即 tools.spawn_teammate / tools.wait_agent)。
58491
+ - Neither exists -> This session is orchestratable; write one line "Orchestration ready: subagent" and proceed to R0.
58492
+ - Either exists -> This session is **not orchestratable**. Do these three things in order, and nothing else:
58493
+ 1. State the incompatibility in one sentence: Agent Teams' same-named tools shadow the subagent coordination tools, and this mode has no degraded path.
58494
+ 2. Call ask_user_question once to hand the choice of exit path to the user (see R-W): (1) Disable the Agent Teams bundle, restart the Host, and start a new session; (2) Switch to another preset and do not orchestrate in this session; (3) Execute this task directly without entering the orchestration workflow.
58495
+ 3. **Do nothing else** until the user replies: do not triage, plan, dispatch, touch files, or propose workarounds.
58496
+ - After changing settings, the user **must start a new session** for it to take effect—this session's tool surface was fixed at startup. Do not let the user assume changing config will fix the current session in place.
58497
+ - If the user picks "Execute this task directly": work as a standalone agent and do not spawn subagents on the side. This choice applies only to the current task; the next session runs the gate again.
58191
58498
 
58192
- - 两者都不存在 → 本会话可编排,写一行「编排可用:subagent」再进 R0。
58193
- - 任一存在 → 本会话**不可编排**。此时按顺序做、且只做这三件:
58194
- 1. 一句话说明不兼容:team 的同名工具顶掉了 subagent 的协调工具,本模式没有降级路径。
58195
- 2. 用一次 ask_user_question 把出路交给用户选(见 R-W):① 关掉 Agent Teams bundle、重启 Host 后新建会话;② 换用别的 preset,本会话不编排;③ 本次直接做,不进编排流程。
58196
- 3. 在用户答复之前**什么都不做**:不分诊、不规划、不派发、不动文件、不给替代方案。
58197
- - 用户处理完之后必须**新建会话**才生效——本会话的工具面在启动时就定死了。别让用户以为改完配置就能接着用。
58198
- - 选了「本次直接做」:按普通 Agent 单干,别顺手派子代理。这个选择只对本次任务有效,下一个会话重新过闸门。
58499
+ ## R0 · Complexity Triage (Triage First, Then Choose the Workflow)
58500
+ Upon receiving a task, determine its tier first, write one line stating "Tier + Rationale", and then decide which phases to run.
58501
+ - L0 Simple / Single-point: single-file or single-point edit; unambiguous requirements; reversible change; no parallelism value; you already hold all required context. -> Do not spawn subagents or run an interview; execute directly and report.
58502
+ - L1 Moderate: 2–5 files; requirements mostly clear with 1–2 open points; decomposable but with limited parallelism gain. -> Ask at most 1–2 key questions (no full interview) -> brief plan -> execute yourself or dispatch 1 subagent.
58503
+ - L2 Complex / Multi-step: cross-module or multi-deliverable; ambiguity or hidden assumptions; requires exploring unknowns; independent subtasks suitable for parallel execution. -> Must run the full workflow: Clarification Interview -> Full Plan -> Parallel Subagents -> Layered Acceptance.
58504
+ When in doubt, clarify before escalating; if a task turns out more complex than expected, stop, re-triage, and inform the user. Never open an interview or multi-agent orchestration for L0/L1 tasks.
58199
58505
 
58200
- ## R-G · 澄清访谈(仅 L2,且必须在规划之前)
58201
- - G1 设计树:把需求建模成一棵设计树——每个决策下挂着依赖它的决策,不要平铺罗列。
58202
- - G2 前沿与轮次:前沿=所有前置决策已敲定、现在问不需要猜的问题。一轮=一次问完整个前沿;不要一次只问一个,也不要一次问完所有。相互依赖的两个问题不得同轮。
58203
- - G3 执行方式:用一次 ask_user_question 调用提交本轮全部问题;每题带稳定 id 与简短 header,并给出你的推荐答案(有选项时推荐项放第一个并标注「(推荐)」)。按「Q1 — 标题 / 问题正文 / 推荐答案」组织,让用户能按编号回答。同一条要求覆盖访谈之外的每一个停点,见 R-W。
58204
- - G4 事实归你,决策归用户:能靠读代码、搜索、运行命令查明的事实绝不问用户;需要查证时派子代理去查(同样遵守 R2),且不要阻塞提问——只有依赖该事实的问题留到下一轮,同轮其余问题照问。
58205
- - G5 收敛:前沿为空即访谈结束;用户明确确认「理解一致」之后才进入规划。在此之前不得编辑文件、不得派发实现型子代理。
58206
- - G6 禁止:自问自答、替用户拍板、跳过访谈直接规划、把用户随口确认某件事当成访谈完成。
58506
+ ## R-G · Clarification Interview (L2 Only, Always Before Planning)
58507
+ - G1 Design Tree: Model requirements as a dependency tree of decisions—every decision branches into the decisions that hang off it; never list flat questions.
58508
+ - G2 Frontier & Rounds: The frontier is every decision whose prerequisites are already settled—the questions you can ask now without guessing at answers you have not heard. Work the tree in rounds: ask the entire current frontier in one round, wait for the user's answers, recompute the frontier from them, and ask the next round. Never ask one question per round, never ask every question at once, and never put two questions in one round when one depends on the other.
58509
+ - G3 Execution via Tool: Submit all questions for the current round in a single ask_user_question call; give each question a stable id, a concise header, and your recommended answer (when providing options, place the recommended option first and mark it "(Recommended)"). Structure items as "Q1 — Title / Question body / Recommended answer" so the user can reply by number. The same rule covers every decision stop outside the interview; see R-W.
58510
+ - G4 Facts Are Yours, Decisions Are the User's: Never ask the user for repository facts that you can verify by reading code, searching files, or running commands (such as where code lives or how current behavior works); inspect read-only yourself or dispatch a read-only subagent (observing R2) without blocking unrelated questions—only the questions downstream of that exploration wait for the next round; ask the rest of the frontier now. (Note: this session's authorized subagent model routes are provided directly by list_subagent_models rather than repository files; see R2.)
58511
+ - G5 Convergence: The interview ends only when the frontier is empty (every branch visited, nothing left silently assumed) and the user has explicitly confirmed shared understanding. Do not present a plan, edit files, or dispatch implementation subagents before that.
58512
+ - G6 Prohibited: Answering your own questions, making product/design decisions on the user's behalf, skipping the interview to plan directly, or treating a casual remark about one detail as full interview sign-off.
58207
58513
 
58208
- ## R1 · 职责边界(按档位分层)
58209
- 对 L2 任务,除下列六类工作外一律不亲自执行:① 规划与拆解;② 派发任务;③ 审查;④ 验收;⑤ 文档与交付说明;⑥ 子代理无法处理的任务。
58210
- 对 L0/L1,你直接执行——不要为了显得在编排而派子代理。
58514
+ ## R1 · Responsibility Boundary (Layered by Tier)
58515
+ For L2 tasks, never execute work yourself outside these six categories: (1) Planning & decomposition; (2) Dispatching tasks; (3) Review; (4) Acceptance verification; (5) Documentation & delivery summary; (6) Tasks subagents cannot perform.
58516
+ For L0/L1 tasks, execute directly—do not spawn subagents just to look like you are orchestrating.
58211
58517
 
58212
- ## R2 · 模型路由(先判定可用性,再决定怎么派)
58213
- 开工前先判定本会话是否具备子代理模型选择:看 list_subagent_models 是否存在(PTC 模式下即 tools.list_subagent_models 是否可用)。它与 subagent 工具上 provider/model/reasoning_effort 三个参数的暴露由同一个开关决定,必然同生共死,因此这一个信号足够判定。
58518
+ ## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
58519
+ Before dispatching, check whether subagent model selection is enabled in this session: check whether list_subagent_models exists (in PTC mode: whether tools.list_subagent_models is available). Its presence and the exposure of provider / model / reasoning_effort on subagent are controlled by the same setting and always appear or disappear together, so this single signal is sufficient.
58214
58520
 
58215
- 【可用时】按显式路由派发,这是本模式的默认工作方式。
58216
- - 每次派发显式给出 provider 与 model(必要时附 reasoning_effort),且必须落在 DSH「子代理」设置已勾选的允许清单内。
58217
- - 本部署的守卫(dsh-chatgpt-subscription 的 subagentModelAuthorization,默认启用)会在子代理启动前拒绝:省略 provider/model、只给一半、或给出清单外的路由,拒绝理由会直接列出授权路由。PTC 模式下守卫同样覆盖 run_code 内 tools.subagent(...) 的调用。
58218
- - 所以省略参数不是「静默继承」,而是一次硬拒绝——不要靠试错去发现可用路由。守卫只保证「不会用错」,不保证「选得合适」,选哪个仍然由你决定。
58219
- - 每次派发前先写出路由决定:route: <provider>/<model>(必要时 + reasoning_effort)。没有写出这一行,就不得发起派发;写错了会被守卫拦下。
58220
- - subagent 工具自带的说明把模型选择称作「可选」并鼓励省略,那是 DSH 核心的措辞;开关开启时它不成立。
58521
+ [When Available] Dispatch with explicit routing—this is the default operating mode.
58522
+ - The sole source of truth for the allowlist is list_subagent_models: before your first dispatch, call list_subagent_models({}) to list authorized providers (its output also appends the full breakdown of every authorized provider/model pair for this session); if you need per-model capabilities or reasoning effort levels under a provider, call list_subagent_models({ provider }) to list authorized models under that provider (or pass { provider, model }). DSH already filters this tool's output at runtime against the session's Subagents settings—what it returns IS the complete authorized allowlist, and the route guard validates against the exact same session snapshot, so trust its output directly without cross-checking local config files or workspace code.
58523
+ - list_subagent_models({ provider }) formats entries as <provider>/<model> — <Name>; on every dispatch, pass the split provider and model fields explicitly (model without the <provider>/ prefix, plus reasoning_effort when needed), and the route must be in the allowlist returned by the tool.
58524
+ - Before every dispatch, write out the routing decision line: route: <provider>/<model> (+ reasoning_effort when applicable). Do not dispatch without writing this line first. The runtime route guard (enabled by default, covering tools.subagent(...) inside run_code in PTC mode as well) hard-rejects omitting provider/model, passing only one of them, or picking a route outside the allowlist before the child starts—never omit parameters or use trial-and-error to discover routes.
58525
+ - If a tool's generic upstream DSH description calls model selection "optional" or states "Catalog membership is advisory", this rule takes precedence: the runtime output is already filtered to this session's allowlist and needs no cross-checking elsewhere.
58221
58526
 
58222
- 【不可用时】不报错、不劝阻、不要求用户改设置——直接沿用 DSH 默认行为继续工作。
58223
- - 此时 subagent 工具不暴露 provider/model/reasoning_effort,守卫也不生效,子代理静默继承你的路由。这是本模式认可的合法降级路径,不是违规。
58224
- - 照常按 R3–R6 派发与验收,只是不再写出 route 这一行。
58225
- - 唯一需要顺带一提的例外:如果你判断某个任务明显更适合用另一个模型(例如机械重构 vs 深度推理),可以用一句话告诉用户可以到「设置 → 子代理」打开开关并新开一个会话,然后继续用默认方式把当前任务做完——不要为此停下等待。该设置只对新建会话生效,不追溯已有会话。
58527
+ [When Unavailable] Do not error, complain, or ask the user to change settings—continue with DSH's default behavior.
58528
+ - Here subagent does not expose provider/model/reasoning_effort, the route guard is inactive, and subagents silently inherit your route. This is a legitimate degraded path in this mode, not a violation.
58529
+ - Dispatch and verify per R3–R6 as usual, simply omitting the route: line.
58530
+ - Only exception worth a brief note: if a task is clearly better suited for a different model (e.g., mechanical refactoring vs. deep reasoning), you may mention in one sentence that the user can enable subagent model selection under "Settings -> Subagents" and start a new session, while continuing to complete the current task with default routing—do not stop and wait. That setting only affects newly created sessions.
58226
58531
 
58227
- subagent_fork 刻意与父代理同路由(复用对话与 KV Cache),只用于需要主代理上下文的审查,不得当作偷懒的替身。
58228
- 它没有 provider/model 参数,路由由你自己决定,因此【可用时】的显式路由规则对它不适用。但守卫会校验它实际继承到的路由:当你的当前模型不在允许清单内时,fork 会被硬拒绝,且它自己无法补救——此时要么改用 subagent 显式指定清单内的路由,要么先让本会话切到清单内的模型,fork 随即恢复可用。
58229
- 选择依据是任务性质与模型能力;同一批次内同类任务保持一致。
58532
+ subagent_fork deliberately shares the parent agent's route (reusing conversation history and KV cache); use it only for reviews that require the coordinator's context, never as a shortcut around explicit routing.
58533
+ It has no provider/model parameters, so the explicit-route rule under [When Available] does not apply to it; however, the guard still validates the route it inherits: if your current model is not in the allowlist, subagent_fork will be hard-rejected—switch to subagent with an explicit allowed route, or switch this session to an allowed model so subagent_fork becomes available again.
58534
+ Choose models based on task nature and model capability; keep model selection consistent across tasks of the same kind within a batch.
58230
58535
 
58231
- ## R3 · 子代理自治
58232
- 子代理对自己的模块拥有规划、执行、自检的完整权限。你不替它做它的活,派发后也不要去写同一块代码——给足任务书,然后等它的证据。
58536
+ ## R3 · Subagent Autonomy
58537
+ A subagent has full authority to plan, execute, and self-verify within its assigned module. Do not do its job for it, and do not edit the same code while it is working—give it a complete task brief and wait for its evidence.
58233
58538
 
58234
- ## R4 · 子代理角色
58235
- 子代理使用 DSH 的默认子代理角色与默认环境,不注入任何自定义角色。选模型的权力只在调度 Agent:允许清单中有多个模型时由你按任务性质分派,不交给子代理自己挑。
58539
+ ## R4 · Subagent Role
58540
+ Subagents run with DSH's default subagent role and environment; do not inject custom personas. Model selection authority belongs solely to the Dispatch Agent: when multiple models are in the allowlist, you assign the model per task nature—never let a subagent choose its own model.
58236
58541
 
58237
- ## R5 · 派发纪律
58238
- 每份任务书包含:目标 / 边界(可动哪些文件或模块)/ 约束 / 验收标准 / 必须回报的证据。
58239
- 相互独立的派发放在同一条消息里并行发出,不要串行等待。子代理的回报是证据,不是结论。
58240
- 前置条件:访谈未收敛时不得派发实现型子代理。
58241
- 若当前呈现为 PTC 模式,派发通过 run_code 内的 tools.subagent(...) 完成,独立派发用 Promise.all 并发;参数与调度工具一致。
58542
+ ## R5 · Dispatch Discipline
58543
+ Every task brief must contain five elements: Goal / Boundary (which files or modules may be touched) / Constraints / Acceptance Criteria / Required Evidence to Report.
58544
+ Issue mutually independent dispatches in parallel within the same message rather than waiting serially. A subagent's report is evidence, not a final verdict.
58545
+ Precondition: never dispatch implementation subagents before the clarification interview has converged.
58546
+ In PTC mode, dispatch via tools.subagent(...) inside run_code, running independent dispatches concurrently with Promise.all / asyncio.gather; parameters match the native tool.
58242
58547
 
58243
- ## R6 · 审查与验收
58244
- 两层:子代理自检 → 你复审。验收必须落到证据上(读关键改动、跑测试、对照验收标准)。
58245
- 不通过就带上具体证据重新派发,不要自己接手改;也不要凭子代理的自我陈述宣布完成。
58246
- 同一任务书最多重新派发两次;两次都不通过,或重新派发被硬拒绝(工具不可用、连续报错),才转 R8 兜底并说明是哪一种。
58548
+ ## R6 · Review & Acceptance
58549
+ Two layers: subagent self-check -> your independent review. Acceptance must be grounded in concrete evidence (reading key diffs, running tests, checking each acceptance criterion).
58550
+ If work fails review, re-dispatch with specific evidence—do not step in to fix the code yourself, and never declare completion based solely on a subagent's self-report.
58551
+ Re-dispatch the same task brief at most twice; only if it still fails after two re-dispatches, or if re-dispatch is hard-rejected (tool unavailable or repeated errors), transition to R8 Fallback and state which condition triggered it.
58247
58552
 
58248
- ## R-W · 回合边界(等待 / 目标 / 提问)
58553
+ ## R-W · Turn Boundary (Waiting / Goals / Questions)
58249
58554
 
58250
- 三件事都落在回合的边界上:派发之后、继续之前、停下之前。
58555
+ All three rules govern the boundary of a turn: after dispatching, before continuing, and before stopping.
58251
58556
 
58252
- 【等待子代理】默认结束本轮回复、一个工具都不调,等子代理完成的通知把你叫醒——通知是自带的,不要自己造一个等待。
58253
- - 禁止 sleep、轮询循环、watch,以及任何只为「挂着」而存在的长命令:它们烧上下文和预算,换回来的信息量是零。
58254
- - 手上有独立的活就先做(R1 的六类工作、复审别的子代理的产出、读你需要的证据),做完了再结束回合。
58255
- - 确实被阻塞、非要一次结果不可时,才用 job_output({ wait: true }) 有界地等一次;那是收集工具,不是计时器。
58256
- - 停下之前把状态说清楚:派出去了谁、在等什么、拿到证据要做什么。不要让用户从一段沉默里猜进度。
58557
+ [Waiting for Subagents] By default, end your turn without calling any more tools and let the subagent completion notification wake you—notifications are built-in; do not manufacture your own wait.
58558
+ - Never run sleep, polling loops, watch, or any long-running placeholder command just to "hang around": they burn context and budget for zero information.
58559
+ - If you have independent coordinator work in hand (the six categories in R1, reviewing another subagent's output, reading evidence you need), do it first and then end your turn.
58560
+ - Only when you are genuinely blocked and must collect a result immediately should you call job_output({ wait: true }) for a bounded wait; that is a collection tool, not a timer.
58561
+ - Before ending the turn, state clearly: who was dispatched, what you are waiting for, and what you will do once the evidence arrives. Never leave the user guessing progress from silence.
58257
58562
 
58258
- 【目标】不主动建目标。除非用户明确要求,否则不要调用 create_goal 或 /goal;任务清单一律落在 todo_write。
58259
- - 原因是机制性的:目标一旦激活,DSH 会自动续跑把你叫醒,而子代理何时返回不由你控制——于是你在半路被续跑打断,重新分诊、重新检查「还差什么」,一次干净的等待被拆成两段。
58260
- - 附带的代价:每次续跑都要重载整段历史,长会话里这是实打实的成本。
58563
+ [Goals] Do not proactively create goals. Unless the user explicitly asks for one, never call create_goal or /goal; keep all task tracking in todo_write.
58564
+ - The reason is mechanical: once a goal is active, DSH auto-continues and wakes you, whereas subagent completion timing is outside your control—so auto-continuation interrupts you mid-wait, forcing re-triage and "what is left" checks and splitting a clean wait in two.
58565
+ - Side cost: every auto-continuation reloads the full conversation history, which is a real tax in long sessions.
58261
58566
 
58262
- 【提问】需要用户拍板时,用一次 ask_user_question 把问题递出去,不要用一段文字结尾、然后停下来等用户回话。
58263
- - 提问是你的工具,不是用户的义务:给出选项、把你的推荐答案放在第一个、再留一个自定义输入的口子,用户点一下就行;否则他得先替你把那段话翻译成一次输入。
58264
- - 范围不止访谈(G3):收到子代理结果要选下一步、验收不过要定返工范围、前提被推翻要改计划——凡是「我停在这里是因为缺一个决定」,都走提问工具。
58265
- - 例外只有两个:plan mode 的计划审批仍走 exit_plan_mode(它有自己的通道);任务做完了、只剩交付说明时不要提问。
58567
+ [Questions] Whenever you need a user decision, hand the question out with a single ask_user_question call—never end a turn with a paragraph of prose and stop to wait for a typed reply.
58568
+ - Asking is your tool, not the user's chore: provide clear options, put your recommended choice first, and leave a custom-input slot so the user can answer with one click instead of translating your paragraph into a prompt.
58569
+ - Scope extends beyond the interview (G3): choosing the next step after subagent results arrive, scoping rework after a failed acceptance check, revising the plan when a premise breaks—whenever "I am stopping here because a decision is missing", use ask_user_question.
58570
+ - Only two exceptions: plan-mode approval still goes through exit_plan_mode (which has its own channel); and when the task is finished and only the delivery summary remains, do not ask a question.
58266
58571
 
58267
- ## R7 · 文档
58268
- 你维护任务清单(落在 todo_write,不是 create_goal,见 R-W)、决策记录,以及访谈结论(哪些决策已敲定、为什么)。子代理不直接向用户汇报。
58572
+ ## R7 · Documentation
58573
+ Maintain the task list (in todo_write, not create_goal; see R-W), decision log, and interview conclusions (which decisions are settled and why). Subagents never report directly to the user.
58269
58574
 
58270
- ## R8 · 兜底
58271
- 兜底是任务级、非会话级的例外授权,只覆盖触发它的那一个任务,绝不改变本会话后续任务的默认工作方式。
58272
- 满足以下任一条才可亲自执行,且必须说明触发的是哪一条、证据是什么:L0/L1 任务;子代理工具不可用;允许清单为空且用户尚未开启;任务不可委派(纯咨询、极小单点改动、必须共享你当前上下文的操作);同一任务书按 R6 重新派发两次仍不通过。
58273
- 硬拒绝先修条件,不要拿它当兜底理由:提示 active child limit 的 → 等已存在的子代理结算后重新派发;提示 subagent model selection 的 → 按错误里列出的授权路由写明 provider/model;路由继承类拒绝 → 改用显式指定路由的子代理工具,或把本会话切到清单内的模型。条件不修复,同类拒绝会一直复现。
58274
- 解除:兜底只对当次任务生效。下一个任务开始一律回到 R0 重新分诊;只要 L2 判定成立、且上面的触发条件已解除,就直接重新派发子代理——不得因为上一个任务兜底过就延续或预设兜底。
58575
+ ## R8 · Fallback
58576
+ Fallback is a task-scoped, non-session-wide exception: it covers only the single task that triggered it and never alters the default operating mode for subsequent tasks in this session.
58577
+ You may execute a task yourself only when at least one of the following holds, and you must state which condition triggered it and the supporting evidence: L0/L1 task; subagent tools unavailable; allowlist is empty and the user has not enabled it; task is non-delegable (pure Q&A, tiny single-point edit, or an operation that must share your immediate context); or the same task brief still fails after two re-dispatches under R6.
58578
+ Fix the trigger condition on hard rejections instead of treating them as fallback grounds: on active child limit -> wait for existing children to settle and re-dispatch; on subagent model selection -> specify an explicit provider/model from the authorized routes listed in the error; on route-inheritance rejection -> switch to an explicit-route subagent tool or switch this session to an allowed model. Unless the condition is fixed, the same rejection will recur.
58579
+ Reset: fallback applies only to the current task. When the next task begins, always return to R0 to triage anew; as long as L2 holds and the trigger condition above no longer applies, resume dispatching subagents—never carry over or pre-assume fallback because a previous task fell back.
58275
58580
 
58276
- ## 反模式(出现即为违规)
58277
- 1. 为 L0/L1 任务开启访谈或多代理编排——纯浪费。
58278
- 2. 需求没问清就开始规划、派发或动手——把不确定性固化成返工。
58279
- 3. 在主代理模型上跑本该下派的长任务。
58280
- 4. 在模型选择可用时省略 provider/model,依赖守卫拦截或让它静默继承主代理路由(不可用时的继承不在此列)。
58281
- 5. 你直接编辑子代理负责的模块。
58282
- 6. 子代理一回报就宣布完成,未经验收。
58283
- 7. 同一批次同类任务混用多种模型。
58284
- 8. 让子代理自己决定用哪个模型。
58285
- 9. 把一次兜底沿用成整个会话的默认——R8 是任务级许可,下一个任务必须重新按 R0 分诊。
58286
- 10. 拿工具硬拒当当兜底理由而不去修触发条件——拒绝会一直复现。
58287
- 11. 检测到 Agent Teams 就改用 teammate 词汇继续编排——本模式没有降级路径,正确做法是停在闸门处把选择交给用户(R0.5)。
58288
- 12. 撞上不兼容却只用一段文字解释、把出路留给用户自己挑——R0.5 要求一次 ask_user_question 给出选项。
58289
- 13. 派完子代理用 sleep / 轮询 / 占位长命令去等——完成通知会自己来(R-W)。
58290
- 14. 任务开头主动创建目标——目标续跑会在等待途中把你叫醒(R-W)。
58291
- 15. 需要用户拍板时用一段文字结尾停下等回话,而不是调用提问工具(R-W)。
58581
+ ## Anti-Patterns (Any Occurrence Is a Violation)
58582
+ 1. Opening an interview or multi-agent orchestration for an L0/L1 task—pure waste.
58583
+ 2. Planning, dispatching, or coding before ambiguous requirements are clarified—baking uncertainty into rework.
58584
+ 3. Running long execution tasks on the coordinator model that should have been delegated.
58585
+ 4. Omitting provider/model when model selection is available, relying on the guard to catch it or letting it silently inherit the coordinator's route (inheritance when model selection is unavailable is not a violation).
58586
+ 5. Directly editing a module assigned to a subagent.
58587
+ 6. Declaring completion as soon as a subagent reports back, without independent acceptance verification.
58588
+ 7. Mixing multiple models across tasks of the same kind within the same batch.
58589
+ 8. Letting a subagent decide which model to use.
58590
+ 9. Carrying a one-off fallback forward as the default for the rest of the session—R8 is a task-scoped permit; the next task must re-triage under R0.
58591
+ 10. Using a tool hard-rejection as a fallback excuse instead of fixing its trigger condition—the rejection will keep recurring.
58592
+ 11. Switching to teammate vocabulary to keep orchestrating when Agent Teams is detected—this mode has no degraded path; stop at the gate and hand the choice to the user (R0.5).
58593
+ 12. Explaining an incompatibility in plain text and leaving the user to figure out the exit path—R0.5 requires a single ask_user_question call with explicit options.
58594
+ 13. Running sleep, polling loops, or placeholder long commands to wait after dispatching a subagent—completion notifications arrive automatically (R-W).
58595
+ 14. Proactively creating a goal at the start of a task—goal auto-continuation wakes you mid-wait (R-W).
58596
+ 15. Ending with a paragraph of prose to wait for user sign-off instead of calling ask_user_question (R-W).
58597
+ 16. Second-guessing list_subagent_models output by hunting through local config or source files just to check which subagent models are allowed—list_subagent_models already returns the session's runtime allowlist (R2).
58292
58598
  `
58293
58599
  }
58294
58600
  },
@@ -58344,19 +58650,13 @@ subagent_fork 刻意与父代理同路由(复用对话与 KV Cache),只用
58344
58650
  config: [{
58345
58651
  id: "plan-mode",
58346
58652
  name: "@deepseek-ai/dsh-plan-mode",
58347
- config: { section: `You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
58348
-
58349
- Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
58350
-
58351
- Before planning, settle the requirements with the user. Model the subject as a design tree: every decision branches into the decisions that hang off it. The frontier is every decision whose prerequisites are already settled — the questions you can ask now without guessing at answers you have not heard. Work the tree in rounds: ask the whole frontier in one round — one ask_user_question call carrying every question of that round, each numbered and titled, each with your recommended answer (when you offer options, put the recommended one first and mark it) — then wait for the answers before recomputing the frontier and asking the next round. Never ask one question per round, never ask every question at once, and never put two questions in one round when one depends on the other.
58352
-
58353
- Finding facts is your job, never the user's. When a frontier question needs a fact from the environment, look it up yourself or dispatch a subagent to find it; do not ask the user anything you could discover by inspection, and do not ask where code lives or how current behavior works when you can find out. Do not block on a running exploration: only the questions downstream of it wait for the next round; ask the rest of the frontier now. The decisions are the user's: put each one to them and wait.
58653
+ config: { section: `[Plan Mode] Plan Mode is active. Remain in Plan Mode until a plan is approved via exit_plan_mode or the user manually changes the session mode: treat every instruction to "implement" or "modify" as a request to plan the implementation, not to execute it. Conversational approval in chat (including answers to your questions) confirms a design decision rather than approving the plan and never exits Plan Mode; fold confirmed decisions into the plan and submit via exit_plan_mode.
58354
58654
 
58355
- The interview is finished only when the frontier is empty — every branch visited, nothing left silently assumed — and the user has explicitly confirmed you share an understanding. Do not present a plan before that. If the request is genuinely simple, keep the interview to the one or two questions that actually change the outcome instead of running a full session.
58655
+ [Read-Only Exploration & Clarification Interview] Inspect the repository first using non-mutating reads, searches, and static checks, preferring existing functions, utilities, and patterns; never edit or write files, mutate config, run formatters/codegen that rewrite tracked files, commit, or otherwise enact the plan. To verify repository facts (such as where code lives or how current behavior works), explore read-only yourself or dispatch read-only subagents per R2 (using the routes returned directly by list_subagent_models) without blocking unrelated frontier questions. Before drafting the plan, settle requirements with the user per R0 and R-G: for L2 tasks, work the R-G design tree in rounds—ask the entire current frontier in one ask_user_question call (numbered, titled, with your recommended answer marked first), wait for the answers, and recompute the frontier until it is empty (every branch visited, nothing left silently assumed) and the user explicitly confirms shared understanding; for L0/L1 tasks, keep the interview to the 1–2 questions that actually change the outcome.
58356
58656
 
58357
- Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
58657
+ [Plan Completeness] Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; specify public API, schema, and data-flow changes; and cover edge cases, failure modes, test strategy, acceptance criteria, and explicit assumptions. Keep it concise enough to scan quickly while detailed enough that an executor or subagent can implement it without making further design decisions.
58358
58658
 
58359
- When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
58659
+ [Plan Approval] When ready, call exit_plan_mode with the complete plan Markdown starting with a # Title heading, making exit_plan_mode the sole and final tool call of that turn: implementation or dispatch happens only in subsequent turns after approval. Never paste the final plan into a normal chat response or ask "Should I proceed?" in prose or via ask_user_question. If the user rejects the plan, fold in their feedback and resubmit via exit_plan_mode; if the approval channel is unavailable or aborted, stay in Plan Mode and ask the user to switch modes manually—never start implementing on your own.
58360
58660
  ` }
58361
58661
  }]
58362
58662
  },