@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.
- package/CHANGELOG.md +31 -0
- package/README.md +2 -1
- package/lib/client.js +2383 -179
- package/lib/client.js.map +1 -1
- package/lib/index.js +2246 -233
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +14 -2
- package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
- package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
- package/lib/types/client/subagent/SubagentCard.d.ts +35 -0
- package/lib/types/client/subagent/SubagentCard.d.ts.map +1 -0
- package/lib/types/client/subagent/index.d.ts +28 -0
- package/lib/types/client/subagent/index.d.ts.map +1 -0
- package/lib/types/client/subagent/locales.d.ts +172 -0
- package/lib/types/client/subagent/locales.d.ts.map +1 -0
- package/lib/types/client/subagent/styles.d.ts +20 -0
- package/lib/types/client/subagent/styles.d.ts.map +1 -0
- package/lib/types/client/subagent/subagent-card-controller.d.ts +329 -0
- package/lib/types/client/subagent/subagent-card-controller.d.ts.map +1 -0
- package/lib/types/compat.d.ts +1 -1
- package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
- package/lib/types/host/common/request-images.d.ts +66 -0
- package/lib/types/host/common/request-images.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts +15 -0
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/cache-wait.d.ts +125 -0
- package/lib/types/host/kimi-code/cache-wait.d.ts.map +1 -0
- package/lib/types/host/kimi-code/mapper.d.ts +123 -6
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/kimi-code/session-cache-state.d.ts +14 -0
- package/lib/types/host/kimi-code/session-cache-state.d.ts.map +1 -1
- package/lib/types/host/kimi-code/types.d.ts +29 -4
- package/lib/types/host/kimi-code/types.d.ts.map +1 -1
- package/lib/types/host/preferences.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/subagent-model-authorization.d.ts +130 -2
- package/lib/types/host/subagent-model-authorization.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +28 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +10 -1
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/lib/types/shared/preferences.d.ts +52 -1
- package/lib/types/shared/preferences.d.ts.map +1 -1
- package/package.json +1 -1
- 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.
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
5292
|
+
const root = record$7(value);
|
|
5293
5293
|
const data = Array.isArray(root?.data) ? root.data[0] : null;
|
|
5294
|
-
const image = string$3(record$
|
|
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$
|
|
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$
|
|
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$
|
|
9077
|
-
const response = record$
|
|
9078
|
-
const error = record$
|
|
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$
|
|
9081
|
-
const parts = Array.isArray(record$
|
|
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$
|
|
9083
|
+
const part = record$6(raw);
|
|
9084
9084
|
if (!part || part.thought === true) continue;
|
|
9085
|
-
const inline = record$
|
|
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$
|
|
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$
|
|
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$
|
|
9438
|
-
record$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
12124
|
-
usage = mapUsage$2(record$
|
|
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$
|
|
12128
|
-
for (const item of output) yield* adoptText(messageItemText(record$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
13551
|
+
const limit = record$2(value);
|
|
13435
13552
|
if (limit === null) continue;
|
|
13436
|
-
const idSource = text$
|
|
13437
|
-
addBucket(buckets, usedIds, uniqueId(slug(idSource), usedIds), readableLimitName(text$
|
|
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$
|
|
13443
|
-
spendControlReached: boolean(record$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
13522
|
-
id: text$
|
|
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$
|
|
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$
|
|
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,
|
|
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
|
-
|
|
25970
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
26315
|
-
let candidateText = "";
|
|
26591
|
+
const candidates = [];
|
|
26316
26592
|
for (const key of JSON_ENVELOPE_LOG_KEYS) {
|
|
26317
|
-
const
|
|
26318
|
-
if (typeof
|
|
26319
|
-
|
|
26320
|
-
|
|
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 (
|
|
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 ===
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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:
|
|
26732
|
+
key: "matches"
|
|
26339
26733
|
},
|
|
26340
|
-
|
|
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
|
-
|
|
26413
|
-
|
|
26414
|
-
|
|
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 {
|
|
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
|
|
26467
|
-
|
|
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 :
|
|
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 :
|
|
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:
|
|
26541
|
-
outputChars:
|
|
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:
|
|
27312
|
+
inputChars: decision.inputChars,
|
|
26554
27313
|
outputChars: decision.preview.length,
|
|
26555
|
-
omittedChars: Math.max(0,
|
|
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:
|
|
27364
|
+
inputChars: decision.inputChars,
|
|
26606
27365
|
outputChars: decision.preview.length,
|
|
26607
|
-
omittedChars: Math.max(0,
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
27466
|
-
*
|
|
27467
|
-
*
|
|
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
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
31913
|
-
const systemPromptHash
|
|
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
|
-
/**
|
|
31938
|
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
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$
|
|
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$
|
|
43810
|
-
const sourceType = text$
|
|
43811
|
-
const outBizNo = text$
|
|
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$
|
|
43835
|
-
const eventType = text$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
69273
|
-
return text$
|
|
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$
|
|
69361
|
-
return text$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
69847
|
-
return text$
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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
|
-
|
|
85893
|
-
|
|
85894
|
-
|
|
85895
|
-
|
|
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
|
|
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) => ({
|