@eddyskywalker/dsh-chatgpt-subscription 0.48.4 → 0.50.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +2 -1
  3. package/lib/client.js +2383 -179
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +2246 -233
  6. package/lib/types/client/index.d.ts +2 -0
  7. package/lib/types/client/index.d.ts.map +1 -1
  8. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/locales.d.ts +14 -2
  10. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  11. package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
  12. package/lib/types/client/subagent/SubagentCard.d.ts +35 -0
  13. package/lib/types/client/subagent/SubagentCard.d.ts.map +1 -0
  14. package/lib/types/client/subagent/index.d.ts +28 -0
  15. package/lib/types/client/subagent/index.d.ts.map +1 -0
  16. package/lib/types/client/subagent/locales.d.ts +172 -0
  17. package/lib/types/client/subagent/locales.d.ts.map +1 -0
  18. package/lib/types/client/subagent/styles.d.ts +20 -0
  19. package/lib/types/client/subagent/styles.d.ts.map +1 -0
  20. package/lib/types/client/subagent/subagent-card-controller.d.ts +329 -0
  21. package/lib/types/client/subagent/subagent-card-controller.d.ts.map +1 -0
  22. package/lib/types/compat.d.ts +1 -1
  23. package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
  24. package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
  25. package/lib/types/host/common/request-images.d.ts +66 -0
  26. package/lib/types/host/common/request-images.d.ts.map +1 -1
  27. package/lib/types/host/kimi-code/adapter.d.ts +15 -0
  28. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  29. package/lib/types/host/kimi-code/cache-wait.d.ts +125 -0
  30. package/lib/types/host/kimi-code/cache-wait.d.ts.map +1 -0
  31. package/lib/types/host/kimi-code/mapper.d.ts +123 -6
  32. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  33. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  34. package/lib/types/host/kimi-code/session-cache-state.d.ts +14 -0
  35. package/lib/types/host/kimi-code/session-cache-state.d.ts.map +1 -1
  36. package/lib/types/host/kimi-code/types.d.ts +29 -4
  37. package/lib/types/host/kimi-code/types.d.ts.map +1 -1
  38. package/lib/types/host/preferences.d.ts.map +1 -1
  39. package/lib/types/host/routes.d.ts.map +1 -1
  40. package/lib/types/host/subagent-model-authorization.d.ts +130 -2
  41. package/lib/types/host/subagent-model-authorization.d.ts.map +1 -1
  42. package/lib/types/index.d.ts.map +1 -1
  43. package/lib/types/shared/contracts.d.ts +28 -0
  44. package/lib/types/shared/contracts.d.ts.map +1 -1
  45. package/lib/types/shared/kimi-code-contracts.d.ts +10 -1
  46. package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
  47. package/lib/types/shared/preferences.d.ts +52 -1
  48. package/lib/types/shared/preferences.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/presets/dispatch/agent.cordis.yml +2 -2
package/lib/index.js CHANGED
@@ -145,7 +145,7 @@ const ROUTE_PREFIX$5 = "/api/dsh-chatgpt-subscription";
145
145
  * `test/responses-client.test.ts` asserts the two agree, so a release that
146
146
  * forgets this constant fails the suite instead of shipping quietly.
147
147
  */
148
- const PLUGIN_VERSION = "0.48.4";
148
+ const PLUGIN_VERSION = "0.50.0";
149
149
  const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
150
150
  const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
151
151
  const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
@@ -859,7 +859,7 @@ async function rehydrateSnapshot(key) {
859
859
  * so a backend that flattens the envelope does not read as an empty catalog.
860
860
  */
861
861
  function parseListing(payload) {
862
- const record = asRecord$19(payload);
862
+ const record = asRecord$20(payload);
863
863
  const models = Array.isArray(payload) ? payload : record?.models;
864
864
  if (!Array.isArray(models)) return [];
865
865
  return models.flatMap((item) => parseListingEntry$1(item));
@@ -873,7 +873,7 @@ function parseListing(payload) {
873
873
  * back to a conservative answer rather than to a guess.
874
874
  */
875
875
  function parseListingEntry$1(value) {
876
- const record = asRecord$19(value);
876
+ const record = asRecord$20(value);
877
877
  if (record === void 0) return [];
878
878
  const id = asString$27(record.slug) ?? asString$27(record.id);
879
879
  if (id === void 0) return [];
@@ -909,7 +909,7 @@ function parseEfforts(value) {
909
909
  if (!Array.isArray(value)) return null;
910
910
  const out = [];
911
911
  for (const item of value) {
912
- const effort = asString$27(item) ?? asString$27(asRecord$19(item)?.effort);
912
+ const effort = asString$27(item) ?? asString$27(asRecord$20(item)?.effort);
913
913
  if (effort !== void 0 && !out.includes(effort)) out.push(effort);
914
914
  }
915
915
  return out.length > 0 ? out : null;
@@ -924,7 +924,7 @@ function parseEfforts(value) {
924
924
  function accountKeyFor$1(credentials) {
925
925
  return credentials.accountId || "default";
926
926
  }
927
- function asRecord$19(value) {
927
+ function asRecord$20(value) {
928
928
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
929
929
  }
930
930
  function asString$27(value) {
@@ -5289,9 +5289,9 @@ function requestImage$1(fetchFn, credentials, prompt, turnId, signal) {
5289
5289
  });
5290
5290
  }
5291
5291
  function readBase64Image(value) {
5292
- const root = record$6(value);
5292
+ const root = record$7(value);
5293
5293
  const data = Array.isArray(root?.data) ? root.data[0] : null;
5294
- const image = string$3(record$6(data)?.b64_json) ?? string$3(record$6(data)?.image_base64) ?? string$3(root?.b64_json) ?? string$3(root?.image);
5294
+ const image = string$3(record$7(data)?.b64_json) ?? string$3(record$7(data)?.image_base64) ?? string$3(root?.b64_json) ?? string$3(root?.image);
5295
5295
  if (image === null) throw new HarnessError("Codex image response did not include image data.", "CODEX_IMAGE_RESPONSE_INVALID");
5296
5296
  return image;
5297
5297
  }
@@ -5305,7 +5305,7 @@ function decodeImageBytes(base64, maxBytes) {
5305
5305
  function maxGeneratedImageBytes(attachments) {
5306
5306
  return Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
5307
5307
  }
5308
- function record$6(value) {
5308
+ function record$7(value) {
5309
5309
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
5310
5310
  }
5311
5311
  function string$3(value) {
@@ -9043,7 +9043,7 @@ function buildImageRequest(prompt, projectId, aspectRatio) {
9043
9043
  requestId: `img_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
9044
9044
  };
9045
9045
  }
9046
- function record$5(value) {
9046
+ function record$6(value) {
9047
9047
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
9048
9048
  }
9049
9049
  function string$2(value) {
@@ -9073,16 +9073,16 @@ function parseImageResponse(body) {
9073
9073
  } catch {
9074
9074
  continue;
9075
9075
  }
9076
- const root = record$5(frame);
9077
- const response = record$5(root?.response) ?? root;
9078
- const error = record$5(root?.error) ?? record$5(response?.error);
9076
+ const root = record$6(frame);
9077
+ const response = record$6(root?.response) ?? root;
9078
+ const error = record$6(root?.error) ?? record$6(response?.error);
9079
9079
  if (error) throw new HarnessError(`Antigravity image generation failed: ${string$2(error.message) ?? "unknown error"}`, "ANTIGRAVITY_IMAGE_FAILED");
9080
- const candidate = record$5(Array.isArray(response?.candidates) ? response.candidates[0] : void 0);
9081
- const parts = Array.isArray(record$5(candidate?.content)?.parts) ? record$5(candidate?.content).parts : [];
9080
+ const candidate = record$6(Array.isArray(response?.candidates) ? response.candidates[0] : void 0);
9081
+ const parts = Array.isArray(record$6(candidate?.content)?.parts) ? record$6(candidate?.content).parts : [];
9082
9082
  for (const raw of parts) {
9083
- const part = record$5(raw);
9083
+ const part = record$6(raw);
9084
9084
  if (!part || part.thought === true) continue;
9085
- const inline = record$5(part.inlineData) ?? record$5(part.inline_data);
9085
+ const inline = record$6(part.inlineData) ?? record$6(part.inline_data);
9086
9086
  const data = string$2(inline?.data);
9087
9087
  if (inline && data) {
9088
9088
  result.images.push({
@@ -9093,7 +9093,7 @@ function parseImageResponse(body) {
9093
9093
  }
9094
9094
  if (typeof part.text === "string") result.text += part.text;
9095
9095
  }
9096
- const blockReason = string$2(record$5(response?.promptFeedback)?.blockReason);
9096
+ const blockReason = string$2(record$6(response?.promptFeedback)?.blockReason);
9097
9097
  result.finishReason = string$2(candidate?.finishReason) ?? (blockReason ? `PROMPT_BLOCKED:${blockReason}` : result.finishReason);
9098
9098
  result.finishMessage = string$2(candidate?.finishMessage) ?? result.finishMessage;
9099
9099
  }
@@ -9429,13 +9429,13 @@ function normalizeSearchResult(data, maxResults) {
9429
9429
  };
9430
9430
  }
9431
9431
  function readSources(data) {
9432
- const root = record$4(data);
9432
+ const root = record$5(data);
9433
9433
  if (root === null) return [];
9434
9434
  const candidates = [
9435
9435
  root.sources,
9436
9436
  root.results,
9437
- record$4(root.search_result)?.sources,
9438
- record$4(root.web_search)?.sources
9437
+ record$5(root.search_result)?.sources,
9438
+ record$5(root.web_search)?.sources
9439
9439
  ];
9440
9440
  for (const candidate of candidates) {
9441
9441
  if (!Array.isArray(candidate)) continue;
@@ -9445,7 +9445,7 @@ function readSources(data) {
9445
9445
  return [];
9446
9446
  }
9447
9447
  function readSource(value) {
9448
- const data = record$4(value);
9448
+ const data = record$5(value);
9449
9449
  if (data === null) return null;
9450
9450
  const url = string$1(data.url) ?? string$1(data.link) ?? string$1(data.uri);
9451
9451
  if (url === null || !isHttpUrl(url)) return null;
@@ -9474,7 +9474,7 @@ function readSource(value) {
9474
9474
  * counterpart of `output`, not a fallback for it.
9475
9475
  */
9476
9476
  function readContent(data) {
9477
- const root = record$4(data);
9477
+ const root = record$5(data);
9478
9478
  if (root === null) return void 0;
9479
9479
  return string$1(root.content) ?? string$1(root.output_text) ?? string$1(root.summary) ?? string$1(root.output) ?? void 0;
9480
9480
  }
@@ -9489,7 +9489,7 @@ function dedupeSources(sources) {
9489
9489
  }
9490
9490
  return result;
9491
9491
  }
9492
- function record$4(value) {
9492
+ function record$5(value) {
9493
9493
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9494
9494
  }
9495
9495
  function string$1(value) {
@@ -10812,7 +10812,9 @@ const DEFAULT_PREFERENCES = {
10812
10812
  contextWindowOverrides: {},
10813
10813
  proxyMode: "auto",
10814
10814
  customProxyUrl: null,
10815
- compactionRoute: null
10815
+ compactionRoute: null,
10816
+ subagentDefaultModels: [],
10817
+ subagentModelsByMainModel: {}
10816
10818
  };
10817
10819
  const SEARCH_PROVIDER_CODEX = "codex";
10818
10820
  function isSearchProviderPreference(value) {
@@ -10827,6 +10829,105 @@ function isCodexReasoningSummary(value) {
10827
10829
  function isProxyMode(value) {
10828
10830
  return value === "auto" || value === "custom" || value === "direct";
10829
10831
  }
10832
+ function asRecord$19(value) {
10833
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
10834
+ }
10835
+ /**
10836
+ * Read one non-empty trimmed string field.
10837
+ * @param value - Candidate field value from stored JSON.
10838
+ * @returns the trimmed string, or undefined when it is not a non-empty string.
10839
+ */
10840
+ function trimmedString(value) {
10841
+ if (typeof value !== "string") return void 0;
10842
+ const trimmed = value.trim();
10843
+ return trimmed.length === 0 ? void 0 : trimmed;
10844
+ }
10845
+ /**
10846
+ * Whether one value is a complete `{ provider, model }` route.
10847
+ *
10848
+ * Both halves are trimmed and must be non-empty: a route with a blank provider
10849
+ * names no adapter, and a blank model names no model, so neither can ever be
10850
+ * authorized. Stored documents are untrusted JSON, which is why this narrows
10851
+ * rather than asserts.
10852
+ * @param value - Candidate route.
10853
+ * @returns whether the value is a usable route.
10854
+ */
10855
+ function isSubagentModelRoute(value) {
10856
+ const record = asRecord$19(value);
10857
+ if (record === void 0) return false;
10858
+ return trimmedString(record["provider"]) !== void 0 && trimmedString(record["model"]) !== void 0;
10859
+ }
10860
+ /**
10861
+ * Normalize one route list, deduplicating it without reordering.
10862
+ *
10863
+ * Order is preserved because it is the order the settings card shows and the
10864
+ * order `list_subagent_models` prints: a set would silently reshuffle the list
10865
+ * a user arranged.
10866
+ * @param value - Candidate route list from stored JSON.
10867
+ * @returns the trimmed, deduplicated routes, or null when the value is not an
10868
+ * array of well-formed routes.
10869
+ */
10870
+ function normalizeSubagentModelRoutes(value) {
10871
+ if (!Array.isArray(value)) return null;
10872
+ const routes = [];
10873
+ const seen = /* @__PURE__ */ new Set();
10874
+ for (const entry of value) {
10875
+ if (!isSubagentModelRoute(entry)) return null;
10876
+ const provider = entry.provider.trim();
10877
+ const model = entry.model.trim();
10878
+ const key = `${provider}/${model}`;
10879
+ if (seen.has(key)) continue;
10880
+ seen.add(key);
10881
+ routes.push({
10882
+ provider,
10883
+ model
10884
+ });
10885
+ }
10886
+ return routes;
10887
+ }
10888
+ /**
10889
+ * Whether one key names a main agent route as `provider/model`.
10890
+ *
10891
+ * The split is at the FIRST slash, so a model id that legally contains a slash
10892
+ * (`command-code` + `deepseek/deepseek-v4.1-flash`) still round-trips: the
10893
+ * provider half ends at the first separator and the model half keeps the rest.
10894
+ * @param key - Candidate map key.
10895
+ * @returns whether the key carries a non-empty provider and a non-empty model.
10896
+ */
10897
+ function isValidMainModelKey(key) {
10898
+ if (typeof key !== "string") return false;
10899
+ const trimmed = key.trim();
10900
+ const separator = trimmed.indexOf("/");
10901
+ if (separator <= 0) return false;
10902
+ return trimmed.slice(separator + 1).length > 0;
10903
+ }
10904
+ /**
10905
+ * Merge one `subagentModelsByMainModel` patch into the stored map.
10906
+ *
10907
+ * A merge rather than a replace, so the card can clear or set one main model
10908
+ * without resending every other entry — and `null` (or `[]`) is how one entry
10909
+ * is cleared, since a stored map has no other way to say "no override".
10910
+ * @param current - Map already stored in the preferences document.
10911
+ * @param patch - Incoming patch from a settings card or a route body.
10912
+ * @returns the merged map, or null when any key or value is malformed.
10913
+ */
10914
+ function mergeSubagentModelsByMainModel(current, patch) {
10915
+ const patchRecord = asRecord$19(patch);
10916
+ if (patchRecord === void 0) return null;
10917
+ const merged = { ...current };
10918
+ for (const [rawKey, rawRoutes] of Object.entries(patchRecord)) {
10919
+ const key = rawKey.trim();
10920
+ if (!isValidMainModelKey(key)) return null;
10921
+ if (rawRoutes === null || Array.isArray(rawRoutes) && rawRoutes.length === 0) {
10922
+ delete merged[key];
10923
+ continue;
10924
+ }
10925
+ const routes = normalizeSubagentModelRoutes(rawRoutes);
10926
+ if (routes === null) return null;
10927
+ merged[key] = routes;
10928
+ }
10929
+ return merged;
10930
+ }
10830
10931
  //#endregion
10831
10932
  //#region src/host/common/legacy-preferences.ts
10832
10933
  /**
@@ -10993,6 +11094,10 @@ function sectionOf(document, id) {
10993
11094
  */
10994
11095
  function registerPreferenceStore(settings) {
10995
11096
  const contextWindowOverrides = z.object(Object.fromEntries(CODEX_MODEL_CATALOG.map((entry) => [entry.id, z.number().step(1).min(1).max(contextWindowLimitForModel(entry.id))]))).default({});
11097
+ const subagentModelRoutes = z.array(z.object({
11098
+ provider: z.string(),
11099
+ model: z.string()
11100
+ }));
10996
11101
  const schema = z.object({
10997
11102
  enabled: z.boolean().default(DEFAULT_PREFERENCES.enabled ?? true),
10998
11103
  quickQuotaVisible: z.boolean().default(DEFAULT_PREFERENCES.quickQuotaVisible),
@@ -11022,7 +11127,9 @@ function registerPreferenceStore(settings) {
11022
11127
  compactionRoute: z.union([z.object({
11023
11128
  provider: z.string(),
11024
11129
  model: z.string()
11025
- }), z.const(null)]).default(DEFAULT_PREFERENCES.compactionRoute ?? null)
11130
+ }), z.const(null)]).default(DEFAULT_PREFERENCES.compactionRoute ?? null),
11131
+ subagentDefaultModels: subagentModelRoutes.default([]),
11132
+ subagentModelsByMainModel: z.dict(subagentModelRoutes).default({})
11026
11133
  });
11027
11134
  if (!hasRegister(settings)) {
11028
11135
  const fileStore = new FilePreferencesStore(schema, preferencesPath(), readLegacyPreferences);
@@ -11080,6 +11187,16 @@ var SettingsPreferenceStore = class {
11080
11187
  model: patch.compactionRoute.model.trim()
11081
11188
  };
11082
11189
  }
11190
+ if (patch.subagentDefaultModels !== void 0) {
11191
+ const routes = normalizeSubagentModelRoutes(patch.subagentDefaultModels);
11192
+ if (routes === null) throw new PreferenceError("Unsupported subagentDefaultModels preference.");
11193
+ normalized.subagentDefaultModels = routes;
11194
+ }
11195
+ if (patch.subagentModelsByMainModel !== void 0) {
11196
+ const merged = mergeSubagentModelsByMainModel(this.scope.get().subagentModelsByMainModel, patch.subagentModelsByMainModel);
11197
+ if (merged === null) throw new PreferenceError("Unsupported subagentModelsByMainModel preference.");
11198
+ normalized.subagentModelsByMainModel = merged;
11199
+ }
11083
11200
  if (patch.customProxyUrl !== void 0) if (patch.customProxyUrl !== null) {
11084
11201
  const trimmed = patch.customProxyUrl.trim();
11085
11202
  if (trimmed.length > 0 && !/^https?:\/\//i.test(trimmed) && !/^socks5?:\/\//i.test(trimmed)) normalized.customProxyUrl = `http://${trimmed}`;
@@ -11175,7 +11292,7 @@ function offloadOldestInputImages(input, maxBytes = maxRequestImageBytes$2()) {
11175
11292
  for (const item of input) {
11176
11293
  if (!Array.isArray(item.content)) continue;
11177
11294
  for (const [index, block] of item.content.entries()) {
11178
- if (record$3(block) === null || block.type !== "input_image") continue;
11295
+ if (record$4(block) === null || block.type !== "input_image") continue;
11179
11296
  const url = block.image_url;
11180
11297
  if (typeof url !== "string") continue;
11181
11298
  retained.push({
@@ -11375,14 +11492,14 @@ function toolParametersForCodex(toolName, parameters, allowSandboxRetry) {
11375
11492
  const augmentCommandTool = isCommandTool(toolName);
11376
11493
  if (!hideSandboxControls && toolName !== "run_code" && !augmentCommandTool) return parameters;
11377
11494
  const cloned = structuredClone(parameters);
11378
- const properties = record$3(cloned.properties);
11495
+ const properties = record$4(cloned.properties);
11379
11496
  if (properties !== null && hideSandboxControls) {
11380
11497
  delete properties.sandbox_permissions;
11381
11498
  delete properties.justification;
11382
11499
  }
11383
11500
  if (hideSandboxControls && Array.isArray(cloned.required)) cloned.required = cloned.required.filter((name) => name !== "sandbox_permissions" && name !== "justification");
11384
11501
  if (toolName === "run_code" && properties !== null) {
11385
- const code = record$3(properties.code);
11502
+ const code = record$4(properties.code);
11386
11503
  if (code !== null) {
11387
11504
  const current = typeof code.description === "string" ? code.description.trim() : "";
11388
11505
  const compatibility = "Strict JavaScript/TypeScript source. Nested shell commands are string data: template literals may consume ${...}, backticks, backslashes, and escape sequences before PowerShell, Bash, or POSIX sh sees them; String.raw still performs ${...} interpolation. Prefer ordinary quoted string arrays joined with \"\\n\", or write a script file with a dedicated file tool.";
@@ -11400,7 +11517,7 @@ function toolParametersForCodex(toolName, parameters, allowSandboxRetry) {
11400
11517
  return cloned;
11401
11518
  }
11402
11519
  function appendPropertyDescription(value, addition) {
11403
- const property = record$3(value);
11520
+ const property = record$4(value);
11404
11521
  if (property === null) return;
11405
11522
  const current = typeof property.description === "string" ? property.description.trim() : "";
11406
11523
  if (current.includes(addition)) return;
@@ -11411,7 +11528,7 @@ function isCommandTool(name) {
11411
11528
  return normalized === "pwsh" || normalized === "powershell" || normalized === "bash" || normalized === "sh" || normalized === "shell";
11412
11529
  }
11413
11530
  function hasSandboxControls(parameters) {
11414
- const properties = record$3(parameters.properties);
11531
+ const properties = record$4(parameters.properties);
11415
11532
  return properties !== null && ("sandbox_permissions" in properties || "justification" in properties);
11416
11533
  }
11417
11534
  function recentSandboxRetryToolNames(messages) {
@@ -11623,11 +11740,11 @@ function replayOutputItems(message) {
11623
11740
  const replay = message.source?.replayState;
11624
11741
  if (typeof replay !== "object" || replay === null || Array.isArray(replay)) return null;
11625
11742
  const envelope = replay;
11626
- const items = Array.isArray(envelope.outputItems) ? envelope.outputItems : Array.isArray(record$3(envelope.response)?.outputItems) ? record$3(envelope.response).outputItems : null;
11743
+ const items = Array.isArray(envelope.outputItems) ? envelope.outputItems : Array.isArray(record$4(envelope.response)?.outputItems) ? record$4(envelope.response).outputItems : null;
11627
11744
  if (!Array.isArray(items)) return null;
11628
11745
  return structuredClone(items.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)));
11629
11746
  }
11630
- function record$3(value) {
11747
+ function record$4(value) {
11631
11748
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
11632
11749
  }
11633
11750
  //#endregion
@@ -12028,7 +12145,7 @@ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /
12028
12145
  return;
12029
12146
  }
12030
12147
  if (type === "response.content_part.done") {
12031
- const part = record$2(event.part);
12148
+ const part = record$3(event.part);
12032
12149
  const partType = string(part?.type);
12033
12150
  if (partType === "output_text" || partType === "refusal") yield* adoptText(string(part?.text) ?? "");
12034
12151
  return;
@@ -12060,7 +12177,7 @@ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /
12060
12177
  return;
12061
12178
  }
12062
12179
  if (type === "response.output_item.added" || type === "response.output_item.done") {
12063
- const item = record$2(event.item);
12180
+ const item = record$3(event.item);
12064
12181
  if (item !== null && type === "response.output_item.done") replayOutput.push(structuredClone(item));
12065
12182
  if (string(item?.type) !== "function_call") {
12066
12183
  if (type === "response.output_item.done") yield* adoptText(messageItemText(item));
@@ -12120,18 +12237,18 @@ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /
12120
12237
  return;
12121
12238
  }
12122
12239
  if (type === "response.completed" || type === "response.incomplete") {
12123
- const completed = record$2(event.response);
12124
- usage = mapUsage$2(record$2(completed?.usage));
12240
+ const completed = record$3(event.response);
12241
+ usage = mapUsage$2(record$3(completed?.usage));
12125
12242
  const output = completed?.output;
12126
12243
  if (Array.isArray(output)) {
12127
- replayOutput = output.filter((item) => record$2(item) !== null).map((item) => structuredClone(item));
12128
- for (const item of output) yield* adoptText(messageItemText(record$2(item)));
12244
+ replayOutput = output.filter((item) => record$3(item) !== null).map((item) => structuredClone(item));
12245
+ for (const item of output) yield* adoptText(messageItemText(record$3(item)));
12129
12246
  }
12130
12247
  terminal = type === "response.incomplete" ? { kind: "max-tokens" } : { kind: "stop" };
12131
12248
  return;
12132
12249
  }
12133
12250
  if (type === "response.failed" || type === "error") {
12134
- const error = record$2(event.error) ?? record$2(record$2(event.response)?.error);
12251
+ const error = record$3(event.error) ?? record$3(record$3(event.response)?.error);
12135
12252
  const message = string(error?.message) ?? "Codex generation failed.";
12136
12253
  const rawCode = string(error?.code)?.toLowerCase();
12137
12254
  const isOverload = message.toLowerCase().includes("overload") || message.toLowerCase().includes("server error") || rawCode === "server_error" || rawCode === "service_unavailable" || rawCode === "internal_error";
@@ -12157,7 +12274,7 @@ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /
12157
12274
  } catch {
12158
12275
  throw new LlmError("Codex returned malformed streaming JSON.", "PROTOCOL_ERROR");
12159
12276
  }
12160
- const valueRecord = record$2(event);
12277
+ const valueRecord = record$3(event);
12161
12278
  if (valueRecord !== null) yield* consume(valueRecord);
12162
12279
  }
12163
12280
  }
@@ -12263,10 +12380,10 @@ function mapUsage$2(value) {
12263
12380
  if (value === null) return null;
12264
12381
  const totalInput = number(value.input_tokens) ?? 0;
12265
12382
  const outputTokens = number(value.output_tokens) ?? 0;
12266
- const details = record$2(value.input_tokens_details);
12383
+ const details = record$3(value.input_tokens_details);
12267
12384
  const cached = number(details?.cached_tokens);
12268
12385
  const written = number(details?.cache_write_tokens);
12269
- const reasoning = number(record$2(value.output_tokens_details)?.reasoning_tokens);
12386
+ const reasoning = number(record$3(value.output_tokens_details)?.reasoning_tokens);
12270
12387
  return {
12271
12388
  inputTokens: Math.max(0, totalInput - (cached ?? 0) - (written ?? 0)),
12272
12389
  outputTokens,
@@ -12285,7 +12402,7 @@ function isSafeJsonArguments(value) {
12285
12402
  }
12286
12403
  function stripSandboxControls(value) {
12287
12404
  try {
12288
- const parsedRecord = record$2(JSON.parse(value));
12405
+ const parsedRecord = record$3(JSON.parse(value));
12289
12406
  if (parsedRecord === null) return value;
12290
12407
  let changed = false;
12291
12408
  for (const name of ["sandbox_permissions", "justification"]) if (name in parsedRecord) {
@@ -12322,14 +12439,14 @@ function messageItemText(item) {
12322
12439
  if (item === null || string(item.type) !== "message" || !Array.isArray(item.content)) return "";
12323
12440
  let text = "";
12324
12441
  for (const part of item.content) {
12325
- const value = record$2(part);
12442
+ const value = record$3(part);
12326
12443
  const kind = string(value?.type);
12327
12444
  if (kind !== "output_text" && kind !== "refusal") continue;
12328
12445
  text += string(value?.text) ?? "";
12329
12446
  }
12330
12447
  return text;
12331
12448
  }
12332
- function record$2(value) {
12449
+ function record$3(value) {
12333
12450
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
12334
12451
  }
12335
12452
  function string(value) {
@@ -13422,7 +13539,7 @@ function mapCodexUsage(value) {
13422
13539
  return parseCodexUsage(value).buckets;
13423
13540
  }
13424
13541
  function parseCodexUsage(value) {
13425
- const data = record$1(value);
13542
+ const data = record$2(value);
13426
13543
  if (data === null) return structuredClone(EMPTY_USAGE);
13427
13544
  const planType = typeof data.plan_type === "string" ? data.plan_type : null;
13428
13545
  const buckets = [];
@@ -13431,21 +13548,21 @@ function parseCodexUsage(value) {
13431
13548
  addBucket(buckets, usedIds, "code-review", "Code review", planType, data.code_review_rate_limit);
13432
13549
  const additional = Array.isArray(data.additional_rate_limits) ? data.additional_rate_limits : [];
13433
13550
  for (const [index, value] of additional.entries()) {
13434
- const limit = record$1(value);
13551
+ const limit = record$2(value);
13435
13552
  if (limit === null) continue;
13436
- const idSource = text$3(limit.limit_name) ?? text$3(limit.metered_feature) ?? `additional-${index + 1}`;
13437
- addBucket(buckets, usedIds, uniqueId(slug(idSource), usedIds), readableLimitName(text$3(limit.limit_name) ?? text$3(limit.metered_feature) ?? idSource), planType, limit.rate_limit);
13553
+ const idSource = text$4(limit.limit_name) ?? text$4(limit.metered_feature) ?? `additional-${index + 1}`;
13554
+ addBucket(buckets, usedIds, uniqueId(slug(idSource), usedIds), readableLimitName(text$4(limit.limit_name) ?? text$4(limit.metered_feature) ?? idSource), planType, limit.rate_limit);
13438
13555
  }
13439
13556
  return {
13440
13557
  buckets,
13441
13558
  credits: mapCredits(data.credits),
13442
- individualLimit: mapIndividualLimit(record$1(data.spend_control)?.individual_limit),
13443
- spendControlReached: boolean(record$1(data.spend_control)?.reached),
13559
+ individualLimit: mapIndividualLimit(record$2(data.spend_control)?.individual_limit),
13560
+ spendControlReached: boolean(record$2(data.spend_control)?.reached),
13444
13561
  resetCredits: mapResetCredits(data.rate_limit_reset_credits)
13445
13562
  };
13446
13563
  }
13447
13564
  function addBucket(result, usedIds, id, name, planType, value) {
13448
- const source = record$1(value);
13565
+ const source = record$2(value);
13449
13566
  if (source === null) return;
13450
13567
  const primary = mapWindow(source.primary_window);
13451
13568
  const secondary = mapWindow(source.secondary_window);
@@ -13461,7 +13578,7 @@ function addBucket(result, usedIds, id, name, planType, value) {
13461
13578
  usedIds.add(id);
13462
13579
  }
13463
13580
  function mapWindow(value) {
13464
- const data = record$1(value);
13581
+ const data = record$2(value);
13465
13582
  if (data === null) return null;
13466
13583
  const used = numeric(data.used_percent);
13467
13584
  if (used === void 0) return null;
@@ -13474,7 +13591,7 @@ function mapWindow(value) {
13474
13591
  };
13475
13592
  }
13476
13593
  function mapCredits(value) {
13477
- const data = record$1(value);
13594
+ const data = record$2(value);
13478
13595
  if (data === null) return null;
13479
13596
  const hasCredits = boolean(data.has_credits);
13480
13597
  const unlimited = boolean(data.unlimited);
@@ -13487,7 +13604,7 @@ function mapCredits(value) {
13487
13604
  };
13488
13605
  }
13489
13606
  function mapIndividualLimit(value) {
13490
- const data = record$1(value);
13607
+ const data = record$2(value);
13491
13608
  if (data === null) return null;
13492
13609
  const remaining = numeric(data.remaining_percent);
13493
13610
  const reset = numeric(data.reset_at);
@@ -13502,7 +13619,7 @@ function mapIndividualLimit(value) {
13502
13619
  };
13503
13620
  }
13504
13621
  function mapResetCredits(value) {
13505
- const data = record$1(value);
13622
+ const data = record$2(value);
13506
13623
  if (data === null) return null;
13507
13624
  const available = numeric(data.available_count);
13508
13625
  if (available === void 0) return null;
@@ -13512,14 +13629,14 @@ function mapResetCredits(value) {
13512
13629
  };
13513
13630
  }
13514
13631
  function parseResetCredits(value) {
13515
- const data = record$1(value);
13632
+ const data = record$2(value);
13516
13633
  if (data === null) return {
13517
13634
  availableCount: 0,
13518
13635
  expiresAt: null,
13519
13636
  availableCreditIds: []
13520
13637
  };
13521
- const available = (Array.isArray(data.credits) ? data.credits : []).map(record$1).filter((credit) => credit !== null && credit.status === "available").map((credit) => ({
13522
- id: text$3(credit.id),
13638
+ const available = (Array.isArray(data.credits) ? data.credits : []).map(record$2).filter((credit) => credit !== null && credit.status === "available").map((credit) => ({
13639
+ id: text$4(credit.id),
13523
13640
  expiresAt: timestamp(credit.expires_at)
13524
13641
  })).filter((credit) => credit.id !== null).sort((left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER));
13525
13642
  const reported = numeric(data.available_count);
@@ -13536,7 +13653,7 @@ function timestamp(value) {
13536
13653
  const parsed = Date.parse(value);
13537
13654
  return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
13538
13655
  }
13539
- function record$1(value) {
13656
+ function record$2(value) {
13540
13657
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
13541
13658
  }
13542
13659
  function boolean(value) {
@@ -13553,7 +13670,7 @@ function decimalText(value) {
13553
13670
  const trimmed = value.trim();
13554
13671
  return trimmed !== "" ? trimmed : null;
13555
13672
  }
13556
- function text$3(value) {
13673
+ function text$4(value) {
13557
13674
  return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
13558
13675
  }
13559
13676
  function slug(value) {
@@ -14006,6 +14123,15 @@ function readPreferencesUpdate(value, current) {
14006
14123
  model: value.compactionRoute.model.trim()
14007
14124
  };
14008
14125
  }
14126
+ if ("subagentDefaultModels" in value) {
14127
+ const routes = normalizeSubagentModelRoutes(value.subagentDefaultModels);
14128
+ if (routes === null) throw new PreferenceError("subagentDefaultModels must be an array of { provider, model } routes.");
14129
+ patch.subagentDefaultModels = routes;
14130
+ }
14131
+ if ("subagentModelsByMainModel" in value) {
14132
+ if (mergeSubagentModelsByMainModel(current.subagentModelsByMainModel, value.subagentModelsByMainModel) === null) throw new PreferenceError("subagentModelsByMainModel must map \"provider/model\" keys to arrays of { provider, model } routes, or null to clear one.");
14133
+ patch.subagentModelsByMainModel = value.subagentModelsByMainModel;
14134
+ }
14009
14135
  if ("contextWindowOverrides" in value) {
14010
14136
  if (!isRecord$55(value.contextWindowOverrides)) throw new PreferenceError("contextWindowOverrides must be an object.");
14011
14137
  const overrides = {};
@@ -25751,11 +25877,73 @@ const TOOL_SUBAGENT = /(?:^|[-_/])(?:subagent|agent|task|delegate|ralph|teammate
25751
25877
  const TOOL_SOURCE = /(?:^|[-_/])(?:read|view|cat|open|edit|write|patch|apply_patch|notebook|document|diff)(?:[-_/]|$)/i;
25752
25878
  /** 独立开关覆盖的检索工具;只有明确导航任务才允许进入新投影。 */
25753
25879
  const TOOL_RETRIEVAL = /^(?:read|web_fetch|grep|glob)$/;
25880
+ /**
25881
+ * 多节/多读输出进入新投影的最小长度门槛(字符)。
25882
+ *
25883
+ * 短于这个长度、又没有 spill 的普通源码与契约读一律逐字保留:
25884
+ * 只有「确定已经大到装不进预算」的输出才值得换成摘要。
25885
+ */
25886
+ const MULTI_OUTPUT_MIN_CHARS = 2e4;
25754
25887
  /** 一行文本的裁剪(超长行才裁剪,并留下省略号)。 */
25755
25888
  function clip(text, max) {
25756
25889
  return text.length <= max ? text : `${text.slice(0, max)}…`;
25757
25890
  }
25758
25891
  /**
25892
+ * DSH 的输出上限提示:正文被截断,全文落在临时文件里。
25893
+ *
25894
+ * 省略说明可能是精确或未知字节数,还可能跟着一段图片计数;这些形态都由 DSH 的 formatSpillNotice 产生。
25895
+ * 路径本身可能带 `.txt` 之类的后缀,所以路径用**非贪婪**捕获,末尾的句点单独留给正则。
25896
+ */
25897
+ const SPILL_NOTICE = /\n\n\((?:Omitted \d+ bytes\.|More bytes were omitted\.)(?: Omitted \d+ images\.)? Full formatted result stored at: ([^\r\n]+?)(?:\. Use read with offset\/limit, or grep this path to search within it)?\.\)\s*$/;
25898
+ /**
25899
+ * 读回一个 spill 文件的完整 UTF-8 内容。
25900
+ *
25901
+ * 文件系统依赖在这里动态引入:`contracts.ts` 必须保持零 Node 依赖,而投影是宿主侧代码。
25902
+ * 读不到、不是普通文件、或超过单结果字节上限时一律返回 undefined —— 宁可少投影,
25903
+ * 也不能把一份读了一半的原文当成全文入库。
25904
+ * @param filePath - spill 提示里给出的路径。
25905
+ * @returns 完整内容,或 undefined。
25906
+ */
25907
+ async function readSpillFile(filePath) {
25908
+ try {
25909
+ const { stat, readFile } = await import("node:fs/promises");
25910
+ const stats = await stat(filePath);
25911
+ if (!stats.isFile()) return void 0;
25912
+ if (stats.size > CACHE_WARMUP_DEFAULTS.maxResultBytes) return void 0;
25913
+ return await readFile(filePath, "utf8");
25914
+ } catch {
25915
+ return;
25916
+ }
25917
+ }
25918
+ /**
25919
+ * 处理 DSH 的 spill 提示。
25920
+ *
25921
+ * 读得到全文:投影与入库都用全文,`cache_warmup_result` 才能翻到**未截断**的输出。
25922
+ * 读不到:剥掉提示尾部,剩下的正文照常参与投影与入库。
25923
+ * @param raw - 工具结果原文。
25924
+ * @returns 解析结果。
25925
+ */
25926
+ async function resolveSpillText(raw) {
25927
+ const match = SPILL_NOTICE.exec(raw);
25928
+ if (match === null) return {
25929
+ text: raw,
25930
+ spilled: false,
25931
+ recovered: false
25932
+ };
25933
+ const filePath = match[1].trim();
25934
+ const full = filePath === "" ? void 0 : await readSpillFile(filePath);
25935
+ if (full !== void 0) return {
25936
+ text: full,
25937
+ spilled: true,
25938
+ recovered: true
25939
+ };
25940
+ return {
25941
+ text: raw.slice(0, match.index),
25942
+ spilled: true,
25943
+ recovered: false
25944
+ };
25945
+ }
25946
+ /**
25759
25947
  * 剥离终端 ANSI/CSI 转义序列(如 `\x1b[31mFAIL\x1b[0m`)与回车符 `\r`。
25760
25948
  *
25761
25949
  * JSON 解码后,原先源码/日志里的 `\u001b[31m` 会还原成控制字符 `\x1b`;
@@ -25833,6 +26021,10 @@ function buildTestSections(evidence) {
25833
26021
  const SHELL_ERROR_LINE = /(?:error|fatal|panic|traceback|exception|EACCES|EPERM|ENOENT|EEXIST|ELIFECYCLE|ERR!|FAILED|permission denied|cannot |could not |not found)/i;
25834
26022
  /** TypeScript 编译器报错首行:`path(line,col): error TSxxxx:` 或 `path:line:col - error TSxxxx:`。 */
25835
26023
  const TS_DIAGNOSTIC_HEADER = /^(?:.+?\(\d+,\d+\):\s+error\s+TS\d+:|.+?:\d+:\d+\s+-\s+error\s+TS\d+:)/;
26024
+ /** shell 预览里最多列出的错误行数;超出的部分用 ref 提示补读。 */
26025
+ const SHELL_ERROR_MAX_LINES = 25;
26026
+ /** shell 预览里错误集最多占用的字符数(行数上限先到,这个只是兜底)。 */
26027
+ const SHELL_ERROR_MAX_CHARS = 6500;
25836
26028
  /**
25837
26029
  * 紧跟在 TypeScript 诊断首行后面的续行(缩进的类型比对、代码高亮行、波浪线指示行),
25838
26030
  * 最多连带收集 6 行,使模型无需补读就能看全 `Type A is not assignable to Type B` 的完整原因。
@@ -25959,15 +26151,27 @@ function buildShellSections(text) {
25959
26151
  }
25960
26152
  if (SHELL_ERROR_LINE.test(trimmed)) {
25961
26153
  errorCount += 1;
25962
- errors.push(` ${clip(trimmed, 360)}`);
26154
+ errors.push(` ${clip(trimmed, 240)}`);
25963
26155
  }
25964
26156
  }
25965
26157
  if (errorCount > 300 || errors.length > 600) return void 0;
25966
26158
  const exitMatch = /(?:exit code|exited with code|process exited with code)[:=\s]+(-?\d+)/i.exec(text) ?? /\[exit code: (-?\d+)\]/i.exec(text);
25967
26159
  const sections = [];
25968
26160
  if (exitMatch !== null) sections.push({ lines: [`exit code: ${exitMatch[1]}`] });
25969
- sections.push({ lines: [`errors: ${errorCount} (complete set)`] });
25970
- if (errors.length > 0) sections.push({ lines: errors });
26161
+ const shownErrors = [];
26162
+ let shownChars = 0;
26163
+ for (const line of errors) {
26164
+ if (shownErrors.length >= SHELL_ERROR_MAX_LINES) break;
26165
+ if (shownChars + line.length + 1 > SHELL_ERROR_MAX_CHARS) break;
26166
+ shownErrors.push(line);
26167
+ shownChars += line.length + 1;
26168
+ }
26169
+ const truncated = shownErrors.length < errors.length;
26170
+ sections.push({ lines: [truncated ? `errors: ${errorCount} (first ${shownErrors.length} shown; full set via ref)` : `errors: ${errorCount} (complete set)`] });
26171
+ if (shownErrors.length > 0) {
26172
+ const body = truncated ? [...shownErrors, ` … ${errors.length - shownErrors.length} more error lines (full list via ref)`] : shownErrors;
26173
+ sections.push({ lines: body });
26174
+ }
25971
26175
  const nonEmpty = lines.map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
25972
26176
  const head = nonEmpty.slice(0, 5).map((line) => ` ${clip(line.trim(), 160)}`);
25973
26177
  const tail = nonEmpty.slice(-5).map((line) => ` ${clip(line.trim(), 160)}`);
@@ -26183,6 +26387,42 @@ function planRetrievalOutline(text, toolName) {
26183
26387
  };
26184
26388
  }
26185
26389
  }
26390
+ /**
26391
+ * 读出装着日志字段的那个对象的元数据。
26392
+ * @param holder - 装着日志字段的对象(顶层信封或 1 层嵌套的内层对象)。
26393
+ * @returns 读到的元数据;两者都没有时 undefined。
26394
+ */
26395
+ function shellMetadataOf(holder) {
26396
+ const job = isPlainRecord(holder.job) ? holder.job : void 0;
26397
+ let exitCode;
26398
+ if (typeof holder.exitCode === "number" && Number.isFinite(holder.exitCode)) exitCode = holder.exitCode;
26399
+ if (exitCode === void 0 && typeof job?.exitCode === "number" && Number.isFinite(job.exitCode)) exitCode = job.exitCode;
26400
+ if (exitCode === void 0 && typeof job?.detail === "string") {
26401
+ const matched = /(?:exit code|exited with code|process exited with code)\s*[:=]?\s*(-?\d+)/i.exec(job.detail);
26402
+ if (matched !== null) exitCode = Number(matched[1]);
26403
+ }
26404
+ const status = typeof job?.status === "string" ? job.status : void 0;
26405
+ if (exitCode === void 0 && status === void 0) return void 0;
26406
+ return {
26407
+ ...exitCode === void 0 ? {} : { exitCode },
26408
+ ...status === void 0 ? {} : { status }
26409
+ };
26410
+ }
26411
+ /**
26412
+ * 把信封里的退出码与状态补到 shell 预览的最前面。
26413
+ * @param sections - `buildShellSections` 交出来的段落。
26414
+ * @param meta - 信封元数据。
26415
+ * @returns 补过元数据的段落。
26416
+ */
26417
+ function withShellMetadata(sections, meta) {
26418
+ if (meta === void 0) return sections;
26419
+ const lines = [];
26420
+ const hasExit = sections.some((section) => section.lines[0]?.startsWith("exit code:"));
26421
+ if (meta.exitCode !== void 0 && !hasExit) lines.push(`exit code: ${meta.exitCode}`);
26422
+ if (meta.status !== void 0) lines.push(`status: ${meta.status}`);
26423
+ if (lines.length === 0) return sections;
26424
+ return [{ lines }, ...sections];
26425
+ }
26186
26426
  /** 契约/类型定义:命中两行以上声明就当它是契约,保持原样。 */
26187
26427
  function looksLikeContract(text) {
26188
26428
  let declarations = 0;
@@ -26242,16 +26482,53 @@ function isJsonDocument(text) {
26242
26482
  }
26243
26483
  return false;
26244
26484
  }
26245
- /** PTC `run_code` JSON envelope 中允许独立投影日志的字段白名单。 */
26485
+ /**
26486
+ * PTC `run_code` JSON envelope 中允许独立投影日志的字段白名单。
26487
+ *
26488
+ * `'text'` 在列:`tools.job_output` 返回的正是 `{ text, job }`,日志正文就叫 `text`。
26489
+ */
26246
26490
  const JSON_ENVELOPE_LOG_KEYS = [
26247
26491
  "stdout",
26248
26492
  "stderr",
26249
26493
  "output",
26250
26494
  "logs",
26251
- "log"
26495
+ "log",
26496
+ "text"
26252
26497
  ];
26253
26498
  /** JSON envelope 其余字段允许占用的最大序列化字符数(防止把含批量源码的混合 envelope 压掉)。 */
26254
26499
  const JSON_ENVELOPE_MAX_OTHER_CHARS = 800;
26500
+ /** 判断一个值是不是普通 JSON 对象(不是数组,也不是 null)。 */
26501
+ function isPlainRecord(value) {
26502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26503
+ }
26504
+ /** 重建信封:只把预览写回被替换的键,其余字段原样保留。 */
26505
+ function rebuildEnvelope(binding, innerPreview) {
26506
+ if (binding.nested === void 0) return JSON.stringify({
26507
+ ...binding.record,
26508
+ [binding.key]: innerPreview
26509
+ });
26510
+ return JSON.stringify({
26511
+ ...binding.record,
26512
+ [binding.nested.key]: {
26513
+ ...binding.nested.record,
26514
+ [binding.key]: innerPreview
26515
+ }
26516
+ });
26517
+ }
26518
+ /** 信封外壳的序列化长度:被替换的字段先按空串算。 */
26519
+ function envelopeShellLength(binding) {
26520
+ if (binding.nested === void 0) return JSON.stringify({
26521
+ ...binding.record,
26522
+ [binding.key]: ""
26523
+ }).length;
26524
+ return JSON.stringify({
26525
+ ...binding.record,
26526
+ [binding.nested.key]: {
26527
+ ...binding.nested.record,
26528
+ [binding.key]: ""
26529
+ }
26530
+ }).length;
26531
+ }
26255
26532
  /**
26256
26533
  * 解开一层或两层 JSON 包装(支持顶层 JSON 字符串 `"..."` 或双重序列化的 `"{...}"`)。
26257
26534
  */
@@ -26295,10 +26572,10 @@ function parseJsonCandidate(text) {
26295
26572
  *
26296
26573
  * 规则刻意保守:
26297
26574
  * - 截断 JSON、数组一律返回 undefined;
26575
+ * - 白名单字段可以出现在**顶层**(`{ stdout: "..." }`),也可以出现在**1 层嵌套**里
26576
+ * (`{ testOutput: { text: "...", job: {...} } }`,即 `tools.job_output` 的形状);
26298
26577
  * - 对象 envelope 除白名单字段外,其余字段序列化后的总长度不超过 `JSON_ENVELOPE_MAX_OTHER_CHARS`;
26299
- * - 解码并剥离 ANSI 后的日志文本自身必须能通过非源码/非契约的结构化日志投影计划;
26300
- * - 返回的 `storeContent` 把解码后的多行日志直接展开(对象 envelope 还会把其余元数据缩进在头部),
26301
- * 彻底避免单行 minified JSON 因没有真实 `\n` 导致 `store.page` 的按行分页失效。
26578
+ * - 解码并剥离 ANSI 后的日志文本自身必须能通过非源码/非契约的结构化日志投影计划。
26302
26579
  */
26303
26580
  function extractJsonEnvelopeLogField(text, toolName) {
26304
26581
  const candidate = parseJsonCandidate(text);
@@ -26311,33 +26588,498 @@ function extractJsonEnvelopeLogField(text, toolName) {
26311
26588
  return { plan };
26312
26589
  }
26313
26590
  const { record } = candidate;
26314
- let candidateKey;
26315
- let candidateText = "";
26591
+ const candidates = [];
26316
26592
  for (const key of JSON_ENVELOPE_LOG_KEYS) {
26317
- const val = record[key];
26318
- if (typeof val === "string" && val.length > candidateText.length) {
26319
- candidateKey = key;
26320
- candidateText = val;
26593
+ const value = record[key];
26594
+ if (typeof value === "string" && value.length > 0) candidates.push({
26595
+ binding: {
26596
+ record,
26597
+ key
26598
+ },
26599
+ text: value
26600
+ });
26601
+ }
26602
+ for (const [outerKey, outerValue] of Object.entries(record)) {
26603
+ if (!isPlainRecord(outerValue)) continue;
26604
+ for (const key of JSON_ENVELOPE_LOG_KEYS) {
26605
+ const value = outerValue[key];
26606
+ if (typeof value === "string" && value.length > 0) candidates.push({
26607
+ binding: {
26608
+ record,
26609
+ key,
26610
+ nested: {
26611
+ key: outerKey,
26612
+ record: outerValue
26613
+ }
26614
+ },
26615
+ text: value
26616
+ });
26321
26617
  }
26322
26618
  }
26323
- if (candidateKey === void 0 || candidateText.length === 0) return void 0;
26619
+ if (candidates.length === 0) return void 0;
26620
+ candidates.sort((left, right) => right.text.length - left.text.length);
26621
+ const picked = candidates[0];
26622
+ const binding = picked.binding;
26623
+ const candidateText = picked.text;
26624
+ const boundKey = binding.key;
26625
+ const nested = binding.nested;
26626
+ const holder = nested === void 0 ? record : nested.record;
26324
26627
  const rest = {};
26325
26628
  for (const [key, value] of Object.entries(record)) {
26326
- if (key === candidateKey) continue;
26629
+ if (nested === void 0 ? key === boundKey : key === nested.key) continue;
26327
26630
  if (typeof value === "string" && (value.length > 400 || looksLikeSourceCode(value) || looksLikeContract(value))) return;
26328
26631
  rest[key] = value;
26329
26632
  }
26330
- if (JSON.stringify(rest).length > JSON_ENVELOPE_MAX_OTHER_CHARS) return void 0;
26633
+ let restJson;
26634
+ if (nested === void 0) restJson = JSON.stringify(rest);
26635
+ else {
26636
+ const innerRest = {};
26637
+ for (const [key, value] of Object.entries(nested.record)) {
26638
+ if (key === boundKey) continue;
26639
+ if (typeof value === "string" && (value.length > 400 || looksLikeSourceCode(value) || looksLikeContract(value))) return;
26640
+ innerRest[key] = value;
26641
+ }
26642
+ restJson = JSON.stringify({
26643
+ ...rest,
26644
+ [nested.key]: innerRest
26645
+ });
26646
+ }
26647
+ if (restJson.length > JSON_ENVELOPE_MAX_OTHER_CHARS) return void 0;
26331
26648
  const cleanField = stripAnsi(candidateText);
26332
26649
  if (isJsonDocument(cleanField) || isMixedEvidenceOutput(cleanField)) return void 0;
26333
26650
  const plan = planPlainTextProjection(cleanField, toolName);
26334
- if (plan.kind !== "preview") return void 0;
26651
+ if (plan.kind !== "preview") {
26652
+ if (plan.strategy === "verbatim-unparsed") {
26653
+ const meta = shellMetadataOf(holder);
26654
+ if (meta !== void 0) {
26655
+ const sections = buildShellSections(cleanField);
26656
+ if (sections !== void 0) return {
26657
+ envelope: binding,
26658
+ plan: {
26659
+ kind: "preview",
26660
+ strategy: "shell-output",
26661
+ sections: withShellMetadata(sections, meta)
26662
+ }
26663
+ };
26664
+ }
26665
+ }
26666
+ const multiSection = planMultiSectionProjection(cleanField);
26667
+ if (multiSection !== void 0) return {
26668
+ envelope: binding,
26669
+ plan: multiSection
26670
+ };
26671
+ return;
26672
+ }
26673
+ return {
26674
+ envelope: binding,
26675
+ plan
26676
+ };
26677
+ }
26678
+ /** 规范 glob 路径表。 */
26679
+ function canonicalGlobPaths(value) {
26680
+ if (!Array.isArray(value) || value.length === 0) return void 0;
26681
+ const paths = [];
26682
+ for (const entry of value) {
26683
+ if (typeof entry !== "string") return void 0;
26684
+ paths.push(entry);
26685
+ }
26686
+ return paths;
26687
+ }
26688
+ /** 规范 grep 命中表。 */
26689
+ function canonicalGrepMatches(value) {
26690
+ if (!Array.isArray(value) || value.length === 0) return void 0;
26691
+ const matches = [];
26692
+ for (const entry of value) {
26693
+ if (!isPlainRecord(entry)) return void 0;
26694
+ if (typeof entry.path !== "string") return void 0;
26695
+ if (!Number.isSafeInteger(entry.lineNumber)) return void 0;
26696
+ if (typeof entry.line !== "string") return void 0;
26697
+ matches.push({
26698
+ path: entry.path,
26699
+ lineNumber: entry.lineNumber,
26700
+ line: entry.line
26701
+ });
26702
+ }
26703
+ return matches;
26704
+ }
26705
+ /** 规范读取结果。 */
26706
+ function canonicalReadResult(value) {
26707
+ if (!isPlainRecord(value)) return void 0;
26708
+ if (typeof value.path !== "string" || value.path.trim() === "") return void 0;
26709
+ if (!Array.isArray(value.lines) || value.lines.length === 0) return void 0;
26710
+ const lines = [];
26711
+ for (const entry of value.lines) {
26712
+ if (!isPlainRecord(entry)) return void 0;
26713
+ if (!Number.isSafeInteger(entry.number)) return void 0;
26714
+ if (typeof entry.text !== "string") return void 0;
26715
+ lines.push({
26716
+ number: entry.number,
26717
+ text: entry.text
26718
+ });
26719
+ }
26335
26720
  return {
26336
- envelope: {
26721
+ path: value.path,
26722
+ ...typeof value.offset === "number" && Number.isFinite(value.offset) ? { offset: value.offset } : {},
26723
+ lines
26724
+ };
26725
+ }
26726
+ /** 在顶层或 1 层嵌套里找规范 grep 命中表。 */
26727
+ function findGrepBinding(record) {
26728
+ const top = canonicalGrepMatches(record.matches);
26729
+ if (top !== void 0) return {
26730
+ binding: {
26337
26731
  record,
26338
- key: candidateKey
26732
+ key: "matches"
26339
26733
  },
26340
- plan
26734
+ matches: top
26735
+ };
26736
+ for (const [outerKey, outerValue] of Object.entries(record)) {
26737
+ if (!isPlainRecord(outerValue)) continue;
26738
+ const nested = canonicalGrepMatches(outerValue.matches);
26739
+ if (nested !== void 0) return {
26740
+ binding: {
26741
+ record,
26742
+ key: "matches",
26743
+ nested: {
26744
+ key: outerKey,
26745
+ record: outerValue
26746
+ }
26747
+ },
26748
+ matches: nested
26749
+ };
26750
+ }
26751
+ }
26752
+ /** 在顶层或 1 层嵌套里找规范 glob 路径表。 */
26753
+ function findGlobBinding(record) {
26754
+ const top = canonicalGlobPaths(record.paths);
26755
+ if (top !== void 0) return {
26756
+ binding: {
26757
+ record,
26758
+ key: "paths"
26759
+ },
26760
+ paths: top,
26761
+ ...typeof record.root === "string" ? { root: record.root } : {}
26762
+ };
26763
+ for (const [outerKey, outerValue] of Object.entries(record)) {
26764
+ if (!isPlainRecord(outerValue)) continue;
26765
+ const nested = canonicalGlobPaths(outerValue.paths);
26766
+ if (nested !== void 0) return {
26767
+ binding: {
26768
+ record,
26769
+ key: "paths",
26770
+ nested: {
26771
+ key: outerKey,
26772
+ record: outerValue
26773
+ }
26774
+ },
26775
+ paths: nested,
26776
+ ...typeof outerValue.root === "string" ? { root: outerValue.root } : {}
26777
+ };
26778
+ }
26779
+ }
26780
+ /** grep 信封的预览段落:复用已有的完整计数策略。 */
26781
+ function buildGrepEnvelopeSections(matches) {
26782
+ const files = /* @__PURE__ */ new Map();
26783
+ for (const match of matches) {
26784
+ const entry = files.get(match.path);
26785
+ if (entry === void 0) files.set(match.path, {
26786
+ count: 1,
26787
+ first: match.line
26788
+ });
26789
+ else entry.count += 1;
26790
+ }
26791
+ return buildGrepSections({
26792
+ files,
26793
+ total: matches.length
26794
+ });
26795
+ }
26796
+ /** glob 信封的预览段落:复用已有的扩展名分布策略。 */
26797
+ function buildGlobEnvelopeSections(paths, root) {
26798
+ const sections = buildGlobSections(paths.join("\n"));
26799
+ if (root === void 0) return sections;
26800
+ return [{ lines: [`root: ${clip(root, 160)}`] }, ...sections];
26801
+ }
26802
+ /**
26803
+ * PTC 的结构化工具返回信封:`grep` 的 `matches` 与 `glob` 的 `paths`。
26804
+ *
26805
+ * 两种形状都认:顶层 `{ matches }` / `{ root, paths }`,以及 1 层嵌套
26806
+ * `{ batch: { matches } }` / `{ batch: { root, paths } }`。
26807
+ *
26808
+ * 落库用缩进 JSON 而不是原始的 minified 文本:minified 文本里没有真换行,
26809
+ * `store.page` 的「按行翻页」会退化成一次吐完一整行;缩进后每行一个字段,
26810
+ * 翻页干净,而且外层与内层的同级元数据一个不少。
26811
+ */
26812
+ function extractStructuredToolEnvelope(text) {
26813
+ const candidate = parseJsonCandidate(text);
26814
+ if (candidate === void 0 || candidate.kind !== "object") return void 0;
26815
+ const { record } = candidate;
26816
+ if (collectCompositeEntries(record).length !== 1) return void 0;
26817
+ const grep = findGrepBinding(record);
26818
+ if (grep !== void 0) {
26819
+ if (envelopeShellLength(grep.binding) > JSON_ENVELOPE_MAX_OTHER_CHARS) return void 0;
26820
+ return {
26821
+ binding: grep.binding,
26822
+ plan: {
26823
+ kind: "preview",
26824
+ strategy: "grep-results",
26825
+ sections: buildGrepEnvelopeSections(grep.matches)
26826
+ },
26827
+ storeContent: JSON.stringify(record, null, 2)
26828
+ };
26829
+ }
26830
+ const glob = findGlobBinding(record);
26831
+ if (glob !== void 0) {
26832
+ if (envelopeShellLength(glob.binding) > JSON_ENVELOPE_MAX_OTHER_CHARS) return void 0;
26833
+ return {
26834
+ binding: glob.binding,
26835
+ plan: {
26836
+ kind: "preview",
26837
+ strategy: "glob-results",
26838
+ sections: buildGlobEnvelopeSections(glob.paths, glob.root)
26839
+ },
26840
+ storeContent: JSON.stringify(record, null, 2)
26841
+ };
26842
+ }
26843
+ }
26844
+ /** 日志正文的错误行数与尾部若干行。 */
26845
+ function logSummaryOf(text) {
26846
+ const lines = text.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
26847
+ const errorLines = lines.filter((line) => SHELL_ERROR_LINE.test(line)).length;
26848
+ const summary = [];
26849
+ if (errorLines > 0) summary.push(`errors: ${errorLines}`);
26850
+ for (const line of lines.slice(-2)) summary.push(clip(line.trim(), 160));
26851
+ return summary;
26852
+ }
26853
+ /**
26854
+ * 把一个普通 JSON 对象分类成复合信封里的一个成员。
26855
+ *
26856
+ * 认得四种形状:读取结果、grep 命中表、glob 路径表、日志正文。
26857
+ * 分不出来就返回 undefined —— 宁可少认一个成员,也不把不相干的数据写进摘要。
26858
+ * @param key - 该成员所在的键。
26859
+ * @param value - 该成员的值。
26860
+ * @returns 识别出的成员,或 undefined。
26861
+ */
26862
+ function classifyCompositeEntry(key, value) {
26863
+ const jsonLength = JSON.stringify(value)?.length ?? 0;
26864
+ const matches = canonicalGrepMatches(value.matches);
26865
+ if (matches !== void 0) {
26866
+ const counts = /* @__PURE__ */ new Map();
26867
+ for (const match of matches) counts.set(match.path, (counts.get(match.path) ?? 0) + 1);
26868
+ const files = [...counts.entries()].map(([path, count]) => ({
26869
+ path,
26870
+ count
26871
+ })).sort((left, right) => right.count - left.count);
26872
+ return {
26873
+ kind: "grep",
26874
+ key,
26875
+ jsonLength,
26876
+ total: matches.length,
26877
+ files
26878
+ };
26879
+ }
26880
+ const paths = canonicalGlobPaths(value.paths);
26881
+ if (paths !== void 0) return {
26882
+ kind: "glob",
26883
+ key,
26884
+ jsonLength,
26885
+ count: paths.length,
26886
+ ...typeof value.root === "string" ? { root: value.root } : {},
26887
+ sample: paths.slice(0, 3)
26888
+ };
26889
+ const read = canonicalReadResult(value);
26890
+ if (read !== void 0) {
26891
+ let first = Number.POSITIVE_INFINITY;
26892
+ let last = Number.NEGATIVE_INFINITY;
26893
+ for (const line of read.lines) {
26894
+ if (line.number < first) first = line.number;
26895
+ if (line.number > last) last = line.number;
26896
+ }
26897
+ return {
26898
+ kind: "read",
26899
+ key,
26900
+ jsonLength,
26901
+ path: read.path,
26902
+ lineCount: read.lines.length,
26903
+ first,
26904
+ last,
26905
+ firstLines: read.lines.slice(0, 2).map((line) => `${line.number}: ${line.text}`)
26906
+ };
26907
+ }
26908
+ for (const logKey of JSON_ENVELOPE_LOG_KEYS) {
26909
+ const logText = value[logKey];
26910
+ if (typeof logText !== "string" || logText.length === 0) continue;
26911
+ if (looksLikeSourceCode(logText) || looksLikeContract(logText) || isMixedEvidenceOutput(logText)) return void 0;
26912
+ const meta = shellMetadataOf(value);
26913
+ return {
26914
+ kind: "log",
26915
+ key,
26916
+ jsonLength,
26917
+ chars: logText.length,
26918
+ ...meta?.exitCode === void 0 ? {} : { exitCode: meta.exitCode },
26919
+ ...meta?.status === void 0 ? {} : { status: meta.status },
26920
+ summary: logSummaryOf(logText)
26921
+ };
26922
+ }
26923
+ }
26924
+ /**
26925
+ * 收集一份 JSON 信封里的全部成员(顶层、1 层嵌套、数组元素)。
26926
+ * @param record - 顶层记录。
26927
+ * @returns 识别出的成员。
26928
+ */
26929
+ function collectCompositeEntries(record) {
26930
+ const entries = [];
26931
+ const push = (key, value) => {
26932
+ const entry = classifyCompositeEntry(key, value);
26933
+ if (entry === void 0) return false;
26934
+ entries.push(entry);
26935
+ return true;
26936
+ };
26937
+ const root = classifyCompositeEntry("(root)", record);
26938
+ if (root !== void 0) entries.push(root);
26939
+ for (const [key, value] of Object.entries(record)) {
26940
+ if (Array.isArray(value)) {
26941
+ value.forEach((item, index) => {
26942
+ if (isPlainRecord(item)) push(`${key}[${index}]`, item);
26943
+ });
26944
+ continue;
26945
+ }
26946
+ if (!isPlainRecord(value)) continue;
26947
+ if (push(key, value)) continue;
26948
+ for (const [subKey, subValue] of Object.entries(value)) if (isPlainRecord(subValue)) push(`${key}.${subKey}`, subValue);
26949
+ }
26950
+ return entries;
26951
+ }
26952
+ /** 复合信封的预览段落:逐个成员给出它是什么、有多大。 */
26953
+ function buildCompositeSections(entries) {
26954
+ const allReads = entries.every((entry) => entry.kind === "read");
26955
+ const shown = entries.slice(0, 20);
26956
+ const summaries = [];
26957
+ const firstLines = [];
26958
+ const details = [];
26959
+ for (const entry of shown) {
26960
+ if (entry.kind === "read") {
26961
+ summaries.push(` ${clip(entry.key, 60)}: ${clip(entry.path, 120)} lines ${entry.first}-${entry.last} (${entry.lineCount})`);
26962
+ continue;
26963
+ }
26964
+ if (entry.kind === "grep") {
26965
+ summaries.push(` ${clip(entry.key, 60)}: grep matches=${entry.total} across ${entry.files.length} files`);
26966
+ for (const file of entry.files.slice(0, 3)) summaries.push(` ${clip(file.path, 100)}: ${file.count}`);
26967
+ continue;
26968
+ }
26969
+ if (entry.kind === "glob") {
26970
+ const root = entry.root === void 0 ? "" : ` root=${clip(entry.root, 80)}`;
26971
+ summaries.push(` ${clip(entry.key, 60)}: glob paths=${entry.count}${root}`);
26972
+ for (const sample of entry.sample) summaries.push(` ${clip(sample, 140)}`);
26973
+ continue;
26974
+ }
26975
+ const state = [entry.exitCode === void 0 ? "" : `exit code=${entry.exitCode}`, entry.status === void 0 ? "" : `status=${entry.status}`].filter((part) => part !== "").join(" ");
26976
+ summaries.push(` ${clip(entry.key, 60)}: log chars=${entry.chars}${state === "" ? "" : ` (${state})`}`);
26977
+ for (const line of entry.summary) details.push(` ${clip(line, 160)}`);
26978
+ }
26979
+ if (entries.length > shown.length) summaries.push(` … ${entries.length - shown.length} more entries (full list via ref)`);
26980
+ for (const entry of shown.slice(0, 3)) {
26981
+ if (entry.kind !== "read") continue;
26982
+ firstLines.push(` ${clip(entry.path, 100)}`);
26983
+ for (const line of entry.firstLines) firstLines.push(` ${clip(line, 160)}`);
26984
+ }
26985
+ const sections = [{ lines: [allReads ? `reads: ${entries.length} (complete set)` : `results: ${entries.length} (complete set)`] }, { lines: summaries }];
26986
+ const extra = [...firstLines, ...details];
26987
+ if (extra.length > 0) sections.push({
26988
+ lines: [allReads ? "first lines:" : "detail:", ...extra],
26989
+ optional: true
26990
+ });
26991
+ return sections;
26992
+ }
26993
+ /**
26994
+ * 复合 JSON 信封的投影计划。
26995
+ *
26996
+ * 一份结果里混着多个成员(grep + read + glob + log)时,逐个成员摘要:
26997
+ * 模型要的是「这一趟拿到了什么、各自多大」,而不是某一份成员的完整正文。
26998
+ *
26999
+ * 触发条件:已经超长(或被 spill 截断)、成员不止一个、或单个成员本身就装不下预算。
27000
+ * @param text - 工具结果原文。
27001
+ * @param budgetMaxChars - 当前预览预算的字符上限。
27002
+ * @param oversized - 是否已经超长或被 spill 截断。
27003
+ * @returns 投影计划,或 undefined。
27004
+ */
27005
+ function extractCompositeEnvelope(text, budgetMaxChars, oversized) {
27006
+ const candidate = parseJsonCandidate(text);
27007
+ if (candidate === void 0 || candidate.kind !== "object") return void 0;
27008
+ const { record } = candidate;
27009
+ const entries = collectCompositeEntries(record);
27010
+ if (entries.length === 0) return void 0;
27011
+ if (!(oversized || entries.length >= 2 || entries.some((entry) => entry.jsonLength > budgetMaxChars))) return void 0;
27012
+ return {
27013
+ kind: "preview",
27014
+ strategy: "file-outline",
27015
+ sections: buildCompositeSections(entries),
27016
+ storeContent: JSON.stringify(record, null, 2)
27017
+ };
27018
+ }
27019
+ /** 多节输出的分节头:整行大写,且至少含一个分隔符(空格、斜杠、连字符)。 */
27020
+ const MULTI_SECTION_HEADER = /^[A-Z][A-Z0-9]*(?:[ _/()-][A-Z0-9]+)*$/;
27021
+ /** 判断一行是不是分节头;光秃秃一个大写词不算一节。 */
27022
+ function isMultiSectionHeader(trimmed) {
27023
+ if (trimmed.length < 4) return false;
27024
+ if (!MULTI_SECTION_HEADER.test(trimmed)) return false;
27025
+ return /[ _/-]/.test(trimmed);
27026
+ }
27027
+ /** 多节输出的预览段落:分了几节、每节多少行与行范围、每节前几行。 */
27028
+ function buildMultiSectionSections(text) {
27029
+ const lines = text.split("\n");
27030
+ const headers = [];
27031
+ for (let index = 0; index < lines.length; index += 1) {
27032
+ const trimmed = lines[index].trim();
27033
+ if (!isMultiSectionHeader(trimmed)) continue;
27034
+ if ((lines[index + 1] ?? "").trim() === "") continue;
27035
+ headers.push({
27036
+ name: trimmed,
27037
+ start: index
27038
+ });
27039
+ }
27040
+ if (headers.length < 2) return void 0;
27041
+ const summaries = [];
27042
+ const firstLines = [];
27043
+ for (let index = 0; index < headers.length; index += 1) {
27044
+ const header = headers[index];
27045
+ const end = headers[index + 1]?.start ?? lines.length;
27046
+ const body = lines.slice(header.start + 1, end).map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
27047
+ let first = Number.POSITIVE_INFINITY;
27048
+ let last = Number.NEGATIVE_INFINITY;
27049
+ for (const line of body) {
27050
+ const matched = /^\s*(\d+)(?::|\t|\|)/.exec(line);
27051
+ if (matched === null) continue;
27052
+ const number = Number(matched[1]);
27053
+ if (number < first) first = number;
27054
+ if (number > last) last = number;
27055
+ }
27056
+ const range = Number.isFinite(first) && Number.isFinite(last) ? ` (${first}-${last})` : "";
27057
+ summaries.push(` ${clip(header.name, 80)}: ${body.length} lines${range}`);
27058
+ }
27059
+ for (let index = 0; index < Math.min(headers.length, 3); index += 1) {
27060
+ const header = headers[index];
27061
+ const end = headers[index + 1]?.start ?? lines.length;
27062
+ const body = lines.slice(header.start + 1, end).map((line) => line.trim()).filter((line) => line !== "");
27063
+ firstLines.push(` ${clip(header.name, 80)}`);
27064
+ for (const line of body.slice(0, 2)) firstLines.push(` ${clip(line, 160)}`);
27065
+ }
27066
+ return [
27067
+ { lines: [`sections: ${headers.length} (complete set)`] },
27068
+ { lines: summaries },
27069
+ {
27070
+ lines: ["first lines:", ...firstLines],
27071
+ optional: true
27072
+ }
27073
+ ];
27074
+ }
27075
+ /** 多节输出的投影计划。 */
27076
+ function planMultiSectionProjection(text) {
27077
+ const sections = buildMultiSectionSections(text);
27078
+ if (sections === void 0) return void 0;
27079
+ return {
27080
+ kind: "preview",
27081
+ strategy: "structured-rows",
27082
+ sections
26341
27083
  };
26342
27084
  }
26343
27085
  /** 对一段纯文本(非 JSON 文档)做投影计划判定。 */
@@ -26398,8 +27140,11 @@ function planPlainTextProjection(rawText, toolName) {
26398
27140
  * 然后是 JSON envelope(只允许纯日志字段投影,混合证据一律原样),
26399
27141
  * 接着是测试失败集、grep、结构化、glob、shell 这些有明确形状的,
26400
27142
  * 最后落到「无法解析 → 原样」。
27143
+ *
27144
+ * `spilled` 与超长门槛决定多节/多读输出还有没有资格进新投影:
27145
+ * 短于门槛、又没有 spill 的普通源码与契约读一律逐字保留。
26401
27146
  */
26402
- function planProjection(text, toolName, navigation, retrievalOnly) {
27147
+ function planProjection(text, toolName, navigation, retrievalOnly, spilled = false, budgetMaxChars = Number.POSITIVE_INFINITY) {
26403
27148
  if (TOOL_SUBAGENT.test(toolName)) return {
26404
27149
  kind: "verbatim",
26405
27150
  strategy: "verbatim-subagent-report"
@@ -26409,10 +27154,21 @@ function planProjection(text, toolName, navigation, retrievalOnly) {
26409
27154
  kind: "verbatim",
26410
27155
  strategy: "verbatim-intent"
26411
27156
  };
26412
- if (TOOL_SOURCE.test(toolName) && navigation !== true) return {
26413
- kind: "verbatim",
26414
- strategy: "verbatim-source-code"
26415
- };
27157
+ const oversized = text.length >= MULTI_OUTPUT_MIN_CHARS || spilled;
27158
+ if (TOOL_SOURCE.test(toolName) && navigation !== true) {
27159
+ if (oversized && !isJsonDocument(text)) {
27160
+ const multiSection = planMultiSectionProjection(text);
27161
+ if (multiSection !== void 0) return multiSection;
27162
+ }
27163
+ return {
27164
+ kind: "verbatim",
27165
+ strategy: "verbatim-source-code"
27166
+ };
27167
+ }
27168
+ if (oversized && !isJsonDocument(text)) {
27169
+ const multiSection = planMultiSectionProjection(text);
27170
+ if (multiSection !== void 0) return multiSection;
27171
+ }
26416
27172
  if (looksLikeContract(text)) return {
26417
27173
  kind: "verbatim",
26418
27174
  strategy: "verbatim-contract"
@@ -26430,6 +27186,14 @@ function planProjection(text, toolName, navigation, retrievalOnly) {
26430
27186
  if (outline !== void 0) return outline;
26431
27187
  }
26432
27188
  if (isJsonDocument(text)) {
27189
+ const structuredTool = extractStructuredToolEnvelope(text);
27190
+ if (structuredTool !== void 0) return {
27191
+ kind: "preview",
27192
+ strategy: structuredTool.plan.strategy,
27193
+ sections: structuredTool.plan.sections,
27194
+ envelope: structuredTool.binding,
27195
+ storeContent: structuredTool.storeContent
27196
+ };
26433
27197
  const extracted = extractJsonEnvelopeLogField(text, toolName);
26434
27198
  if (extracted !== void 0) return {
26435
27199
  kind: "preview",
@@ -26437,6 +27201,8 @@ function planProjection(text, toolName, navigation, retrievalOnly) {
26437
27201
  sections: extracted.plan.sections,
26438
27202
  ...extracted.envelope === void 0 ? {} : { envelope: extracted.envelope }
26439
27203
  };
27204
+ const composite = extractCompositeEnvelope(text, budgetMaxChars, oversized);
27205
+ if (composite !== void 0) return composite;
26440
27206
  return {
26441
27207
  kind: "verbatim",
26442
27208
  strategy: "verbatim-unparsed"
@@ -26455,34 +27221,30 @@ function footerFor(refId) {
26455
27221
  * 入库失败一律降级为「保持原样」—— 没有 ref 的预览等于把原文丢了。
26456
27222
  */
26457
27223
  async function decide(args) {
26458
- const { text, input, budget } = args;
27224
+ const { input, budget } = args;
27225
+ const spill = await resolveSpillText(args.text);
27226
+ const text = spill.text;
26459
27227
  if (text.length <= budget.maxChars) return { reason: "verbatim-small" };
26460
- const plan = planProjection(text, input.toolName, input.navigation, input.retrievalOnly);
27228
+ const plan = planProjection(text, input.toolName, input.navigation, input.retrievalOnly, spill.spilled, budget.maxChars);
26461
27229
  if (plan.kind === "verbatim") return { reason: plan.strategy };
26462
27230
  if (input.isError === true && plan.strategy !== "test-failure" && plan.strategy !== "shell-output") return { reason: "verbatim-unparsed" };
26463
27231
  const header = `[cache-warmup preview] strategy=${plan.strategy} input=${text.length}ch`;
26464
27232
  const footerBudget = footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`).length;
26465
27233
  const jsonEscaped = plan.envelope !== void 0;
26466
- const envelopeShellLength = plan.envelope === void 0 ? 0 : JSON.stringify({
26467
- ...plan.envelope.record,
26468
- [plan.envelope.key]: ""
26469
- }).length;
26470
- const bodyBudget = budget.maxChars - header.length - footerBudget - envelopeShellLength - (jsonEscaped ? 4 : 2);
27234
+ const envelopeShell = plan.envelope === void 0 ? 0 : envelopeShellLength(plan.envelope);
27235
+ const bodyBudget = budget.maxChars - header.length - footerBudget - envelopeShell - (jsonEscaped ? 4 : 2);
26471
27236
  if (bodyBudget <= 0) return { reason: "verbatim-budget" };
26472
27237
  const body = fitSections(plan.sections, bodyBudget, jsonEscaped);
26473
27238
  if (body === void 0) return { reason: "verbatim-budget" };
26474
27239
  const store = input.store;
26475
27240
  if (store === void 0) return { reason: "store-unavailable" };
26476
27241
  const placeholderInner = `${header}\n${body}\n${footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`)}`;
26477
- if ((plan.envelope === void 0 ? placeholderInner : JSON.stringify({
26478
- ...plan.envelope.record,
26479
- [plan.envelope.key]: placeholderInner
26480
- })).length > budget.maxChars) return { reason: "verbatim-budget" };
27242
+ if ((plan.envelope === void 0 ? placeholderInner : rebuildEnvelope(plan.envelope, placeholderInner)).length > budget.maxChars) return { reason: "verbatim-budget" };
26481
27243
  let reference;
26482
27244
  try {
26483
27245
  reference = await store.put({
26484
27246
  sessionId: input.sessionId,
26485
- content: text,
27247
+ content: plan.storeContent ?? text,
26486
27248
  toolName: input.toolName,
26487
27249
  at: args.at
26488
27250
  });
@@ -26490,15 +27252,13 @@ async function decide(args) {
26490
27252
  return { reason: "store-unavailable" };
26491
27253
  }
26492
27254
  const innerPreview = `${header}\n${body}\n${footerFor(reference.refId)}`;
26493
- const preview = plan.envelope === void 0 ? innerPreview : JSON.stringify({
26494
- ...plan.envelope.record,
26495
- [plan.envelope.key]: innerPreview
26496
- });
27255
+ const preview = plan.envelope === void 0 ? innerPreview : rebuildEnvelope(plan.envelope, innerPreview);
26497
27256
  if (preview.length > budget.maxChars) return { reason: "verbatim-budget" };
26498
27257
  return {
26499
27258
  preview,
26500
27259
  refId: reference.refId,
26501
- strategy: plan.strategy
27260
+ strategy: plan.strategy,
27261
+ inputChars: text.length
26502
27262
  };
26503
27263
  }
26504
27264
  /** 块数组里文本字符数的合计。 */
@@ -26525,9 +27285,8 @@ async function projectToolResult(input) {
26525
27285
  at
26526
27286
  };
26527
27287
  if (typeof input.content === "string") {
26528
- const text = input.content;
26529
27288
  const decision = await decide({
26530
- text,
27289
+ text: input.content,
26531
27290
  input,
26532
27291
  budget,
26533
27292
  at
@@ -26537,8 +27296,8 @@ async function projectToolResult(input) {
26537
27296
  content: input.content,
26538
27297
  changed: false,
26539
27298
  metrics: {
26540
- inputChars: text.length,
26541
- outputChars: text.length,
27299
+ inputChars: input.content.length,
27300
+ outputChars: input.content.length,
26542
27301
  omittedChars: 0,
26543
27302
  strategy: decision.reason
26544
27303
  }
@@ -26550,9 +27309,9 @@ async function projectToolResult(input) {
26550
27309
  refId: decision.refId,
26551
27310
  preview: decision.preview,
26552
27311
  metrics: {
26553
- inputChars: text.length,
27312
+ inputChars: decision.inputChars,
26554
27313
  outputChars: decision.preview.length,
26555
- omittedChars: Math.max(0, text.length - decision.preview.length),
27314
+ omittedChars: Math.max(0, decision.inputChars - decision.preview.length),
26556
27315
  strategy: decision.strategy
26557
27316
  }
26558
27317
  };
@@ -26602,15 +27361,26 @@ async function projectToolResult(input) {
26602
27361
  refId: decision.refId,
26603
27362
  preview: decision.preview,
26604
27363
  metrics: {
26605
- inputChars: text.length,
27364
+ inputChars: decision.inputChars,
26606
27365
  outputChars: decision.preview.length,
26607
- omittedChars: Math.max(0, text.length - decision.preview.length),
27366
+ omittedChars: Math.max(0, decision.inputChars - decision.preview.length),
26608
27367
  strategy: decision.strategy
26609
27368
  }
26610
27369
  };
26611
27370
  }
26612
27371
  //#endregion
26613
27372
  //#region src/host/common/request-images.ts
27373
+ /**
27374
+ * Image resolution, byte budgeting and data-URL handling for one request.
27375
+ *
27376
+ * Shared by the provider lines that send inline images, because the budget is a
27377
+ * property of the deployment rather than of any one endpoint, and because two
27378
+ * lines that budget differently send different request bytes for the same picture.
27379
+ *
27380
+ * The notable decision is SCALE, not drop: see {@link resolveRequestImages}.
27381
+ *
27382
+ * @module dsh-chatgpt-subscription/request-images
27383
+ */
26614
27384
  function isRecord$50(value) {
26615
27385
  return typeof value === "object" && value !== null && !Array.isArray(value);
26616
27386
  }
@@ -26708,7 +27478,7 @@ function base64Length$1(bytes) {
26708
27478
  * the only estimate available, so it stays the fallback and every call site that
26709
27479
  * has no resolved map behaves exactly as before.
26710
27480
  */
26711
- function requestImagePayloadLength(block, images) {
27481
+ function inlinePayloadOf(block) {
26712
27482
  let data = asString$21(block.data) || asString$21(block.base64);
26713
27483
  const source = isRecord$50(block.source) ? block.source : void 0;
26714
27484
  if (!data && source) data = asString$21(source.data) || asString$21(source.base64);
@@ -26716,7 +27486,11 @@ function requestImagePayloadLength(block, images) {
26716
27486
  const matched = data.match(/^data:([^;,]+);base64,(.*)$/s);
26717
27487
  if (matched) data = matched[2] ?? "";
26718
27488
  }
26719
- if (data) return data.length;
27489
+ return data === void 0 || data === "" ? void 0 : data;
27490
+ }
27491
+ function requestImagePayloadLength(block, images) {
27492
+ const data = inlinePayloadOf(block);
27493
+ if (data !== void 0) return data.length;
26720
27494
  const attachment = attachmentOf$3(block);
26721
27495
  if (!attachment) return void 0;
26722
27496
  if (images === void 0) return base64Length$1(attachment.bytes);
@@ -26901,6 +27675,186 @@ function offloadOldestRequestImages$1(options, maxBytes = MAX_REQUEST_IMAGE_BYTE
26901
27675
  return replaceOldestRequestImages(options, imagesToOffloadCount(requestImagePayloadLengths(options, images), maxBytes), images);
26902
27676
  }
26903
27677
  /**
27678
+ * A stable identity for one image occurrence.
27679
+ *
27680
+ * Two occurrences of the SAME picture have to share one identity, and that
27681
+ * identity has to survive the turn: a durable attachment keeps its id, and an
27682
+ * inline payload keeps its bytes, so hashing the payload names the same
27683
+ * picture the same way on every later request.
27684
+ *
27685
+ * `undefined` for a block that carries no sendable payload: an image that
27686
+ * resolved to `unavailable` is serialized as a short text placeholder, so it
27687
+ * is not an image on the wire and has nothing to remember.
27688
+ */
27689
+ function requestImageKey(block, images) {
27690
+ if (requestImagePayloadLength(block, images) === void 0) return void 0;
27691
+ const attachment = attachmentOf$3(block);
27692
+ if (attachment !== void 0) return `attachment:${attachment.attachmentId}`;
27693
+ const data = inlinePayloadOf(block);
27694
+ if (data === void 0) return void 0;
27695
+ return `inline:${createHash("sha256").update(data).digest("hex")}`;
27696
+ }
27697
+ /**
27698
+ * Every sendable image occurrence one request carries, in request order.
27699
+ *
27700
+ * The traversal is the one `collectRequestImagePayloads` uses, tool results
27701
+ * included: a screenshot inside a tool result reaches the wire, so an
27702
+ * occurrence the budget cannot see is an occurrence it cannot budget for.
27703
+ */
27704
+ function collectImageOccurrences(options, images) {
27705
+ const occurrences = [];
27706
+ const walk = (content) => {
27707
+ if (!Array.isArray(content)) return;
27708
+ for (const block of content) {
27709
+ if (!isRecord$50(block)) continue;
27710
+ if (block.type === "image") {
27711
+ const bytes = requestImagePayloadLength(block, images);
27712
+ const key = requestImageKey(block, images);
27713
+ if (bytes !== void 0 && key !== void 0) occurrences.push({
27714
+ key,
27715
+ bytes
27716
+ });
27717
+ continue;
27718
+ }
27719
+ if (block.type === "tool-result") walk(block.content);
27720
+ }
27721
+ };
27722
+ for (const message of options.messages) walk(message.content);
27723
+ return occurrences;
27724
+ }
27725
+ /**
27726
+ * The identities of the images one request still carries, each once.
27727
+ *
27728
+ * The caller compares two of these to learn what a later build gave back: the
27729
+ * difference names the images a body-fit pass dropped after the budget pass
27730
+ * had already run.
27731
+ */
27732
+ function collectRemainingImageKeys(options, images) {
27733
+ const keys = [];
27734
+ const seen = /* @__PURE__ */ new Set();
27735
+ for (const occurrence of collectImageOccurrences(options, images)) {
27736
+ if (seen.has(occurrence.key)) continue;
27737
+ seen.add(occurrence.key);
27738
+ keys.push(occurrence.key);
27739
+ }
27740
+ return keys;
27741
+ }
27742
+ /**
27743
+ * Replace every occurrence of a dropped image with the placeholder.
27744
+ *
27745
+ * Unlike {@link replaceOldestRequestImages} this replaces by IDENTITY rather
27746
+ * than by position: a sticky budget drops a picture, and the picture has to
27747
+ * stay dropped for the rest of the session wherever it appears.
27748
+ */
27749
+ function replaceDroppedRequestImages(options, droppedKeys, images) {
27750
+ if (droppedKeys.size === 0) return {
27751
+ options,
27752
+ count: 0
27753
+ };
27754
+ let count = 0;
27755
+ const replaceIn = (content) => {
27756
+ if (!Array.isArray(content)) return {
27757
+ content,
27758
+ replaced: false
27759
+ };
27760
+ let replaced = false;
27761
+ return {
27762
+ content: content.map((block) => {
27763
+ if (!isRecord$50(block)) return block;
27764
+ if (block.type === "image") {
27765
+ const key = requestImageKey(block, images);
27766
+ if (key === void 0 || !droppedKeys.has(key)) return block;
27767
+ replaced = true;
27768
+ count += 1;
27769
+ return {
27770
+ type: "text",
27771
+ text: OMITTED_IMAGE_TEXT$1
27772
+ };
27773
+ }
27774
+ if (block.type === "tool-result" && Array.isArray(block.content)) {
27775
+ const inner = replaceIn(block.content);
27776
+ if (!inner.replaced) return block;
27777
+ replaced = true;
27778
+ return {
27779
+ ...block,
27780
+ content: inner.content
27781
+ };
27782
+ }
27783
+ return block;
27784
+ }),
27785
+ replaced
27786
+ };
27787
+ };
27788
+ let changed = false;
27789
+ const messages = options.messages.map((message) => {
27790
+ const result = replaceIn(message.content);
27791
+ if (!result.replaced) return message;
27792
+ changed = true;
27793
+ return {
27794
+ ...message,
27795
+ content: result.content
27796
+ };
27797
+ });
27798
+ return {
27799
+ options: changed ? {
27800
+ ...options,
27801
+ messages
27802
+ } : options,
27803
+ count
27804
+ };
27805
+ }
27806
+ /**
27807
+ * Offload images against a budget that RECOVERS instead of a budget that resets.
27808
+ *
27809
+ * WHY THE TWO WATERMARKS. A single ceiling makes the retained prefix depend
27810
+ * on the request: two conversations carrying the same images drop a different
27811
+ * number of them as one more image arrives, so the prefix the service cached
27812
+ * is rewritten on the next turn and the cache is cold again. Native `kimi-cli`
27813
+ * therefore keeps a HIGH mark (20 MiB) and a LOW mark (10 MiB): the request
27814
+ * is allowed to grow to the high mark, and once it passes it, the oldest
27815
+ * images are given back until the request is back under the LOW mark. The gap
27816
+ * between the two is the slack that keeps the number of retained images — and
27817
+ * therefore the cached prefix — stable across turns.
27818
+ *
27819
+ * WHY THE DROPS ARE STICKY. The images a request gives back are replaced for
27820
+ * the REST of the session: `priorDroppedKeys` carries the identities a
27821
+ * previous turn already dropped, and they are replaced again here. A picture
27822
+ * that was omitted stays omitted, so the request the service caches stops
27823
+ * changing once the session has settled under its low mark.
27824
+ *
27825
+ * @param options - the request to budget.
27826
+ * @param maxBytes - high-water mark; images are untouched below it.
27827
+ * @param lowBytes - low-water mark; the trimming stops once the request is back under it.
27828
+ * @param images - the resolution the request will be sent with.
27829
+ * @param priorDroppedKeys - identities dropped by earlier turns.
27830
+ */
27831
+ function offloadRequestImagesWithStickyBudget(options, maxBytes = MAX_REQUEST_IMAGE_BYTES$1, lowBytes = Math.max(1, Math.floor(maxBytes / 2)), images, priorDroppedKeys) {
27832
+ const allDroppedKeys = new Set(priorDroppedKeys ?? []);
27833
+ const pending = /* @__PURE__ */ new Map();
27834
+ for (const occurrence of collectImageOccurrences(options, images)) {
27835
+ if (allDroppedKeys.has(occurrence.key)) continue;
27836
+ pending.set(occurrence.key, (pending.get(occurrence.key) ?? 0) + occurrence.bytes);
27837
+ }
27838
+ let total = 0;
27839
+ for (const bytes of pending.values()) total += bytes;
27840
+ const newlyDroppedKeys = [];
27841
+ if (total > maxBytes) for (const [key, bytes] of pending) {
27842
+ if (total <= lowBytes) break;
27843
+ if (bytes === 0) continue;
27844
+ allDroppedKeys.add(key);
27845
+ newlyDroppedKeys.push(key);
27846
+ total -= bytes;
27847
+ }
27848
+ const replaced = replaceDroppedRequestImages(options, allDroppedKeys, images);
27849
+ return {
27850
+ options: replaced.options,
27851
+ remainingBytes: total,
27852
+ droppedCount: replaced.count,
27853
+ newlyDroppedKeys,
27854
+ allDroppedKeys
27855
+ };
27856
+ }
27857
+ /**
26904
27858
  * Whether a request still carries at least one image the mapper can send.
26905
27859
  *
26906
27860
  * A request whose images have all been replaced by placeholders can no longer
@@ -27462,11 +28416,26 @@ function effectiveContextWindow(tokenWindow, env = process.env) {
27462
28416
  /**
27463
28417
  * Base64 image payload one request on this route may carry.
27464
28418
  *
27465
- * Sized to leave room for the conversation inside the body ceiling above, which
27466
- * is why it is the smaller of the two. DSH_KIMI_CODE_MAX_IMAGE_BYTES overrides it
27467
- * under the same rules: ignored, never applied as a disabled guard.
28419
+ * Kimi's own CLI budgets request media at 20 MiB (REQUEST_MEDIA_BUDGET_BYTES),
28420
+ * and this route now speaks the same number. The previous 1.5 MB figure was
28421
+ * Kimi's TEXT-and-image body ceiling applied to the images alone, which threw
28422
+ * away pictures the service was perfectly willing to accept: a conversation
28423
+ * that carried three screenshots lost two of them to a limit that never
28424
+ * existed upstream.
28425
+ *
28426
+ * DSH_KIMI_CODE_MAX_IMAGE_BYTES overrides it under the same rules: ignored,
28427
+ * never applied as a disabled guard.
28428
+ */
28429
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES$1 = 20 * 1024 * 1024;
28430
+ /**
28431
+ * The mark the request is brought back under once it has passed the high one.
28432
+ *
28433
+ * Half the high mark, exactly as the official client does
28434
+ * (REQUEST_MEDIA_BUDGET_LOW_BYTES). The gap between the two is the slack that
28435
+ * keeps the number of retained images stable from turn to turn, and a stable
28436
+ * image count is what keeps the cached prefix valid.
27468
28437
  */
27469
- const DEFAULT_MAX_REQUEST_IMAGE_BYTES$1 = MAX_REQUEST_IMAGE_BYTES$1;
28438
+ const DEFAULT_LOW_REQUEST_IMAGE_BYTES = 10 * 1024 * 1024;
27470
28439
  /** Image budget this process enforces, honouring the deployment override. */
27471
28440
  function maxRequestImageBytes$1(env = process.env) {
27472
28441
  const raw = (env.DSH_KIMI_CODE_MAX_IMAGE_BYTES ?? "").trim();
@@ -27476,6 +28445,22 @@ function maxRequestImageBytes$1(env = process.env) {
27476
28445
  return Math.floor(parsed);
27477
28446
  }
27478
28447
  /**
28448
+ * The low-water mark this process enforces, honouring the deployment override.
28449
+ *
28450
+ * Half of whatever the deployment pinned, so an override moves both marks
28451
+ * together: a deployment that lowers the ceiling to 4 MB gets a 2 MB low mark
28452
+ * rather than the built-in 10 MB, which would let every request sit above the
28453
+ * mark it is supposed to recover to. A deployment that pins nothing gets
28454
+ * Kimi's own 10 MiB.
28455
+ */
28456
+ function lowRequestImageBytes(env = process.env) {
28457
+ const raw = (env.DSH_KIMI_CODE_MAX_IMAGE_BYTES ?? "").trim();
28458
+ if (raw === "") return DEFAULT_LOW_REQUEST_IMAGE_BYTES;
28459
+ const parsed = Number(raw);
28460
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LOW_REQUEST_IMAGE_BYTES;
28461
+ return Math.max(1, Math.floor(Math.floor(parsed) / 2));
28462
+ }
28463
+ /**
27479
28464
  * Which wire dialect a model id is served over.
27480
28465
  *
27481
28466
  * Always the OpenAI-compatible chat completions dialect: this line uses that one
@@ -27943,7 +28928,7 @@ async function persistRegion(region) {
27943
28928
  */
27944
28929
  const lastActiveBySession = /* @__PURE__ */ new Map();
27945
28930
  /** Bound on remembered sessions. */
27946
- const MAX_TRACKED_SESSIONS$1 = 256;
28931
+ const MAX_TRACKED_SESSIONS$2 = 256;
27947
28932
  /**
27948
28933
  * Note that a session just did something.
27949
28934
  *
@@ -27956,7 +28941,7 @@ function markSessionActive(sessionId, now = Date.now()) {
27956
28941
  const key = sessionId.trim();
27957
28942
  lastActiveBySession.delete(key);
27958
28943
  lastActiveBySession.set(key, now);
27959
- if (lastActiveBySession.size <= MAX_TRACKED_SESSIONS$1) return;
28944
+ if (lastActiveBySession.size <= MAX_TRACKED_SESSIONS$2) return;
27960
28945
  const oldest = lastActiveBySession.keys().next();
27961
28946
  if (oldest.done !== true) lastActiveBySession.delete(oldest.value);
27962
28947
  }
@@ -31190,6 +32175,110 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
31190
32175
  return body;
31191
32176
  }
31192
32177
  /**
32178
+ * How the side-channel title request opens its prompt.
32179
+ *
32180
+ * Matching the opening identifies the request even when the provenance stamp
32181
+ * is absent - a harness generation that does not stamp it still asks for the
32182
+ * title in exactly this wording.
32183
+ */
32184
+ const TITLE_REQUEST_PROMPT_PREFIX = "Generate the session title from this JSON array of human messages:";
32185
+ /**
32186
+ * How the side-channel title request opens its system prompt.
32187
+ *
32188
+ * Only a request that carries NO tools can be the title request: the real
32189
+ * conversation always carries the session's tool set, so the (absent) tool
32190
+ * list is what separates the two shapes.
32191
+ */
32192
+ const TITLE_REQUEST_SYSTEM_PREFIX = "Create a concise title for an AI coding-assistant session";
32193
+ /**
32194
+ * The first non-empty text a message body contributes.
32195
+ *
32196
+ * The same ordering {@link textOf} folds a body in, but it stops at the first
32197
+ * contribution instead of joining all of them: a caller that only needs to know
32198
+ * where the text STARTS must not pay for the whole conversation. The join in
32199
+ * {@link textOf} puts the blocks in order and the empty ones contribute
32200
+ * nothing, so the first non-empty contribution is where the joined text begins.
32201
+ *
32202
+ * @param content - The body of one message.
32203
+ * @returns The first non-empty text it contributes, or undefined.
32204
+ */
32205
+ function firstTextOf(content) {
32206
+ if (typeof content === "string") {
32207
+ const text = sanitizeText$7(content);
32208
+ return text === "" ? void 0 : text;
32209
+ }
32210
+ if (!Array.isArray(content)) return void 0;
32211
+ for (const block of content) {
32212
+ if (!isRecord$45(block)) continue;
32213
+ if (block.type === "text" && typeof block.text === "string") {
32214
+ const text = sanitizeText$7(block.text);
32215
+ if (text !== "") return text;
32216
+ continue;
32217
+ }
32218
+ if (block.type === "tool-result") {
32219
+ const nested = firstTextOf(block.content);
32220
+ if (nested !== void 0) return nested;
32221
+ }
32222
+ }
32223
+ }
32224
+ /**
32225
+ * The text the folded system prompt starts with.
32226
+ *
32227
+ * Same ordering as {@link leadingSystemText}: `options.system` leads, and the
32228
+ * system-role messages follow in history order. Only the first contribution
32229
+ * matters here, so the scan stops at it.
32230
+ *
32231
+ * @param options - The request being inspected.
32232
+ * @returns The leading text of the system prompt, or undefined when there is none.
32233
+ */
32234
+ function leadingSystemTextStart(options) {
32235
+ if (typeof options.system === "string" && options.system.trim() !== "") return options.system;
32236
+ for (const message of options.messages) {
32237
+ if (message.role !== "system") continue;
32238
+ if (messageToolsOf(message) !== void 0) continue;
32239
+ const text = firstTextOf(message.content);
32240
+ if (text !== void 0) return text;
32241
+ }
32242
+ }
32243
+ /**
32244
+ * Whether this request is the side-channel one that titles a session.
32245
+ *
32246
+ * A title request is asked on the same adapter, under the same session id, and
32247
+ * with the same credential as the conversation it titles - but it shares
32248
+ * nothing else with it. Left alone it would:
32249
+ *
32250
+ * - send the session's prompt cache key, so the service would route the title
32251
+ * prompt at the entry the conversation cached;
32252
+ * - overwrite the session's prefix snapshot, so the next real turn would be
32253
+ * told its system prompt or its tool list changed; and
32254
+ * - file its usage under the session, so the session's cache ratio would
32255
+ * describe a prompt the session never sent.
32256
+ *
32257
+ * The detection is deliberately structural rather than a flag on the options:
32258
+ * the caller is the harness, and the harness states a title request by its
32259
+ * CONTENT, not by anything this route could switch on.
32260
+ *
32261
+ * @param options - The request about to be mapped.
32262
+ * @returns Whether the request is the side-channel title request.
32263
+ */
32264
+ function isSideChannelTitleRequest(options) {
32265
+ if (options === void 0 || options === null) return false;
32266
+ const messages = Array.isArray(options.messages) ? options.messages : [];
32267
+ const tools = options.tools;
32268
+ const toolLess = tools === void 0 || tools.length === 0;
32269
+ for (const message of messages) {
32270
+ if (!isRecord$45(message)) continue;
32271
+ if (message.source?.kind === "dsh-session-title-llm") return true;
32272
+ const text = firstTextOf(message.content);
32273
+ if (toolLess && text !== void 0 && text.startsWith(TITLE_REQUEST_PROMPT_PREFIX)) return true;
32274
+ }
32275
+ if (toolLess) {
32276
+ const systemText = leadingSystemTextStart(options);
32277
+ if (systemText !== void 0 && systemText.startsWith(TITLE_REQUEST_SYSTEM_PREFIX)) return true;
32278
+ }
32279
+ return false;
32280
+ }
32281
+ /**
31193
32282
  * Stable identifier for the conversation this request belongs to.
31194
32283
  *
31195
32284
  * The key is derived from the session identifier alone — never from message
@@ -31202,6 +32291,7 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
31202
32291
  * all rather than a key that will drift.
31203
32292
  */
31204
32293
  function promptCacheKey(options) {
32294
+ if (isSideChannelTitleRequest(options)) return void 0;
31205
32295
  if (typeof options.sessionId === "string" && options.sessionId.trim() !== "") return `dsh-${options.sessionId.trim()}`;
31206
32296
  }
31207
32297
  function anthropicUserContent$2(message, images, toolCallIds) {
@@ -31344,7 +32434,9 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {
31344
32434
  }
31345
32435
  /** Build the body for whichever endpoint serves `wire`. */
31346
32436
  function buildRequest(options, wire, images = NO_RESOLVED_IMAGES$3, preserveThinking = preserveThinkingEnabled(), media = {}) {
31347
- recordDriftCause(trackPrefixStability(options), options.sessionId);
32437
+ if (isSideChannelTitleRequest(options)) return wire === "anthropic" ? buildAnthropicRequest(options, images, media) : buildOpenAIRequest(options, images, preserveThinking, media);
32438
+ const cause = trackPrefixStability(options);
32439
+ recordDriftCause(cause === "stable" && (pendingImageOffloads.get(cacheScopeKey(options.sessionId)) ?? 0) > 0 ? "image-offload" : cause, options.sessionId, options);
31348
32440
  return wire === "anthropic" ? buildAnthropicRequest(options, images, media) : buildOpenAIRequest(options, images, preserveThinking, media);
31349
32441
  }
31350
32442
  /**
@@ -31441,10 +32533,11 @@ function oversizedBodyMessage(bytes, limit, body, context) {
31441
32533
  const parts = requestBodyBreakdown(body);
31442
32534
  return `Kimi Code rejected the request before sending: the serialized body is ${bytes} bytes, above the ${limit}-byte limit this route enforces. Composition: ${`images ${parts.imageBytes} bytes, tool schemas ${parts.toolSchemaBytes} bytes, conversation text and system prompt ${parts.otherBytes} bytes`}. ${bodyFitRemedy(parts, context)}`;
31443
32535
  }
31444
- function createStreamState$5(wire, sessionId) {
32536
+ function createStreamState$5(wire, sessionId, sideChannel = false) {
31445
32537
  return {
31446
32538
  wire,
31447
32539
  sessionId: typeof sessionId === "string" && sessionId.trim() !== "" ? sessionId.trim() : void 0,
32540
+ sideChannel,
31448
32541
  blocks: [],
31449
32542
  current: null,
31450
32543
  reasoningRuns: /* @__PURE__ */ new Map(),
@@ -31871,7 +32964,7 @@ const UNSCOPED_CACHE_KEY = "";
31871
32964
  * count: eviction only begins once a host has served hundreds of them, and it
31872
32965
  * evicts the least recently written key, which is a stale session by then.
31873
32966
  */
31874
- const MAX_TRACKED_SESSIONS = 256;
32967
+ const MAX_TRACKED_SESSIONS$1 = 256;
31875
32968
  /**
31876
32969
  * Composite key separating one session's numbers from another's.
31877
32970
  *
@@ -31889,7 +32982,7 @@ function cacheScopeKey(sessionId, accountId) {
31889
32982
  function rememberScoped(store, key, value) {
31890
32983
  store.delete(key);
31891
32984
  store.set(key, value);
31892
- if (store.size <= MAX_TRACKED_SESSIONS) return;
32985
+ if (store.size <= MAX_TRACKED_SESSIONS$1) return;
31893
32986
  const oldest = store.keys().next();
31894
32987
  if (oldest.done !== true) store.delete(oldest.value);
31895
32988
  }
@@ -31904,19 +32997,31 @@ function rememberScoped(store, key, value) {
31904
32997
  */
31905
32998
  const prefixSnapshots = /* @__PURE__ */ new Map();
31906
32999
  /**
33000
+ * Hash the prefix-breaking inputs of one request.
33001
+ *
33002
+ * @param options - The request about to be built.
33003
+ * @returns The hashes of its system prompt and its tool list.
33004
+ */
33005
+ function prefixFingerprints(options) {
33006
+ const systemText = leadingSystemText$7(options) ?? "";
33007
+ const tools = options.tools ?? [];
33008
+ return {
33009
+ systemPromptHash: systemText === "" ? null : fingerprint(systemText),
33010
+ toolsHash: tools.length === 0 ? null : fingerprint(JSON.stringify(tools.map((tool) => [
33011
+ tool.name,
33012
+ tool.description ?? "",
33013
+ tool.parameters ?? {}
33014
+ ])))
33015
+ };
33016
+ }
33017
+ /**
31907
33018
  * Fingerprint the current request's stability inputs and diff them against the
31908
33019
  * last request. Called once per request build; the result is read back when the
31909
33020
  * stream reports usage so a low-or-zero cache read can be explained.
31910
33021
  */
31911
33022
  function trackPrefixStability(options) {
31912
- const systemText = leadingSystemText$7(options) ?? "";
31913
- const systemPromptHash = systemText === "" ? null : fingerprint(systemText);
31914
- const tools = options.tools ?? [];
31915
- const toolsHash = tools.length === 0 ? null : fingerprint(JSON.stringify(tools.map((tool) => [
31916
- tool.name,
31917
- tool.description ?? "",
31918
- tool.parameters ?? {}
31919
- ])));
33023
+ if (isSideChannelTitleRequest(options)) return "stable";
33024
+ const { systemPromptHash, toolsHash } = prefixFingerprints(options);
31920
33025
  const cacheKey = promptCacheKey(options) ?? null;
31921
33026
  const scope = cacheScopeKey(options.sessionId);
31922
33027
  const previous = prefixSnapshots.get(scope);
@@ -31934,8 +33039,16 @@ function trackPrefixStability(options) {
31934
33039
  }
31935
33040
  /** Drift cause recorded for each session. */
31936
33041
  const driftCauses = /* @__PURE__ */ new Map();
31937
- /** Record the cause computed for one in-flight request. */
31938
- function recordDriftCause(cause, sessionId) {
33042
+ /**
33043
+ * Record the cause computed for one in-flight request.
33044
+ *
33045
+ * The options are optional and only used to recognise a side-channel request:
33046
+ * a title request carries its own prefix, so attributing it to the session
33047
+ * would blame the session's next cache miss on the title request's system
33048
+ * prompt or on its (absent) tool list.
33049
+ */
33050
+ function recordDriftCause(cause, sessionId, options) {
33051
+ if (options !== void 0 && isSideChannelTitleRequest(options)) return;
31939
33052
  rememberScoped(driftCauses, cacheScopeKey(sessionId), cause);
31940
33053
  }
31941
33054
  /**
@@ -31948,6 +33061,94 @@ function recordDriftCause(cause, sessionId) {
31948
33061
  function getLastDriftCause(sessionId) {
31949
33062
  return driftCauses.get(cacheScopeKey(sessionId));
31950
33063
  }
33064
+ /**
33065
+ * Image identities this session has given back to the image budget.
33066
+ *
33067
+ * Scoped per session for the same reason the prefix snapshots are: DSH runs
33068
+ * several conversations — and subagents — at once, so one process-wide set
33069
+ * would let one conversation's omission replace another conversation's image.
33070
+ */
33071
+ const sessionDroppedImageKeys = /* @__PURE__ */ new Map();
33072
+ /**
33073
+ * What the most recent request of one session carried, in image bytes.
33074
+ */
33075
+ const sessionImageBudgetStats = /* @__PURE__ */ new Map();
33076
+ /**
33077
+ * Images the request CURRENTLY BEING BUILT has already dropped.
33078
+ *
33079
+ * Separate from {@link sessionDroppedImageKeys} because the two answer
33080
+ * different questions: the set is what the session remembers, and this counter
33081
+ * is what the request in flight did. The counter is what lets the drift
33082
+ * attribution name the offload, whichever order the build and the recording
33083
+ * happen in.
33084
+ */
33085
+ const pendingImageOffloads = /* @__PURE__ */ new Map();
33086
+ /**
33087
+ * The image identities this session has already dropped.
33088
+ *
33089
+ * `durableDropped` seeds the memory the first time a session is asked about,
33090
+ * so a session that ran before the restart keeps the omissions it made: the
33091
+ * budget is sticky, and a picture that was given back must stay given back.
33092
+ */
33093
+ function getSessionDroppedImageKeys(sessionId, durableDropped) {
33094
+ if (sessionId === void 0 || sessionId.trim() === "") return /* @__PURE__ */ new Set();
33095
+ const scope = cacheScopeKey(sessionId);
33096
+ const existing = sessionDroppedImageKeys.get(scope);
33097
+ if (existing !== void 0) return existing;
33098
+ const seeded = /* @__PURE__ */ new Set();
33099
+ for (const key of durableDropped ?? []) if (typeof key === "string" && key !== "") seeded.add(key);
33100
+ rememberScoped(sessionDroppedImageKeys, scope, seeded);
33101
+ return seeded;
33102
+ }
33103
+ /**
33104
+ * Note that the request being built has dropped images, before its body is built.
33105
+ *
33106
+ * The drift attribution reads this back when the prefix is fingerprinted, so
33107
+ * the order of the two does not matter: a request that drops an image is
33108
+ * attributed to the offload whether the body was built before or after the
33109
+ * drop was recorded.
33110
+ */
33111
+ function markPendingImageOffload(sessionId, newlyDroppedCount) {
33112
+ const scope = cacheScopeKey(sessionId);
33113
+ if (newlyDroppedCount <= 0) {
33114
+ pendingImageOffloads.delete(scope);
33115
+ return;
33116
+ }
33117
+ rememberScoped(pendingImageOffloads, scope, newlyDroppedCount);
33118
+ }
33119
+ /**
33120
+ * Record what one request's image budget did.
33121
+ *
33122
+ * The dropped identities are kept for the whole session: the budget is sticky,
33123
+ * so an image given back stays given back, and the next turn has to know
33124
+ * which ones those are.
33125
+ */
33126
+ function recordSessionImageBudget(sessionId, input, options) {
33127
+ const scope = cacheScopeKey(sessionId);
33128
+ const named = sessionId !== void 0 && sessionId.trim() !== "";
33129
+ const keys = named ? sessionDroppedImageKeys.get(scope) ?? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set();
33130
+ if (named) {
33131
+ for (const key of input.allDroppedKeys) if (typeof key === "string" && key !== "") keys.add(key);
33132
+ rememberScoped(sessionDroppedImageKeys, scope, keys);
33133
+ }
33134
+ rememberScoped(sessionImageBudgetStats, scope, {
33135
+ requestImageBytes: input.requestImageBytes,
33136
+ droppedImages: input.droppedImages,
33137
+ droppedKeys: named ? [...keys] : []
33138
+ });
33139
+ markPendingImageOffload(sessionId, input.newlyDroppedCount);
33140
+ if (input.newlyDroppedCount > 0 && getLastDriftCause(sessionId) === "stable") recordDriftCause("image-offload", sessionId, options);
33141
+ }
33142
+ /**
33143
+ * What one session's last request carried of images, when it is known.
33144
+ *
33145
+ * `undefined` for a session that has not sent a request, rather than a zeroed
33146
+ * answer: "no request yet" and "a request that carried no images" are
33147
+ * different facts.
33148
+ */
33149
+ function getSessionImageBudgetStats(sessionId) {
33150
+ return sessionImageBudgetStats.get(cacheScopeKey(sessionId));
33151
+ }
31951
33152
  /** Last lift per session. */
31952
33153
  const thinkingCoercions = /* @__PURE__ */ new Map();
31953
33154
  /**
@@ -32025,6 +33226,7 @@ const EMPTY_CACHE_STATS = {
32025
33226
  /** Record one request's usage into its own session's totals. */
32026
33227
  function recordCacheStats(state, sessionId, accountId) {
32027
33228
  if (!state.sawUsage) return;
33229
+ if (state.sideChannel) return;
32028
33230
  const scope = cacheScopeKey(sessionId ?? state.sessionId, accountId);
32029
33231
  const previous = cacheStatsByScope.get(scope) ?? EMPTY_CACHE_STATS;
32030
33232
  rememberScoped(cacheStatsByScope, scope, {
@@ -32326,6 +33528,9 @@ function normalizeRecord(value) {
32326
33528
  if (lastActiveAt === void 0 || updatedAt === void 0) return void 0;
32327
33529
  const drift = typeof value.lastDrift === "string" && value.lastDrift !== "" ? value.lastDrift : void 0;
32328
33530
  const warmupStreak = count$1(value.warmupConsecutiveHits);
33531
+ const budgetDropped = droppedImageKeys(value.budgetDropped);
33532
+ const lastRequestImageBytes = nonNegative(value.lastRequestImageBytes);
33533
+ const lastDroppedImages = nonNegative(value.lastDroppedImages);
32329
33534
  return {
32330
33535
  lastActiveAt,
32331
33536
  updatedAt,
@@ -32338,10 +33543,46 @@ function normalizeRecord(value) {
32338
33543
  cacheWrite5mTokens: count$1(value.cacheWrite5mTokens),
32339
33544
  cacheWrite1hTokens: count$1(value.cacheWrite1hTokens),
32340
33545
  ...drift === void 0 ? {} : { lastDrift: drift },
32341
- ...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {}
33546
+ ...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {},
33547
+ ...budgetDropped.length > 0 ? { budgetDropped } : {},
33548
+ ...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
33549
+ ...lastDroppedImages === void 0 ? {} : { lastDroppedImages }
32342
33550
  };
32343
33551
  }
32344
33552
  /**
33553
+ * How many dropped image identities one record keeps.
33554
+ *
33555
+ * A session that pastes hundreds of pictures would otherwise grow this
33556
+ * document without bound, and it is a diagnostic file: the identities that
33557
+ * matter are the ones a later turn still has to replace.
33558
+ */
33559
+ const MAX_BUDGET_DROPPED_KEYS = 512;
33560
+ /**
33561
+ * The dropped image identities a stored record carries.
33562
+ *
33563
+ * Only non-empty strings, deduplicated and capped: the file is written from
33564
+ * whatever the process happened to hold, and a malformed entry must not take
33565
+ * the record down with it.
33566
+ */
33567
+ function droppedImageKeys(value) {
33568
+ if (!Array.isArray(value)) return [];
33569
+ const keys = [];
33570
+ const seen = /* @__PURE__ */ new Set();
33571
+ for (const entry of value) {
33572
+ if (typeof entry !== "string" || entry === "") continue;
33573
+ if (seen.has(entry)) continue;
33574
+ seen.add(entry);
33575
+ keys.push(entry);
33576
+ if (keys.length >= MAX_BUDGET_DROPPED_KEYS) break;
33577
+ }
33578
+ return keys;
33579
+ }
33580
+ /** A stored byte or image count, or undefined when the field is absent or absurd. */
33581
+ function nonNegative(value) {
33582
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
33583
+ return Math.floor(value);
33584
+ }
33585
+ /**
32345
33586
  * Combine process memory with the durable record into the hint's two inputs.
32346
33587
  *
32347
33588
  * The process supplies the idle baseline whenever it has served the conversation,
@@ -33451,6 +34692,155 @@ function toolUpdateMode$1(modelId, catalog) {
33451
34692
  ].includes(raw)) return void 0;
33452
34693
  return "in-history";
33453
34694
  }
34695
+ /**
34696
+ * How long a cold-start follower waits for the leader warming the same prefix.
34697
+ *
34698
+ * The leader only has to reach the response headers: past that the service is
34699
+ * already processing the prefix, and a follower that waited out the whole
34700
+ * budget would have lost more than the shared warm-up was worth.
34701
+ */
34702
+ const COLD_START_WARMUP_WAIT_MS = 2500;
34703
+ /** How many prefix groups are remembered as warm. */
34704
+ const MAX_WARM_PREFIX_GROUPS = 256;
34705
+ /**
34706
+ * When each prefix group was last warmed.
34707
+ *
34708
+ * An entry older than the cache lifetime is treated as cold, because the
34709
+ * service's own entry is gone by then too. Bounded, so a long-lived process
34710
+ * cannot accumulate one entry per distinct prefix forever.
34711
+ */
34712
+ const warmPrefixTimestamps = /* @__PURE__ */ new Map();
34713
+ /**
34714
+ * The request currently warming each prefix group.
34715
+ *
34716
+ * The promise is resolved when that leader reaches the response headers, which
34717
+ * is what releases the followers waiting behind it.
34718
+ */
34719
+ const inFlightColdLeaders = /* @__PURE__ */ new Map();
34720
+ /** When each leader started, so a hung leader does not own its group forever. */
34721
+ const inFlightColdLeaderStartedAt = /* @__PURE__ */ new Map();
34722
+ /**
34723
+ * The resolver belonging to each in-flight leader.
34724
+ *
34725
+ * Kept beside the promise it settles, because a promise cannot be resolved
34726
+ * from the outside.
34727
+ */
34728
+ const coldLeaderReleases = /* @__PURE__ */ new Map();
34729
+ /** The gate of a request that neither waits for nor leads a cold start. */
34730
+ const NO_COLD_START_GATE = {
34731
+ isLeader: false,
34732
+ release: () => void 0,
34733
+ markWarm: () => void 0
34734
+ };
34735
+ /**
34736
+ * The group of requests that address the same cached prefix.
34737
+ *
34738
+ * The service derives its cache from the request content, so two requests with
34739
+ * the same model, the same system prompt and the same tool list hit the same
34740
+ * entry. The region is part of the key because each region is a separate
34741
+ * service with a cache of its own.
34742
+ *
34743
+ * A request with no system prompt has no prefix worth warming, and gets no
34744
+ * group at all.
34745
+ *
34746
+ * @param region - The region the request will be answered from.
34747
+ * @param options - The request about to be sent.
34748
+ * @returns The group key, or undefined when the request has no prefix.
34749
+ */
34750
+ function coldStartGroupKey(region, options) {
34751
+ const { systemPromptHash, toolsHash } = prefixFingerprints(options);
34752
+ if (systemPromptHash === null) return void 0;
34753
+ return `${region}:${options.model}:${systemPromptHash}:${toolsHash ?? "none"}`;
34754
+ }
34755
+ /**
34756
+ * Remember that a prefix group is warm.
34757
+ *
34758
+ * @param groupKey - The group that was just warmed.
34759
+ */
34760
+ function rememberWarmPrefix(groupKey) {
34761
+ warmPrefixTimestamps.delete(groupKey);
34762
+ warmPrefixTimestamps.set(groupKey, Date.now());
34763
+ while (warmPrefixTimestamps.size > MAX_WARM_PREFIX_GROUPS) {
34764
+ const oldest = warmPrefixTimestamps.keys().next();
34765
+ if (oldest.done === true) break;
34766
+ warmPrefixTimestamps.delete(oldest.value);
34767
+ }
34768
+ }
34769
+ /**
34770
+ * Wait for the leader of a prefix group, for the wait budget, or for an abort.
34771
+ *
34772
+ * The timer and the abort listener are removed whichever way the race ends, so
34773
+ * a follower that proceeds leaves neither behind.
34774
+ *
34775
+ * @param leader - The promise the in-flight leader settles.
34776
+ * @param signal - The caller's abort signal; an abort ends the wait.
34777
+ */
34778
+ function raceColdStartLeader(leader, signal) {
34779
+ const cleanups = [];
34780
+ const budget = new Promise((resolve) => {
34781
+ const timer = setTimeout(() => resolve(), COLD_START_WARMUP_WAIT_MS);
34782
+ cleanups.push(() => clearTimeout(timer));
34783
+ });
34784
+ const aborted = new Promise((resolve) => {
34785
+ if (signal.aborted) {
34786
+ resolve();
34787
+ return;
34788
+ }
34789
+ const onAbort = () => resolve();
34790
+ signal.addEventListener("abort", onAbort, { once: true });
34791
+ cleanups.push(() => signal.removeEventListener("abort", onAbort));
34792
+ });
34793
+ return Promise.race([
34794
+ leader,
34795
+ budget,
34796
+ aborted
34797
+ ]).finally(() => {
34798
+ for (const cleanup of cleanups) cleanup();
34799
+ });
34800
+ }
34801
+ /**
34802
+ * Join the cold-start singleflight for one prefix group.
34803
+ *
34804
+ * A warm group returns at once. A cold group that already has a leader waits
34805
+ * for that leader — but never longer than {@link COLD_START_WARMUP_WAIT_MS}.
34806
+ * A cold group with no leader becomes the leader itself.
34807
+ *
34808
+ * @param groupKey - The prefix group this request belongs to.
34809
+ * @param signal - The caller's abort signal; an abort ends the wait.
34810
+ * @returns The gate this request holds.
34811
+ */
34812
+ async function acquireColdStartWarmup(groupKey, signal) {
34813
+ const warmedAt = warmPrefixTimestamps.get(groupKey);
34814
+ if (warmedAt !== void 0 && Date.now() - warmedAt < 15e5) return NO_COLD_START_GATE;
34815
+ if (inFlightColdLeaders.get(groupKey) !== void 0) {
34816
+ const startedAt = inFlightColdLeaderStartedAt.get(groupKey);
34817
+ if (startedAt !== void 0 && Date.now() - startedAt >= 2500) coldLeaderReleases.get(groupKey)?.();
34818
+ }
34819
+ const leader = inFlightColdLeaders.get(groupKey);
34820
+ if (leader !== void 0) {
34821
+ await raceColdStartLeader(leader, signal);
34822
+ return NO_COLD_START_GATE;
34823
+ }
34824
+ let resolveLeader;
34825
+ const promise = new Promise((resolve) => {
34826
+ resolveLeader = resolve;
34827
+ });
34828
+ inFlightColdLeaders.set(groupKey, promise);
34829
+ inFlightColdLeaderStartedAt.set(groupKey, Date.now());
34830
+ const release = () => {
34831
+ resolveLeader();
34832
+ if (coldLeaderReleases.get(groupKey) !== release) return;
34833
+ coldLeaderReleases.delete(groupKey);
34834
+ inFlightColdLeaders.delete(groupKey);
34835
+ inFlightColdLeaderStartedAt.delete(groupKey);
34836
+ };
34837
+ coldLeaderReleases.set(groupKey, release);
34838
+ return {
34839
+ isLeader: true,
34840
+ release,
34841
+ markWarm: () => rememberWarmPrefix(groupKey)
34842
+ };
34843
+ }
33454
34844
  /** Sessions already told that their prefix broke, so the log stays readable. */
33455
34845
  const driftWarned = /* @__PURE__ */ new Set();
33456
34846
  /**
@@ -33476,6 +34866,13 @@ function noteSessionCacheState(sessionId, state) {
33476
34866
  const lastDrift = getLastDriftCause(sessionId);
33477
34867
  reportDriftCause(sessionId, lastDrift);
33478
34868
  const now = Date.now();
34869
+ const imageBudget = getSessionImageBudgetStats(sessionId);
34870
+ const priorRecord = sessionCacheState.read(sessionId);
34871
+ const budgetDropped = imageBudget !== void 0 && imageBudget.droppedKeys.length > 0 ? imageBudget.droppedKeys : priorRecord?.budgetDropped;
34872
+ const liveBytes = imageBudget?.requestImageBytes ?? 0;
34873
+ const liveDropped = imageBudget?.droppedImages ?? 0;
34874
+ const lastRequestImageBytes = liveBytes > 0 ? liveBytes : priorRecord?.lastRequestImageBytes;
34875
+ const lastDroppedImages = liveDropped > 0 ? liveDropped : priorRecord?.lastDroppedImages;
33479
34876
  sessionCacheState.write(sessionId, {
33480
34877
  lastActiveAt: now,
33481
34878
  totalTokens: state.cacheReadTokens + state.inputTokens,
@@ -33487,6 +34884,9 @@ function noteSessionCacheState(sessionId, state) {
33487
34884
  cacheWrite5mTokens: stats.cacheWrite5mTokens,
33488
34885
  cacheWrite1hTokens: stats.cacheWrite1hTokens,
33489
34886
  ...lastDrift === void 0 ? {} : { lastDrift },
34887
+ ...budgetDropped === void 0 ? {} : { budgetDropped },
34888
+ ...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
34889
+ ...lastDroppedImages === void 0 ? {} : { lastDroppedImages },
33490
34890
  updatedAt: now
33491
34891
  });
33492
34892
  const warmupState = recordCacheWarmupUsage({
@@ -33714,8 +35114,12 @@ var KimiCodeAdapter = class extends LlmAdapter {
33714
35114
  const wire = wireForCatalogEntry(options.model, catalog);
33715
35115
  const entry = catalog.find((model) => model.id === options.model);
33716
35116
  const normalized = normalizeGenerateOptions(options);
35117
+ const sideChannel = isSideChannelTitleRequest(normalized);
33717
35118
  const images = await resolveRequestImages$1(normalized, this.options.attachments, signal, KIMI_CODE_REQUEST_IMAGE_MAX_EDGE);
33718
- const imageTrimmed = offloadOldestRequestImages$1(normalized, maxRequestImageBytes$1(), images);
35119
+ const priorDroppedKeys = getSessionDroppedImageKeys(options.sessionId, options.sessionId === void 0 ? void 0 : sessionCacheState.read(options.sessionId)?.budgetDropped);
35120
+ const stickyOffload = offloadRequestImagesWithStickyBudget(normalized, maxRequestImageBytes$1(), lowRequestImageBytes(), images, priorDroppedKeys);
35121
+ const imageTrimmed = stickyOffload.options;
35122
+ if (!sideChannel) markPendingImageOffload(options.sessionId, stickyOffload.newlyDroppedKeys.length);
33719
35123
  const gatedEffort = coerceThinkingEffort(options.model, mapReasoningEffort(options.reasoningEffort === void 0 || options.reasoningEffort === null ? void 0 : String(options.reasoningEffort)));
33720
35124
  if (gatedEffort.coercedFrom !== null) recordThinkingCoercion({
33721
35125
  model: options.model,
@@ -33891,7 +35295,22 @@ var KimiCodeAdapter = class extends LlmAdapter {
33891
35295
  throw new LlmError(`Kimi Code request failed: ${error instanceof Error ? error.message : String(error)}`, "TRANSPORT", { cause: error });
33892
35296
  }
33893
35297
  };
33894
- response = await post(body);
35298
+ const coldGroupKey = sideChannel ? void 0 : coldStartGroupKey(region, requestOptions);
35299
+ const coldGate = coldGroupKey === void 0 ? void 0 : await acquireColdStartWarmup(coldGroupKey, signal);
35300
+ let coldGateSettled = false;
35301
+ const settleColdGate = (warm) => {
35302
+ if (coldGateSettled) return;
35303
+ coldGateSettled = true;
35304
+ coldGate?.release();
35305
+ if (warm) coldGate?.markWarm();
35306
+ };
35307
+ response = await post(body).then((value) => {
35308
+ settleColdGate(value.ok);
35309
+ return value;
35310
+ }, (error) => {
35311
+ settleColdGate(false);
35312
+ throw error;
35313
+ });
33895
35314
  let detail = response.ok ? "" : (await response.text().catch(() => "")).slice(0, 2e3);
33896
35315
  if (response.status === 404 && uploads !== void 0 && uploads.cited.length > 0 && isMissingResourceBody(detail)) {
33897
35316
  const uploader = this.mediaFiles();
@@ -33935,12 +35354,25 @@ var KimiCodeAdapter = class extends LlmAdapter {
33935
35354
  ...after === void 0 ? {} : { providerRetryAfterMs: after }
33936
35355
  });
33937
35356
  }
35357
+ if (measured !== void 0 && !sideChannel) {
35358
+ const requestImageBytes = requestImagePayloadLengths(measured.options, images).reduce((sum, bytes) => sum + bytes, 0);
35359
+ const trimmedKeys = new Set(collectRemainingImageKeys(imageTrimmed, images));
35360
+ const measuredKeys = new Set(collectRemainingImageKeys(measured.options, images));
35361
+ const bodyFitDroppedKeys = [...trimmedKeys].filter((key) => !measuredKeys.has(key));
35362
+ const allDroppedKeys = /* @__PURE__ */ new Set([...stickyOffload.allDroppedKeys, ...bodyFitDroppedKeys]);
35363
+ recordSessionImageBudget(options.sessionId, {
35364
+ allDroppedKeys,
35365
+ newlyDroppedCount: stickyOffload.newlyDroppedKeys.length + bodyFitDroppedKeys.length,
35366
+ requestImageBytes,
35367
+ droppedImages: stickyOffload.droppedCount + bodyFitDroppedKeys.length
35368
+ }, requestOptions);
35369
+ }
33938
35370
  if (response.body === null) throw new LlmError("Kimi Code returned an empty response body", "PROVIDER_ERROR");
33939
35371
  const reader = response.body.getReader();
33940
35372
  const decoder = new TextDecoder();
33941
- const state = createStreamState$5(wire, requestOptions.sessionId);
35373
+ const state = createStreamState$5(wire, requestOptions.sessionId, sideChannel);
33942
35374
  applyCacheWriteTiers(state, response.headers);
33943
- markSessionActive(requestOptions.sessionId);
35375
+ if (!sideChannel) markSessionActive(requestOptions.sessionId);
33944
35376
  let buffer = "";
33945
35377
  /**
33946
35378
  * Set the moment ANY chunk reaches the caller, and never cleared.
@@ -33986,7 +35418,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
33986
35418
  throw streamFailure(error, signal);
33987
35419
  }
33988
35420
  } finally {
33989
- if (state.sawUsage) noteSessionCacheState(requestOptions.sessionId ?? state.sessionId, state);
35421
+ if (state.sawUsage && !sideChannel) noteSessionCacheState(requestOptions.sessionId ?? state.sessionId, state);
33990
35422
  reader.cancel().catch(() => void 0);
33991
35423
  }
33992
35424
  }
@@ -34505,6 +35937,7 @@ function cacheStatsOrNull(sessionId) {
34505
35937
  ...stats,
34506
35938
  ...lastDrift === void 0 ? {} : { lastDrift },
34507
35939
  ...warmup === void 0 ? {} : { warmup },
35940
+ imageBudget: imageBudgetOf(sessionId),
34508
35941
  cacheWriteTokens: stats.cacheWriteTokens,
34509
35942
  cacheWrite5mTokens: stats.cacheWrite5mTokens,
34510
35943
  cacheWrite1hTokens: stats.cacheWrite1hTokens
@@ -34557,7 +35990,28 @@ function durableCacheStats(sessionId) {
34557
35990
  cacheWrite5mTokens: durable.cacheWrite5mTokens,
34558
35991
  cacheWrite1hTokens: durable.cacheWrite1hTokens,
34559
35992
  ...lastDrift === void 0 ? {} : { lastDrift },
34560
- ...warmup === void 0 ? {} : { warmup }
35993
+ ...warmup === void 0 ? {} : { warmup },
35994
+ imageBudget: imageBudgetOf(sessionId)
35995
+ };
35996
+ }
35997
+ /**
35998
+ * The image budget this route enforces and what the last turn of one session
35999
+ * spent of it.
36000
+ *
36001
+ * The live numbers win over the durable ones, because the durable record
36002
+ * describes the last run of this machine and the live numbers describe the
36003
+ * request that actually went out. The ceilings are read from the environment
36004
+ * every time, so a deployment that pinned a different budget sees its own
36005
+ * number rather than the built-in one.
36006
+ */
36007
+ function imageBudgetOf(sessionId) {
36008
+ const live = getSessionImageBudgetStats(sessionId);
36009
+ const durable = sessionCacheState.read(sessionId);
36010
+ return {
36011
+ budgetBytes: maxRequestImageBytes$1(),
36012
+ lowBudgetBytes: lowRequestImageBytes(),
36013
+ requestImageBytes: live?.requestImageBytes ?? durable?.lastRequestImageBytes ?? 0,
36014
+ droppedImages: live?.droppedImages ?? durable?.lastDroppedImages ?? 0
34561
36015
  };
34562
36016
  }
34563
36017
  /**
@@ -34574,7 +36028,8 @@ function driftCauseOf(value) {
34574
36028
  case "system-prompt":
34575
36029
  case "tools":
34576
36030
  case "cache-key":
34577
- case "cold-key": return value;
36031
+ case "cold-key":
36032
+ case "image-offload": return value;
34578
36033
  default: return;
34579
36034
  }
34580
36035
  }
@@ -43711,7 +45166,7 @@ function ms(value) {
43711
45166
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
43712
45167
  }
43713
45168
  /** A non-empty string, or undefined. */
43714
- function text$2(value) {
45169
+ function text$3(value) {
43715
45170
  return typeof value === "string" && value !== "" ? value : void 0;
43716
45171
  }
43717
45172
  function withTimeout$6(signal, ms_) {
@@ -43806,9 +45261,9 @@ async function callAcrossHosts(input) {
43806
45261
  function parseResetCard(raw) {
43807
45262
  const row = asRecord$9(raw);
43808
45263
  if (row === void 0) return void 0;
43809
- const cardId = text$2(row.card_id);
43810
- const sourceType = text$2(row.source_type);
43811
- const outBizNo = text$2(row.out_biz_no);
45264
+ const cardId = text$3(row.card_id);
45265
+ const sourceType = text$3(row.source_type);
45266
+ const outBizNo = text$3(row.out_biz_no);
43812
45267
  const expiresAtMs = ms(row.expire_time);
43813
45268
  const createdAtMs = ms(row.create_time);
43814
45269
  if (cardId === void 0 || sourceType === void 0 || outBizNo === void 0) return void 0;
@@ -43831,8 +45286,8 @@ function parseResetCard(raw) {
43831
45286
  function parseResetCardFlow(raw) {
43832
45287
  const row = asRecord$9(raw);
43833
45288
  if (row === void 0) return void 0;
43834
- const cardId = text$2(row.card_id);
43835
- const eventType = text$2(row.event_type);
45289
+ const cardId = text$3(row.card_id);
45290
+ const eventType = text$3(row.event_type);
43836
45291
  const createdAtMs = ms(row.create_time);
43837
45292
  if (cardId === void 0 || createdAtMs === void 0) return void 0;
43838
45293
  if (eventType !== "GRANT" && eventType !== "USE") return void 0;
@@ -43930,7 +45385,7 @@ async function useResetCard(credentials, card, options = {}) {
43930
45385
  }),
43931
45386
  options
43932
45387
  });
43933
- const outcome = text$2(asRecord$9(payload)?.outcome);
45388
+ const outcome = text$3(asRecord$9(payload)?.outcome);
43934
45389
  if (outcome === void 0 || !RESET_OUTCOMES.has(outcome)) throw new MinimaxCodeResetCardError("reset-card use answered without a usable outcome", null, readStatusCode(payload));
43935
45390
  return outcome;
43936
45391
  }
@@ -68764,7 +70219,7 @@ function createClinePassStore({ historyLimit = 100 } = {}) {
68764
70219
  *
68765
70220
  * @module dsh-chatgpt-subscription/cline-pass-tools
68766
70221
  */
68767
- function text$1(value) {
70222
+ function text$2(value) {
68768
70223
  return [{
68769
70224
  type: "text",
68770
70225
  text: value
@@ -69038,7 +70493,7 @@ function registerClinePassTools(ctx, options) {
69038
70493
  }
69039
70494
  }
69040
70495
  },
69041
- render: (_args, value) => text$1([
70496
+ render: (_args, value) => text$2([
69042
70497
  `${value.displayName} provider "cline-pass" @ ${value.baseURL}`,
69043
70498
  `route registered=${value.routeRegistered}`,
69044
70499
  `accountMode=${value.accountMode}`,
@@ -69132,7 +70587,7 @@ function registerClinePassTools(ctx, options) {
69132
70587
  if (model.lastProvider !== "") lines.push(` last=${model.lastProvider} (${model.lastMs}ms)`);
69133
70588
  return lines.join("\n");
69134
70589
  });
69135
- return text$1([
70590
+ return text$2([
69136
70591
  `${value.models.length} Cline Pass model(s); official catalog=${value.catalogCount}${value.catalogSources.length === 0 ? "" : ` via ${value.catalogSources.join(",")}`}`,
69137
70592
  ...value.added.length === 0 ? [] : [`newly adopted: ${value.added.join(", ")}`],
69138
70593
  ...blocks
@@ -69269,8 +70724,8 @@ function registerClinePassTools(ctx, options) {
69269
70724
  }
69270
70725
  },
69271
70726
  render: (_args, value) => {
69272
- if (!value.ok) return text$1(`probe ${value.model} failed: ${value.error}`);
69273
- return text$1([
70727
+ if (!value.ok) return text$2(`probe ${value.model} failed: ${value.error}`);
70728
+ return text$2([
69274
70729
  `probe ${value.model}: ok in ${value.ms}ms`,
69275
70730
  `pipeline=${value.pipeline} pinnable=${value.pinnable} canonicalSlug=${value.canonicalSlug || "-"} lastProvider=${value.lastProvider || "-"}`,
69276
70731
  value.upstreams.length === 0 ? "upstreams: (none discovered)" : `upstreams (${value.upstreams.length}): ${value.upstreams.join(", ")}`,
@@ -69357,8 +70812,8 @@ function registerClinePassTools(ctx, options) {
69357
70812
  }
69358
70813
  },
69359
70814
  render: (_args, value) => {
69360
- if (!value.ok) return text$1(`validate ${value.model} failed: ${value.error}`);
69361
- return text$1([
70815
+ if (!value.ok) return text$2(`validate ${value.model} failed: ${value.error}`);
70816
+ return text$2([
69362
70817
  `validate ${value.model}: ${value.results.length} upstream(s)`,
69363
70818
  `available=${value.summary.ok} rate-limited=${value.summary.limited} not-pinnable=${value.summary.bad} auth-failed=${value.summary.auth} unknown=${value.summary.unknown}`,
69364
70819
  ...value.results.map((entry) => `${entry.upstream}: ${statusLabel(entry.status)} (${entry.ms}ms)${entry.note === "" ? "" : ` — ${entry.note}`}`)
@@ -69478,7 +70933,7 @@ function registerClinePassTools(ctx, options) {
69478
70933
  },
69479
70934
  render: (_args, value) => {
69480
70935
  const trace = value.trace.map((entry) => ` ${entry.upstream} -> HTTP ${entry.status} in ${entry.ms}ms${entry.note === "" ? "" : ` — ${entry.note}`}`);
69481
- return text$1([
70936
+ return text$2([
69482
70937
  value.ok ? `test ${value.model}: ok in ${value.ms}ms via ${value.actual || "(unknown)"}` : `test ${value.model}: FAILED — ${value.error}`,
69483
70938
  ...value.warnings.map((warning) => `⚠ ${warning}`),
69484
70939
  trace.length === 0 ? "" : `attempts (${trace.length}):`,
@@ -69598,7 +71053,7 @@ function registerClinePassTools(ctx, options) {
69598
71053
  }
69599
71054
  }
69600
71055
  },
69601
- render: (_args, value) => text$1([
71056
+ render: (_args, value) => text$2([
69602
71057
  `pinned ${value.model}`,
69603
71058
  `upstreams: ${value.pinned.length === 0 ? "(auto)" : value.pinned.join(" > ")}`,
69604
71059
  `exclude: ${value.excluded.length === 0 ? "(none)" : value.excluded.join(", ")}`,
@@ -69708,7 +71163,7 @@ function registerClinePassTools(ctx, options) {
69708
71163
  }
69709
71164
  }
69710
71165
  },
69711
- render: (_args, value) => text$1([
71166
+ render: (_args, value) => text$2([
69712
71167
  `accounts.${value.action}: ${value.ok ? "ok" : "FAILED"}`,
69713
71168
  value.note === "" ? "" : `note: ${value.note}`,
69714
71169
  ...value.accounts.map((account) => ` ${account.alias} (${account.id})${account.enabled ? "" : " (disabled)"}${account.authStatus ? ` [${account.authStatus}]` : ""}`)
@@ -69843,8 +71298,8 @@ function registerClinePassTools(ctx, options) {
69843
71298
  }
69844
71299
  },
69845
71300
  render: (_args, value) => {
69846
- if (value.entries.length === 0) return text$1("No Cline Pass model calls recorded yet in this process.");
69847
- return text$1([`last ${value.entries.length} of ${value.total} call(s):`, ...value.entries.map((entry) => `${entry.ts === 0 ? "-" : new Date(entry.ts).toISOString()} ${entry.model} -> ${entry.provider || "(auto)"} ${entry.ttft > 0 ? `${entry.ttft}ms` : "—"}/${entry.ms}ms account=${entry.account || "-"}${entry.error === "" ? "" : ` error=${entry.error}`}`)].join("\n"));
71301
+ if (value.entries.length === 0) return text$2("No Cline Pass model calls recorded yet in this process.");
71302
+ return text$2([`last ${value.entries.length} of ${value.total} call(s):`, ...value.entries.map((entry) => `${entry.ts === 0 ? "-" : new Date(entry.ts).toISOString()} ${entry.model} -> ${entry.provider || "(auto)"} ${entry.ttft > 0 ? `${entry.ttft}ms` : "—"}/${entry.ms}ms account=${entry.account || "-"}${entry.error === "" ? "" : ` error=${entry.error}`}`)].join("\n"));
69848
71303
  }
69849
71304
  },
69850
71305
  async execute(args) {
@@ -80176,6 +81631,124 @@ function subagentModelSelectionPreference(settings) {
80176
81631
  }
80177
81632
  }
80178
81633
  /**
81634
+ * Main-model route per root Session, locked the first time it is observed.
81635
+ *
81636
+ * Without this, a mid-session model switch would silently re-point a Session's
81637
+ * child allowlist: the guard would start answering with a different main model's
81638
+ * routes, so the routes advertised in the prompt change, the prefix cache is
81639
+ * invalidated, and a model can be handed a list its own earlier turn contradicted.
81640
+ * Locking the FIRST resolved route keeps one Session's delegation policy stable
81641
+ * for its whole life, which is the same promise the durable policy event makes.
81642
+ *
81643
+ * Keyed weakly by the root Session object, so a closed Session's entry goes with
81644
+ * it. A Session object that is not an object (a minimal fixture) is simply not
81645
+ * locked.
81646
+ */
81647
+ const lockedRootMainRoutes = /* @__PURE__ */ new WeakMap();
81648
+ /**
81649
+ * Authorized child routes per root Session, locked the first time they are used.
81650
+ *
81651
+ * Locking the main ROUTE is not enough on its own: the route is only the key
81652
+ * into `byMainModel`, and the list it resolves to is read from the live
81653
+ * preference on every step. A Settings edit made at step 5 of a running session
81654
+ * would therefore swap the list under it — the prompt-assembly hook rewrites the
81655
+ * `subagent` tool description and the `tools:sdk` section from that list, so the
81656
+ * next request would carry a different prefix, drop the provider's prefix cache,
81657
+ * and contradict the Settings card's promise that a routing change applies to
81658
+ * new sessions only. The first resolved list is frozen here for the same reason
81659
+ * the durable policy event is a snapshot: one Session is governed by one policy.
81660
+ */
81661
+ const lockedRootAllowedRoutes = /* @__PURE__ */ new WeakMap();
81662
+ /**
81663
+ * Narrow one candidate route pair into an exact route.
81664
+ * @param provider - Candidate provider id.
81665
+ * @param model - Candidate model id.
81666
+ * @returns the route, or undefined unless both halves are non-empty strings.
81667
+ */
81668
+ function routeOf(provider, model) {
81669
+ const providerId = asString$3(provider);
81670
+ const modelId = asString$3(model);
81671
+ if (providerId === void 0 || modelId === void 0) return void 0;
81672
+ if (providerId.length === 0 || modelId.length === 0) return void 0;
81673
+ return {
81674
+ provider: providerId,
81675
+ model: modelId
81676
+ };
81677
+ }
81678
+ /**
81679
+ * Read the route a Session's next request uses, when its header names one.
81680
+ * @param session - Session whose request header is read.
81681
+ * @returns the exact route, or undefined when the header is absent or silent.
81682
+ */
81683
+ function headerRouteOf(session) {
81684
+ let header;
81685
+ try {
81686
+ header = session?.requestHeader?.();
81687
+ } catch {
81688
+ return;
81689
+ }
81690
+ return routeOf(header?.config?.provider, header?.config?.model);
81691
+ }
81692
+ /**
81693
+ * Resolve the top-level Session of one agent's lineage, together with the route
81694
+ * its main model runs on.
81695
+ *
81696
+ * The main model is a property of the ROOT Session: a child subagent's own route
81697
+ * is the child's, and reading it would authorize children by what a child chose
81698
+ * rather than by what the user's main agent is. Walking up through
81699
+ * `header.parentSession` finds that root, and the root's route is read from its
81700
+ * own header (or, for an agent that IS the root, from the live model accessor and
81701
+ * then the delegation hint, which is what an unstarted Session has).
81702
+ *
81703
+ * Once resolved for a Session that has already sent a request, the route is
81704
+ * locked in {@link lockedRootMainRoutes}, so the answer never drifts mid-session.
81705
+ * @param agent - Calling agent.
81706
+ * @param sessions - Session registry used for ancestor lookup.
81707
+ * @param modelSelectionHint - Route fields the caller already resolved.
81708
+ * @param lockSnapshot - Lock even a route that has no request header yet; the
81709
+ * prompt-assembly, post-execute and denial paths pass true, so a Session's
81710
+ * first turn fixes its policy for the rest of its life.
81711
+ * @returns the root Session (when one exists) and the main route (when known).
81712
+ */
81713
+ function resolveRootSessionAndMainRoute(agent, sessions, modelSelectionHint, lockSnapshot = false) {
81714
+ let rootSession = agent?.session;
81715
+ for (let depth = 0; depth < 8 && rootSession !== void 0; depth += 1) {
81716
+ const header = rootSession.header;
81717
+ if (header?.origin !== "subagent") break;
81718
+ const parentId = asString$3(header.parentSession);
81719
+ if (parentId === void 0) break;
81720
+ const parent = sessions.get(parentId);
81721
+ if (parent === void 0) break;
81722
+ rootSession = parent;
81723
+ }
81724
+ if (rootSession === void 0) return { mainRoute: routeOf(modelSelectionHint?.provider, modelSelectionHint?.model) };
81725
+ const lockable = typeof rootSession === "object" && rootSession !== null;
81726
+ const locked = lockable ? lockedRootMainRoutes.get(rootSession) : void 0;
81727
+ if (locked !== void 0) return {
81728
+ rootSession,
81729
+ mainRoute: locked
81730
+ };
81731
+ let mainRoute;
81732
+ if (agent?.session === rootSession || agent?.session === void 0) {
81733
+ let live;
81734
+ try {
81735
+ live = agent?.model?.current?.();
81736
+ } catch {
81737
+ live = void 0;
81738
+ }
81739
+ mainRoute = headerRouteOf(rootSession) ?? routeOf(live?.provider, live?.model) ?? routeOf(modelSelectionHint?.provider, modelSelectionHint?.model);
81740
+ } else {
81741
+ const rootOptions = rootSession.options;
81742
+ mainRoute = headerRouteOf(rootSession) ?? routeOf(rootOptions?.provider, rootOptions?.model);
81743
+ }
81744
+ if (mainRoute === void 0) return { rootSession };
81745
+ if (lockable && (lockSnapshot || headerRouteOf(rootSession) !== void 0)) lockedRootMainRoutes.set(rootSession, mainRoute);
81746
+ return {
81747
+ rootSession,
81748
+ mainRoute
81749
+ };
81750
+ }
81751
+ /**
80179
81752
  * Read the route an inherit-mode child would run on: the calling agent's own
80180
81753
  * effective request route. This mirrors the delegation seam's
80181
81754
  * `parentAgentOptionsForDelegation`, where the latest request header owns
@@ -80389,9 +81962,36 @@ function delegationModeOf(policies, toolName) {
80389
81962
  * @param scope - Whether an unrecorded Session falls back to the preference.
80390
81963
  * @returns the authorized routes in force for this agent, or an empty list when unrestricted.
80391
81964
  */
80392
- function effectiveAllowedRoutes(agent, preference, sessions, scope = "session") {
81965
+ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session", lockSnapshot = false) {
80393
81966
  if (preference !== void 0 && !preference.enabled) return [];
80394
- return authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
81967
+ const baseRoutes = authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
81968
+ if (baseRoutes.length === 0) return [];
81969
+ return mainModelRoutesFor(agent, sessions, preference, baseRoutes, lockSnapshot);
81970
+ }
81971
+ /**
81972
+ * Apply the per-main-model override to a Session's authorized routes.
81973
+ *
81974
+ * Precedence is exact-match, then fallback, then the Session's own list: an
81975
+ * entry for the main model the Session actually runs is the most specific
81976
+ * statement available, the fallback list covers every other main model, and the
81977
+ * Session policy is what governs when the deployment configured neither.
81978
+ * @param agent - Calling agent.
81979
+ * @param sessions - Session registry used for ancestor lookup.
81980
+ * @param preference - Current Host preference, when the settings service exists.
81981
+ * @param baseRoutes - Routes the Session itself authorized; never empty here.
81982
+ * @param lockSnapshot - Freeze the resolved main route AND the resolved route
81983
+ * list for this Session now.
81984
+ * @returns the routes that actually govern this delegation.
81985
+ */
81986
+ function mainModelRoutesFor(agent, sessions, preference, baseRoutes, lockSnapshot = false) {
81987
+ const { rootSession, mainRoute } = resolveRootSessionAndMainRoute(agent, sessions, void 0, lockSnapshot);
81988
+ const lockable = typeof rootSession === "object" && rootSession !== null;
81989
+ const locked = lockable ? lockedRootAllowedRoutes.get(rootSession) : void 0;
81990
+ if (locked !== void 0) return locked;
81991
+ const overrideRoutes = mainRoute === void 0 ? void 0 : preference?.byMainModel?.[`${mainRoute.provider}/${mainRoute.model}`];
81992
+ const resolved = overrideRoutes !== void 0 && overrideRoutes.length > 0 ? overrideRoutes : preference?.defaultAllowedModels !== void 0 && preference.defaultAllowedModels.length > 0 ? preference.defaultAllowedModels : baseRoutes;
81993
+ if (lockable && (lockSnapshot || headerRouteOf(rootSession) !== void 0)) lockedRootAllowedRoutes.set(rootSession, [...resolved]);
81994
+ return resolved;
80395
81995
  }
80396
81996
  /**
80397
81997
  * State a Session's routing posture in the `list_subagent_models` result.
@@ -80421,8 +82021,96 @@ function enrichListSubagentModelsOutput(output, allowed) {
80421
82021
  }
80422
82022
  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):";
80423
82023
  if (output.includes(header)) return output;
82024
+ const filtered = filterListSubagentModelsOutput(output, allowed);
80424
82025
  const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
80425
- return output.trim().length === 0 ? suffix : `${output}\n\n${suffix}`;
82026
+ return filtered.trim().length === 0 ? suffix : `${filtered}\n\n${suffix}`;
82027
+ }
82028
+ /** One rendered `provider — Name` line of the upstream provider list. */
82029
+ const PROVIDER_LINE = /^([^/\s]+)\s+—\s+.+$/;
82030
+ /** One rendered `provider/model — Name` line of the upstream model list. */
82031
+ const MODEL_LINE = /^([^/\s]+)\/([^\s]+)\s+—\s+.+$/;
82032
+ /**
82033
+ * Drop the upstream lines that name a route this Session does not authorize.
82034
+ *
82035
+ * The upstream discovery tool reads the union policy recorded on the Session, so
82036
+ * its raw answer can list a provider or a `provider/model` line that belongs to
82037
+ * another main model's entry and NOT to the routes this main model is governed
82038
+ * by. Leaving those lines in place is worse than a cosmetic mismatch: the
82039
+ * documented way to read this output is to copy a line into `subagent`, so every
82040
+ * stale line is an invitation to a delegation the guard will refuse.
82041
+ *
82042
+ * Only the two known renderings are filtered, and only lines that MATCH one of
82043
+ * them are candidates: any other line (a heading, a hint, an empty answer) is
82044
+ * passed through untouched, so a generation that changes its wording degrades to
82045
+ * the old behavior rather than to an empty answer.
82046
+ * @param output - Original text returned by `list_subagent_models`.
82047
+ * @param allowed - Routes the calling Session authorizes; never empty here.
82048
+ * @returns the output with unauthorized lines removed.
82049
+ */
82050
+ function filterListSubagentModelsOutput(output, allowed) {
82051
+ if (allowed.length === 0 || output.length === 0) return output;
82052
+ if (output.includes("\nReasoning efforts:\n")) return output;
82053
+ const lines = output.split("\n");
82054
+ const modelList = lines.some((line) => MODEL_LINE.test(line));
82055
+ let matched = false;
82056
+ const kept = [];
82057
+ for (const line of lines) {
82058
+ if (modelList) {
82059
+ const match = MODEL_LINE.exec(line);
82060
+ if (match === null) {
82061
+ kept.push(line);
82062
+ continue;
82063
+ }
82064
+ matched = true;
82065
+ if (routesInclude(allowed, match[1], match[2])) kept.push(line);
82066
+ continue;
82067
+ }
82068
+ const match = PROVIDER_LINE.exec(line);
82069
+ if (match === null) {
82070
+ kept.push(line);
82071
+ continue;
82072
+ }
82073
+ matched = true;
82074
+ if (allowed.some((route) => route.provider === match[1])) kept.push(line);
82075
+ }
82076
+ if (!matched) return output;
82077
+ const keptText = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
82078
+ if (keptText.length > 0) return keptText;
82079
+ if (modelList) return `(no advertised models for ${MODEL_LINE.exec(output)?.[1] ?? ""})`;
82080
+ return "(no LLM providers)";
82081
+ }
82082
+ /**
82083
+ * Merge live per-main-model overrides into a resolved preference.
82084
+ *
82085
+ * The overrides are a separate seam from the settings namespace because they
82086
+ * live in THIS plugin's own preference document, not in the upstream
82087
+ * `subagent-model-selection` namespace. They also have to work on a harness
82088
+ * where that namespace cannot be registered at all (0.1.7 dropped
82089
+ * `settings.register`): there, an override is the only per-main-model statement
82090
+ * available, so it stands up an enabled preference by itself rather than being
82091
+ * dropped for lack of a base to attach to.
82092
+ * @param base - Preference resolved from the settings namespace, when any.
82093
+ * @param overrides - Live overrides read from this plugin's preferences.
82094
+ * @returns the merged preference, or undefined when neither source says anything.
82095
+ */
82096
+ function mergePreferenceOverrides(base, overrides) {
82097
+ const hasDefault = overrides?.defaultAllowedModels !== void 0;
82098
+ const hasByMain = overrides?.byMainModel !== void 0;
82099
+ if (base === void 0) {
82100
+ if (!hasDefault && !hasByMain) return void 0;
82101
+ return {
82102
+ enabled: true,
82103
+ allowedModels: overrides?.defaultAllowedModels ?? [],
82104
+ ...hasDefault ? { defaultAllowedModels: overrides?.defaultAllowedModels } : {},
82105
+ ...hasByMain ? { byMainModel: overrides?.byMainModel } : {}
82106
+ };
82107
+ }
82108
+ if (!hasDefault && !hasByMain) return base;
82109
+ return {
82110
+ ...base,
82111
+ ...hasDefault ? { defaultAllowedModels: overrides?.defaultAllowedModels } : {},
82112
+ ...hasByMain ? { byMainModel: overrides?.byMainModel } : {}
82113
+ };
80426
82114
  }
80427
82115
  /**
80428
82116
  * The statement appended to `list_subagent_models` when no allowlist governs the
@@ -80533,7 +82221,7 @@ function rewriteAuthorizedPromptAssembly(assembly, allowed, toolNames = DEFAULT_
80533
82221
  * @param inheritToolNames - Delegation tools that start a child on the caller's own route.
80534
82222
  * @returns a denial reason, or undefined to leave the call untouched.
80535
82223
  */
80536
- function delegationDenialReason(agent, toolName, args, preference, sessions, toolNames = DEFAULT_DELEGATION_TOOLS, scope = "session", inheritToolNames = [], declaresRouteParameters) {
82224
+ function delegationDenialReason(agent, toolName, args, preference, sessions, toolNames = DEFAULT_DELEGATION_TOOLS, scope = "session", inheritToolNames = [], declaresRouteParameters, lockSnapshot = true) {
80537
82225
  const mode = delegationModeOf([...toolNames.map((name) => ({
80538
82226
  name,
80539
82227
  mode: "explicit"
@@ -80542,7 +82230,7 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
80542
82230
  mode: "inherit"
80543
82231
  }))], toolName);
80544
82232
  if (mode === void 0) return void 0;
80545
- const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope);
82233
+ const allowed = effectiveAllowedRoutes(agent, preference, sessions, scope, lockSnapshot);
80546
82234
  if (allowed.length === 0) return void 0;
80547
82235
  const request = asRecord$2(args) ?? {};
80548
82236
  const requestedProvider = asString$3(request["provider"]);
@@ -80570,7 +82258,26 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
80570
82258
  * @returns a guard that denies delegations outside the Session allowlist.
80571
82259
  */
80572
82260
  function createSubagentAuthorization(options) {
80573
- return (agent, toolName, args) => delegationDenialReason(agent, toolName, args, subagentModelSelectionPreference(options.settings), options.sessions, options.toolNames, options.scope ?? "session", options.inheritToolNames ?? [], options.declaresRouteParameters);
82261
+ return (agent, toolName, args) => delegationDenialReason(agent, toolName, args, mergedPreference(options), options.sessions, options.toolNames, options.scope ?? "session", options.inheritToolNames ?? [], options.declaresRouteParameters);
82262
+ }
82263
+ /**
82264
+ * Resolve the preference one guard evaluation uses.
82265
+ *
82266
+ * The namespace preference and this plugin's per-main-model overrides are
82267
+ * separate seams — the override document outlives a harness that cannot register
82268
+ * the namespace at all — so they are merged here, once, rather than at each of
82269
+ * the four call sites that need the result.
82270
+ * @param options - Runtime inputs the guard closed over.
82271
+ * @returns the merged preference, or undefined when neither seam says anything.
82272
+ */
82273
+ function mergedPreference(options) {
82274
+ let overrides;
82275
+ try {
82276
+ overrides = options.routingOverrides?.();
82277
+ } catch {
82278
+ overrides = void 0;
82279
+ }
82280
+ return mergePreferenceOverrides(subagentModelSelectionPreference(options.settings), overrides);
80574
82281
  }
80575
82282
  /** Delegation names this guard recognizes when configuration omits them. */
80576
82283
  function normalizeDelegationToolNames(value) {
@@ -80662,14 +82369,19 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
80662
82369
  toolNames,
80663
82370
  inheritToolNames,
80664
82371
  scope,
80665
- declaresRouteParameters: routeParameterProbe(ctx)
82372
+ declaresRouteParameters: routeParameterProbe(ctx),
82373
+ routingOverrides: config.routingOverrides
82374
+ });
82375
+ const currentPreference = () => mergedPreference({
82376
+ settings,
82377
+ routingOverrides: config.routingOverrides
80666
82378
  });
80667
82379
  const disposeGuard = ctx.tools.guard((exec) => authorize(exec.agent, exec.name, exec.arguments));
80668
82380
  const eventHost = ctx;
80669
82381
  const disposePostExecute = eventHost.on?.("tools/post-execute", async (exec, result, next) => {
80670
82382
  const decision = await next();
80671
82383
  if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
80672
- const allowed = effectiveAllowedRoutes(exec.agent, subagentModelSelectionPreference(settings), sessions, scope);
82384
+ const allowed = effectiveAllowedRoutes(exec.agent, currentPreference(), sessions, scope, true);
80673
82385
  const currentValue = decision.value !== void 0 ? decision.value : result.value;
80674
82386
  if (typeof currentValue === "string") {
80675
82387
  const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
@@ -80697,7 +82409,7 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
80697
82409
  return decision;
80698
82410
  });
80699
82411
  const disposeAssemble = eventHost.on?.("system-prompt/assemble", async (_assembly, context, next) => {
80700
- return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope, subagentModelSelectionPreference(settings), sessions, scope), toolNames);
82412
+ return rewriteAuthorizedPromptAssembly(await next(), effectiveAllowedRoutes(context?.agent ?? context?.scope, currentPreference(), sessions, scope, true), toolNames);
80701
82413
  });
80702
82414
  return () => {
80703
82415
  disposeGuard();
@@ -83120,7 +84832,7 @@ When in doubt, clarify before escalating; if a task turns out more complex than
83120
84832
  - 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.
83121
84833
 
83122
84834
  ## R1 · Responsibility Boundary (Layered by Tier)
83123
- 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.
84835
+ For L2 tasks, never execute work yourself outside these seven categories: (1) Planning & decomposition; (2) Dispatching tasks; (3) Review; (4) Acceptance verification; (5) Documentation & delivery summary; (6) Tasks subagents cannot perform; (7) Independent parallel-module implementation that shares no files, unfinished interfaces, or acceptance dependencies with active subagents and can be verified independently.
83124
84836
  For L0/L1 tasks, execute directly—do not spawn subagents just to look like you are orchestrating.
83125
84837
 
83126
84838
  ## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
@@ -83176,7 +84888,7 @@ All three rules govern the boundary of a turn: after dispatching, before continu
83176
84888
  [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.
83177
84889
  - The notification is a post-hoc signal. It tells you a child stopped running; it does not tell you the work is correct, and nothing about it arrives before the work is done. Treat it as "there is now something to review", never as "this passed". The pre-hoc control is the acceptance criteria in the brief (R5); the review itself is R6.
83178
84890
  - Never run sleep, polling loops, watch, or any long-running placeholder command just to "hang around": they burn context and budget for zero information.
83179
- - 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.
84891
+ - If you have independent coordinator work in hand (the seven categories in R1, including non-overlapping parallel-module development, reviewing another subagent's output, or reading evidence you need), do it first and then end your turn. Do not manufacture work merely to keep a provider cache warm.
83180
84892
  - 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.
83181
84893
  - 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.
83182
84894
 
@@ -83789,10 +85501,10 @@ const DELIVERY_FAILURE_MARKERS = [
83789
85501
  "is closing",
83790
85502
  "ACTIVATION_CLOSING"
83791
85503
  ];
83792
- function record(value) {
85504
+ function record$1(value) {
83793
85505
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
83794
85506
  }
83795
- function text(value) {
85507
+ function text$1(value) {
83796
85508
  return typeof value === "string" ? value : void 0;
83797
85509
  }
83798
85510
  function count(value) {
@@ -83807,8 +85519,8 @@ function oneLine(value, max) {
83807
85519
  function sessionTag(session) {
83808
85520
  if (session === void 0) return "session=?";
83809
85521
  const id = typeof session.id === "string" && session.id !== "" ? session.id : "?";
83810
- const origin = text(session.header?.origin);
83811
- const parent = text(session.header?.parentSession);
85522
+ const origin = text$1(session.header?.origin);
85523
+ const parent = text$1(session.header?.parentSession);
83812
85524
  return `session=${id}${parent === void 0 || parent === "" ? "" : ` parent=${parent}`}${origin === void 0 ? "" : ` origin=${origin}`}`;
83813
85525
  }
83814
85526
  /** Block-type histogram plus whether a message carries anything the user can see. */
@@ -83818,11 +85530,11 @@ function blocksSummary(content) {
83818
85530
  let visible = false;
83819
85531
  let chars = 0;
83820
85532
  for (const block of content) {
83821
- const value = record(block);
83822
- const type = text(value?.type) ?? "unknown";
85533
+ const value = record$1(block);
85534
+ const type = text$1(value?.type) ?? "unknown";
83823
85535
  counts.set(type, (counts.get(type) ?? 0) + 1);
83824
85536
  if (type === "tool-call") visible = true;
83825
- const body = text(value?.text);
85537
+ const body = text$1(value?.text);
83826
85538
  if (body !== void 0) {
83827
85539
  chars += body.length;
83828
85540
  if (type === "text" && body.trim().length > 0) visible = true;
@@ -83859,9 +85571,9 @@ var RelayProbe = class {
83859
85571
  }
83860
85572
  }
83861
85573
  step(session, event) {
83862
- const type = text(event?.type);
85574
+ const type = text$1(event?.type);
83863
85575
  if (type === void 0) return;
83864
- const data = record(event.data);
85576
+ const data = record$1(event.data);
83865
85577
  if (data === void 0) return;
83866
85578
  switch (type) {
83867
85579
  case "agent/inbox/spliced":
@@ -83894,9 +85606,9 @@ var RelayProbe = class {
83894
85606
  this.observedRequest(data);
83895
85607
  return;
83896
85608
  case "assistant/message": {
83897
- const message = record(data.message);
83898
- const source = record(message?.source);
83899
- const model = source === void 0 ? "" : ` model=${text(source.provider) ?? "?"}/${text(source.model) ?? "?"}`;
85609
+ const message = record$1(data.message);
85610
+ const source = record$1(message?.source);
85611
+ const model = source === void 0 ? "" : ` model=${text$1(source.provider) ?? "?"}/${text$1(source.model) ?? "?"}`;
83900
85612
  this.line(`assistant turn=${count(data.turn) ?? "?"} step=${count(data.step) ?? "?"}${model} ${blocksSummary(message?.content)}`);
83901
85613
  return;
83902
85614
  }
@@ -83905,16 +85617,16 @@ var RelayProbe = class {
83905
85617
  }
83906
85618
  /** A delivery entering (or leaving) the parent's inbox. */
83907
85619
  onSplice(session, data) {
83908
- const target = text(data.target) ?? "?";
85620
+ const target = text$1(data.target) ?? "?";
83909
85621
  const inserted = Array.isArray(data.inserted) ? data.inserted : [];
83910
85622
  const removed = count(data.removedCount) ?? 0;
83911
85623
  const outcomes = [];
83912
85624
  for (const entry of inserted) {
83913
- const message = record(entry);
83914
- const source = record(message?.source);
83915
- const kind = text(source?.kind) ?? "";
85625
+ const message = record$1(entry);
85626
+ const source = record$1(message?.source);
85627
+ const kind = text$1(source?.kind) ?? "";
83916
85628
  if (!RELAY_SOURCE_KINDS.includes(kind)) continue;
83917
- outcomes.push(`kind=${kind}${this.formTag(source)} msg=${text(message?.id) ?? "?"}`);
85629
+ outcomes.push(`kind=${kind}${this.formTag(source)} msg=${text$1(message?.id) ?? "?"}`);
83918
85630
  }
83919
85631
  if (outcomes.length > 0) {
83920
85632
  this.watch(session);
@@ -83923,84 +85635,84 @@ var RelayProbe = class {
83923
85635
  }
83924
85636
  if (!this.observed.has(session.id)) return;
83925
85637
  if (removed === 0 && inserted.length === 0) return;
83926
- const outcome = text(data.outcome);
85638
+ const outcome = text$1(data.outcome);
83927
85639
  this.line(`inbox-splice ${sessionTag(session)} target=${target} inserted=${inserted.length} removed=${removed}${outcome === void 0 ? "" : ` outcome=${outcome}`} pending=${this.pending(session)}`);
83928
85640
  }
83929
85641
  /** A message committed to the session surface — for a relay, the claim. */
83930
85642
  onUserMessage(session, data) {
83931
- const message = record(data.message);
83932
- const source = record(message?.source);
83933
- const kind = text(source?.kind) ?? "?";
85643
+ const message = record$1(data.message);
85644
+ const source = record$1(message?.source);
85645
+ const kind = text$1(source?.kind) ?? "?";
83934
85646
  const boundary = RELAY_SOURCE_KINDS.includes(kind);
83935
85647
  if (boundary) this.watch(session);
83936
85648
  if (!boundary && !this.observed.has(session.id)) return;
83937
- const sender = text(source?.senderSessionId);
85649
+ const sender = text$1(source?.senderSessionId);
83938
85650
  const senderTag = sender === void 0 || sender === "" ? "" : ` sender=${sender}`;
83939
- this.line(`user-committed ${sessionTag(session)} kind=${kind}${this.formTag(source)} msg=${text(message?.id) ?? "?"}${senderTag} turn=${count(data.turn) ?? "-"} ${blocksSummary(message?.content)} pending=${this.pending(session)}`);
85651
+ this.line(`user-committed ${sessionTag(session)} kind=${kind}${this.formTag(source)} msg=${text$1(message?.id) ?? "?"}${senderTag} turn=${count(data.turn) ?? "-"} ${blocksSummary(message?.content)} pending=${this.pending(session)}`);
83940
85652
  if (boundary) this.line(`agent-state ${this.agentState(session)}`);
83941
85653
  }
83942
85654
  /** A child attempting to report to its parent. */
83943
85655
  onToolCall(session, data) {
83944
- const message = record(data.message);
85656
+ const message = record$1(data.message);
83945
85657
  if (!Array.isArray(message?.content)) return;
83946
85658
  for (const block of message.content) {
83947
- const value = record(block);
83948
- if (text(value?.type) !== "tool-call") continue;
83949
- const name = text(value?.name) ?? "";
85659
+ const value = record$1(block);
85660
+ if (text$1(value?.type) !== "tool-call") continue;
85661
+ const name = text$1(value?.name) ?? "";
83950
85662
  if (!CHILD_SEND_TOOLS.includes(name)) continue;
83951
- this.line(`child-send ${sessionTag(session)} tool=${name} call=${text(value?.id) ?? "?"}`);
85663
+ this.line(`child-send ${sessionTag(session)} tool=${name} call=${text$1(value?.id) ?? "?"}`);
83952
85664
  }
83953
85665
  }
83954
85666
  /** A tool result that may be a refused delivery. */
83955
85667
  onToolResult(session, data) {
83956
- const message = record(data.message);
85668
+ const message = record$1(data.message);
83957
85669
  if (!Array.isArray(message?.content)) return;
83958
85670
  for (const block of message.content) {
83959
- const value = record(block);
83960
- if (value === void 0 || text(value.type) !== "tool-result") continue;
85671
+ const value = record$1(block);
85672
+ if (value === void 0 || text$1(value.type) !== "tool-result") continue;
83961
85673
  const body = this.toolResultText(value.content);
83962
85674
  const marker = DELIVERY_FAILURE_MARKERS.find((candidate) => body.includes(candidate));
83963
85675
  if (marker === void 0) continue;
83964
- this.line(`child-send-failed ${sessionTag(session)} call=${text(value.toolCallId) ?? "?"} marker=${JSON.stringify(marker)} excerpt=${JSON.stringify(oneLine(body, EXCERPT_MAX))}`);
85676
+ this.line(`child-send-failed ${sessionTag(session)} call=${text$1(value.toolCallId) ?? "?"} marker=${JSON.stringify(marker)} excerpt=${JSON.stringify(oneLine(body, EXCERPT_MAX))}`);
83965
85677
  }
83966
85678
  }
83967
85679
  toolResultText(content) {
83968
85680
  if (!Array.isArray(content)) return "";
83969
85681
  const parts = [];
83970
85682
  for (const block of content) {
83971
- const body = text(record(block)?.text);
85683
+ const body = text$1(record$1(block)?.text);
83972
85684
  if (body !== void 0) parts.push(body);
83973
85685
  }
83974
85686
  return parts.join(" ");
83975
85687
  }
83976
85688
  observedRequest(data) {
83977
- const config = record(record(data.header)?.config);
85689
+ const config = record$1(record$1(data.header)?.config);
83978
85690
  if (config === void 0) return;
83979
85691
  const series = data.startsSeries === true ? " startsSeries=yes" : "";
83980
- this.line(`request provider=${text(config.provider) ?? "?"} model=${text(config.model) ?? "?"} reason=${text(data.reason) ?? "-"}${series}`);
85692
+ this.line(`request provider=${text$1(config.provider) ?? "?"} model=${text$1(config.model) ?? "?"} reason=${text$1(data.reason) ?? "-"}${series}`);
83981
85693
  }
83982
85694
  turnReason(reason) {
83983
- const value = record(reason);
85695
+ const value = record$1(reason);
83984
85696
  if (value === void 0) return oneLine(String(reason ?? "?"), SUMMARY_MAX);
83985
- const kind = text(value.kind) ?? "?";
83986
- const failure = record(value.error);
85697
+ const kind = text$1(value.kind) ?? "?";
85698
+ const failure = record$1(value.error);
83987
85699
  if (failure === void 0) return kind;
83988
- const code = text(failure.code) ?? "?";
83989
- const message = text(failure.message);
85700
+ const code = text$1(failure.code) ?? "?";
85701
+ const message = text$1(failure.message);
83990
85702
  return message === void 0 ? `${kind} code=${code}` : `${kind} code=${code} message=${JSON.stringify(oneLine(message, SUMMARY_MAX))}`;
83991
85703
  }
83992
85704
  formTag(source) {
83993
85705
  if (source === void 0) return "";
83994
- const form = text(source.form);
83995
- const summary = text(source.summary);
83996
- const plugin = text(source.plugin);
85706
+ const form = text$1(source.form);
85707
+ const summary = text$1(source.summary);
85708
+ const plugin = text$1(source.plugin);
83997
85709
  return `${form === void 0 ? "" : ` form=${form}`}${plugin === void 0 ? "" : ` plugin=${plugin}`}${summary === void 0 ? "" : ` summary=${JSON.stringify(oneLine(summary, SUMMARY_MAX))}`}`;
83998
85710
  }
83999
85711
  /** Live snapshot of the Agent behind this Session, when it is resident. */
84000
85712
  agentState(session) {
84001
85713
  const agent = this.options.agents?.get(session.id);
84002
85714
  if (agent === void 0) return `${sessionTag(session)} resident=no`;
84003
- const status = text(agent.status) ?? "?";
85715
+ const status = text$1(agent.status) ?? "?";
84004
85716
  return `${sessionTag(session)} resident=yes status=${status} pending=${agent.inbox === void 0 ? "?" : `nextStep:${agent.inbox.nextStep?.length ?? 0},nextTurn:${agent.inbox.nextTurn?.length ?? 0}`}`;
84005
85717
  }
84006
85718
  pending(session) {
@@ -84425,7 +86137,7 @@ function asRegistry$1(value) {
84425
86137
  * Both reads are guarded: a host without the reader, and one without the
84426
86138
  * service, must leave this plugin loadable rather than failing its import.
84427
86139
  */
84428
- function agentRegistry$1(ctx) {
86140
+ function agentRegistry$2(ctx) {
84429
86141
  try {
84430
86142
  const registry = asRegistry$1(typeof ctx.get === "function" ? ctx.get("agents") : void 0);
84431
86143
  if (registry !== void 0) return registry;
@@ -84504,7 +86216,7 @@ function installReasoningCollapseGuard(ctx, options = {}) {
84504
86216
  function resolveAgent(sessionId) {
84505
86217
  if (sessionId === void 0) return void 0;
84506
86218
  try {
84507
- const registered = agentRegistry$1(ctx)?.get(sessionId);
86219
+ const registered = agentRegistry$2(ctx)?.get(sessionId);
84508
86220
  if (registered !== void 0 && registered !== null) return registered;
84509
86221
  } catch {}
84510
86222
  return known.get(sessionId);
@@ -84892,7 +86604,7 @@ function asRegistry(value) {
84892
86604
  return value !== null && typeof value === "object" && typeof value.get === "function" ? value : void 0;
84893
86605
  }
84894
86606
  /** The host's Agent registry, read defensively through both available routes. */
84895
- function agentRegistry(ctx) {
86607
+ function agentRegistry$1(ctx) {
84896
86608
  try {
84897
86609
  const registry = asRegistry(typeof ctx.get === "function" ? ctx.get("agents") : void 0);
84898
86610
  if (registry !== void 0) return registry;
@@ -84962,7 +86674,7 @@ function installNoAnswerResume(ctx, options = {}) {
84962
86674
  function resolveAgent(sessionId) {
84963
86675
  if (sessionId === void 0) return void 0;
84964
86676
  try {
84965
- const registered = agentRegistry(ctx)?.get(sessionId);
86677
+ const registered = agentRegistry$1(ctx)?.get(sessionId);
84966
86678
  if (registered !== void 0 && registered !== null) return registered;
84967
86679
  } catch {}
84968
86680
  }
@@ -85789,11 +87501,13 @@ function installCacheWarmup(ctx, options) {
85789
87501
  *
85790
87502
  * 决策取舍写在代码里,因为它全是「什么时候**不动**」:
85791
87503
  * - 不是 `accept`(`block` 的 feedback 是别人的东西);
85792
- * - 已经带 `value`(执行本地的 canonical 值,replay 与结构化消费方读它,
85793
- * 替换掉等于把一份 JSON 换成字符串);
85794
87504
  * - 工具名不是 `run_code`、线路不是本线、会话拿不到;
85795
87505
  * - 没有存储、内容为空。
85796
87506
  * 上面任何一条成立就原样返回同一个决策对象。
87507
+ *
87508
+ * 决策带了 `value` 不再是「不动」的理由:核心没真正生成预览时,末尾仍然
87509
+ * 原样交回同一个决策对象(`value` 一点不丢);只有核心真的换了内容,
87510
+ * 才换成带预览的 `accept` 决策。
85797
87511
  * @param exec - 工具执行上下文。
85798
87512
  * @param result - 归一化后的执行结果。
85799
87513
  * @param decision - `next()` 交回来的决策。
@@ -85811,7 +87525,6 @@ function installCacheWarmup(ctx, options) {
85811
87525
  const navigation = retrievalTool || ptcRetrieval ? cacheWarmupNavigationIntentOf(exec.agent) : void 0;
85812
87526
  if (retrievalTool && navigation !== true) return decision;
85813
87527
  if (sourceTool && !settings.enabled && navigation !== true) return decision;
85814
- if (sourceTool && decision.value !== void 0 && navigation !== true) return decision;
85815
87528
  const route = cacheWarmupRouteOf(exec.agent);
85816
87529
  if (route.provider !== "kimi-code") return decision;
85817
87530
  const sessionId = route.sessionId;
@@ -85886,14 +87599,292 @@ function blocksOf(content) {
85886
87599
  }];
85887
87600
  return content;
85888
87601
  }
85889
- /**
85890
- * 本文件刻意**不保留**任何"把异常压成一句话"的辅助函数。
85891
- *
85892
- * 这里曾经有一个 `describe(error)`,把 `error.message` 截断后拼进宿主日志。它看上去
85893
- * 无害(只取第一行、还截了长度),实际上是一条原文出口:异常消息里常常带着绝对
85894
- * 路径、命令行片段、凭据,或者被读取结果的一段正文。三处降级日志现在统一走
85895
- * {@link cacheWarmupErrorLabel}——只输出稳定的 name/code。
85896
- */
87602
+ /** Opt-in switch for one bounded warmup during a delegation batch. */
87603
+ const CACHE_WAIT_WARMUP_ENV = "DSH_KIMI_CODE_CACHE_WAIT_WARMUP";
87604
+ const MAX_TRACKED_SESSIONS = 256;
87605
+ /** Pure cache-wait policy; no child deadline or forced progress is involved. */
87606
+ function evaluateCacheWait(input) {
87607
+ if (!input.waiting || !input.hasChildren || input.lastActiveAt === void 0) return { kind: "none" };
87608
+ if (!Number.isFinite(input.lastActiveAt) || !Number.isFinite(input.now)) return { kind: "none" };
87609
+ const decisionMs = input.decisionMs ?? 12e5;
87610
+ const ttlMs = input.ttlMs ?? 15e5;
87611
+ const minWarmupTokens = input.minWarmupTokens ?? 1e5;
87612
+ const idleMs = Math.max(0, input.now - input.lastActiveAt);
87613
+ if (idleMs >= ttlMs) return { kind: "cold" };
87614
+ if (idleMs < decisionMs) return {
87615
+ kind: "schedule",
87616
+ delayMs: decisionMs - idleMs
87617
+ };
87618
+ if (input.warmupEnabled && !input.warmupUsed && input.totalTokens !== void 0 && Number.isFinite(input.totalTokens) && input.totalTokens >= minWarmupTokens) return { kind: "warmup" };
87619
+ return { kind: "cold" };
87620
+ }
87621
+ /** The one opt-in message, built through the generation-safe harness factory. */
87622
+ function createCacheWaitWarmupMessage() {
87623
+ return createUserMessage$1({
87624
+ content: [{
87625
+ type: "text",
87626
+ text: "Automatic cache maintenance while delegated work continues. This is not a new task and must not change priorities or cause tool calls. Reply with one brief acknowledgment, then continue waiting for existing results. Do not request or manufacture progress from any subagent."
87627
+ }],
87628
+ source: {
87629
+ kind: PLUGIN_MESSAGE_SOURCE_KIND,
87630
+ form: "notice",
87631
+ summary: boundContextSummary("Cache maintenance only; delegated work continues unchanged.")
87632
+ }
87633
+ });
87634
+ }
87635
+ /** Whether the deployment explicitly opted into bounded warmup. */
87636
+ function cacheWaitWarmupEnabled(env = process.env) {
87637
+ const raw = env[CACHE_WAIT_WARMUP_ENV]?.trim().toLowerCase();
87638
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
87639
+ }
87640
+ function record(value) {
87641
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
87642
+ }
87643
+ function text(value) {
87644
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
87645
+ }
87646
+ function defaultTimer() {
87647
+ return {
87648
+ set(callback, delayMs) {
87649
+ const handle = setTimeout(callback, delayMs);
87650
+ handle.unref?.();
87651
+ return handle;
87652
+ },
87653
+ clear(handle) {
87654
+ clearTimeout(handle);
87655
+ }
87656
+ };
87657
+ }
87658
+ function defaultActivity(sessionId) {
87659
+ const durable = sessionCacheState.read(sessionId);
87660
+ const lastActiveAt = sessionLastActiveAt(sessionId) ?? durable?.lastActiveAt;
87661
+ if (lastActiveAt === void 0) return void 0;
87662
+ return {
87663
+ lastActiveAt,
87664
+ totalTokens: durable?.totalTokens
87665
+ };
87666
+ }
87667
+ function agentRegistry(ctx) {
87668
+ try {
87669
+ const value = ctx.get?.("agents") ?? ctx.agents;
87670
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value;
87671
+ } catch {}
87672
+ }
87673
+ /** Host-side cache-wait state machine. It never sends anything to a child. */
87674
+ var KimiCacheWaitController = class {
87675
+ options;
87676
+ states = /* @__PURE__ */ new Map();
87677
+ parentByChild = /* @__PURE__ */ new Map();
87678
+ providerBySession = /* @__PURE__ */ new Map();
87679
+ now;
87680
+ timer;
87681
+ agents;
87682
+ activity;
87683
+ kimiSession;
87684
+ warmup;
87685
+ logger;
87686
+ decisionMs;
87687
+ ttlMs;
87688
+ minWarmupTokens;
87689
+ disposed = false;
87690
+ constructor(options = {}) {
87691
+ this.options = options;
87692
+ this.now = options.now ?? (() => Date.now());
87693
+ this.timer = options.timer ?? defaultTimer();
87694
+ this.agents = options.agents;
87695
+ this.activity = options.activity ?? defaultActivity;
87696
+ this.kimiSession = options.kimiSession;
87697
+ this.warmup = options.warmup === true;
87698
+ this.logger = options.logger;
87699
+ this.decisionMs = options.decisionMs ?? 12e5;
87700
+ this.ttlMs = options.ttlMs ?? 15e5;
87701
+ this.minWarmupTokens = options.minWarmupTokens ?? 1e5;
87702
+ }
87703
+ /** Observe one durable Session event. */
87704
+ observe(session, event) {
87705
+ if (this.disposed) return;
87706
+ try {
87707
+ const type = text(event.type);
87708
+ if (type === void 0) return;
87709
+ const data = record(event.data);
87710
+ if (type === "request/header") {
87711
+ const provider = text(record(record(data?.header)?.config)?.provider);
87712
+ if (provider !== void 0) this.providerBySession.set(session.id, provider);
87713
+ return;
87714
+ }
87715
+ if (type === "subagent/catalog") {
87716
+ const childId = text(data?.childId);
87717
+ if (childId === void 0 || childId === session.id) return;
87718
+ const state = this.stateFor(session.id);
87719
+ const newChild = !state.children.has(childId);
87720
+ state.children.add(childId);
87721
+ this.parentByChild.set(childId, session.id);
87722
+ if (newChild) {
87723
+ state.warmupUsed = false;
87724
+ state.handled = false;
87725
+ }
87726
+ this.reconcile(session, state);
87727
+ return;
87728
+ }
87729
+ if (type === "subagent/end") {
87730
+ this.removeChild(text(data?.id));
87731
+ return;
87732
+ }
87733
+ if (type === "user/message") {
87734
+ const source = record(record(data?.message)?.source);
87735
+ if (text(source?.kind) === "subagent-settled") this.removeChild(text(source?.senderSessionId));
87736
+ return;
87737
+ }
87738
+ const state = this.stateFor(session.id);
87739
+ if (type === "turn/start") {
87740
+ state.waiting = false;
87741
+ this.clearTimer(state);
87742
+ return;
87743
+ }
87744
+ if (type === "turn/end") {
87745
+ state.waiting = true;
87746
+ state.handled = false;
87747
+ this.reconcile(session, state);
87748
+ }
87749
+ } catch (error) {
87750
+ this.logger?.warn(`kimi-cache-wait: ignored an invalid event: ${error instanceof Error ? error.message : String(error)}`);
87751
+ }
87752
+ }
87753
+ /** Forget a disposed Agent and any parent/child links it owned. */
87754
+ disposeAgent(agent) {
87755
+ const id = text(agent.id);
87756
+ if (id === void 0) return;
87757
+ this.dropState(id);
87758
+ this.removeChild(id);
87759
+ this.providerBySession.delete(id);
87760
+ }
87761
+ /** Test/diagnostic view; no mutable state escapes. */
87762
+ snapshot(sessionId) {
87763
+ const state = this.states.get(sessionId);
87764
+ return {
87765
+ waiting: state?.waiting ?? false,
87766
+ childCount: state?.children.size ?? 0,
87767
+ handled: state?.handled ?? false,
87768
+ warmupUsed: state?.warmupUsed ?? false,
87769
+ scheduled: state?.timer !== void 0,
87770
+ ...state?.lastDecision === void 0 ? {} : { lastDecision: state.lastDecision }
87771
+ };
87772
+ }
87773
+ dispose() {
87774
+ if (this.disposed) return;
87775
+ this.disposed = true;
87776
+ for (const state of this.states.values()) this.clearTimer(state);
87777
+ this.states.clear();
87778
+ this.parentByChild.clear();
87779
+ this.providerBySession.clear();
87780
+ }
87781
+ stateFor(sessionId) {
87782
+ const existing = this.states.get(sessionId);
87783
+ if (existing !== void 0) {
87784
+ this.states.delete(sessionId);
87785
+ this.states.set(sessionId, existing);
87786
+ return existing;
87787
+ }
87788
+ const state = {
87789
+ children: /* @__PURE__ */ new Set(),
87790
+ waiting: false,
87791
+ handled: false,
87792
+ warmupUsed: false
87793
+ };
87794
+ this.states.set(sessionId, state);
87795
+ if (this.states.size > MAX_TRACKED_SESSIONS) {
87796
+ const oldest = this.states.keys().next();
87797
+ if (oldest.done !== true) this.dropState(oldest.value);
87798
+ }
87799
+ return state;
87800
+ }
87801
+ dropState(sessionId) {
87802
+ const state = this.states.get(sessionId);
87803
+ if (state === void 0) return;
87804
+ this.clearTimer(state);
87805
+ for (const child of state.children) this.parentByChild.delete(child);
87806
+ this.states.delete(sessionId);
87807
+ }
87808
+ removeChild(childId) {
87809
+ if (childId === void 0) return;
87810
+ const parent = this.parentByChild.get(childId);
87811
+ if (parent === void 0) return;
87812
+ this.parentByChild.delete(childId);
87813
+ const state = this.states.get(parent);
87814
+ if (state === void 0) return;
87815
+ state.children.delete(childId);
87816
+ if (state.children.size === 0) this.clearTimer(state);
87817
+ }
87818
+ clearTimer(state) {
87819
+ if (state.timer === void 0) return;
87820
+ this.timer.clear(state.timer);
87821
+ state.timer = void 0;
87822
+ }
87823
+ isKimiSession(session) {
87824
+ if (this.kimiSession !== void 0) return this.kimiSession(session);
87825
+ try {
87826
+ const current = text(session.requestHeader?.()?.config?.provider);
87827
+ if (current !== void 0) return current === "kimi-code";
87828
+ } catch {}
87829
+ const latest = this.providerBySession.get(session.id);
87830
+ if (latest !== void 0) return latest === "kimi-code";
87831
+ return this.activity(session.id)?.lastActiveAt !== void 0;
87832
+ }
87833
+ reconcile(session, state) {
87834
+ this.clearTimer(state);
87835
+ if (this.disposed || !state.waiting || state.handled || state.children.size === 0) return;
87836
+ if (!this.isKimiSession(session)) return;
87837
+ const activity = this.activity(session.id);
87838
+ if (activity === void 0) return;
87839
+ const action = evaluateCacheWait({
87840
+ waiting: state.waiting,
87841
+ hasChildren: state.children.size > 0,
87842
+ now: this.now(),
87843
+ lastActiveAt: activity.lastActiveAt,
87844
+ totalTokens: activity.totalTokens,
87845
+ warmupEnabled: this.warmup,
87846
+ warmupUsed: state.warmupUsed,
87847
+ decisionMs: this.decisionMs,
87848
+ ttlMs: this.ttlMs,
87849
+ minWarmupTokens: this.minWarmupTokens
87850
+ });
87851
+ if (action.kind === "none") return;
87852
+ if (action.kind === "schedule") {
87853
+ state.timer = this.timer.set(() => {
87854
+ state.timer = void 0;
87855
+ this.reconcile(session, state);
87856
+ }, action.delayMs);
87857
+ return;
87858
+ }
87859
+ state.handled = true;
87860
+ state.lastDecision = action.kind;
87861
+ if (action.kind === "cold") return;
87862
+ state.warmupUsed = true;
87863
+ try {
87864
+ const agent = this.agents?.get(session.id);
87865
+ if (agent?.steer === void 0) throw new Error("the parent Agent is not available for warmup");
87866
+ agent.steer(createCacheWaitWarmupMessage());
87867
+ this.logger?.info?.(`kimi-cache-wait: queued one bounded warmup for session ${session.id}`);
87868
+ } catch (error) {
87869
+ this.logger?.warn(`kimi-cache-wait: warmup skipped for session ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
87870
+ }
87871
+ }
87872
+ };
87873
+ /** Install the host listener. Older hosts without the event bus remain inert. */
87874
+ function installKimiCacheWait(ctx, options = {}) {
87875
+ if (typeof ctx.on !== "function") return void 0;
87876
+ const agents = options.agents ?? agentRegistry(ctx);
87877
+ const controller = new KimiCacheWaitController({
87878
+ ...options,
87879
+ agents
87880
+ });
87881
+ const disposers = [ctx.on("session/event", (session, event) => controller.observe(session, event)), ctx.on("agent/disposed", ({ agent }) => controller.disposeAgent(agent))];
87882
+ return () => {
87883
+ controller.dispose();
87884
+ for (const handle of disposers) if (typeof handle === "function") handle();
87885
+ else handle?.dispose?.();
87886
+ };
87887
+ }
85897
87888
  //#endregion
85898
87889
  //#region src/host/cache-warmup/settings.ts
85899
87890
  /**
@@ -94013,7 +96004,16 @@ function apply(ctx, pluginConfig = {}) {
94013
96004
  return installSubagentModelAuthorization(scoped, sessions, {
94014
96005
  toolNames: delegationToolNames,
94015
96006
  inheritToolNames: delegationInheritNames,
94016
- scope: pluginConfig.subagentModelScope ?? "session"
96007
+ scope: pluginConfig.subagentModelScope ?? "session",
96008
+ routingOverrides: () => {
96009
+ const current = preferences.status();
96010
+ const defaultAllowedModels = current.subagentDefaultModels ?? [];
96011
+ const byMainModel = current.subagentModelsByMainModel ?? {};
96012
+ return {
96013
+ ...defaultAllowedModels.length > 0 ? { defaultAllowedModels } : {},
96014
+ ...Object.keys(byMainModel).length > 0 ? { byMainModel } : {}
96015
+ };
96016
+ }
94017
96017
  });
94018
96018
  }, "dsh-chatgpt-subscription: subagent model authorization");
94019
96019
  });
@@ -94052,6 +96052,14 @@ function apply(ctx, pluginConfig = {}) {
94052
96052
  };
94053
96053
  }
94054
96054
  }), "dsh-chatgpt-subscription: cache warmup");
96055
+ ctx.effect(() => {
96056
+ const dispose = installKimiCacheWait(ctx, { warmup: cacheWaitWarmupEnabled(process.env) });
96057
+ if (dispose === void 0) {
96058
+ ctx.logger.warn("[dsh-chatgpt-subscription] Kimi cache wait skipped: this harness exposes no event bus");
96059
+ return () => void 0;
96060
+ }
96061
+ return dispose;
96062
+ }, "dsh-chatgpt-subscription: Kimi cache wait");
94055
96063
  const probeEnvFile = relayProbeEnvFile();
94056
96064
  if (relayProbeEnabled(process.env, { envFile: probeEnvFile })) {
94057
96065
  const probePath = relayProbeLogPath(process.env, { envFile: probeEnvFile });
@@ -94361,7 +96369,12 @@ function apply(ctx, pluginConfig = {}) {
94361
96369
  const sessions = ctx.get("sessions");
94362
96370
  const target = sessions.get(sessionId);
94363
96371
  if (target === void 0) throw new Error(`Unknown session "${sessionId}".`);
94364
- const allowed = auditedRoutesOf(target);
96372
+ const audited = auditedRoutesOf(target);
96373
+ const current = preferences.status();
96374
+ const mainRoute = resolveRootSessionAndMainRoute({ session: target }, sessions).mainRoute;
96375
+ const override = mainRoute === void 0 ? void 0 : (current.subagentModelsByMainModel ?? {})[`${mainRoute.provider}/${mainRoute.model}`];
96376
+ const fallback = current.subagentDefaultModels ?? [];
96377
+ const allowed = audited === void 0 ? audited : override !== void 0 && override.length > 0 ? override : fallback.length > 0 ? fallback : audited;
94365
96378
  return Promise.resolve({
94366
96379
  sessionId,
94367
96380
  allowedModels: (allowed ?? []).map((route) => ({