@eddyskywalker/dsh-chatgpt-subscription 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/codemode.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as PLUGIN_MESSAGE_SOURCE_KIND } from "./llm-compat-u9mor0uu.js";
2
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
3
3
  import { NativeToolExecutor } from "@deepseek-ai/dsh-tool-executor-native";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  //#region src/host/codemode/discovery.ts
@@ -587,6 +587,10 @@ function summarizeHidden(names) {
587
587
  * 工具的数量、namespace 分布、会话 id 或时间,因此同一部署下逐字节恒定:挂载或卸载
588
588
  * MCP 服务端不会改写它,前缀缓存不会因为它失效。
589
589
  *
590
+ * 结果形状索引(`read` / `glob` / `grep` … 的顶层字段)也**必须是静态的**:它照抄自各工具
591
+ * 声明过的 output schema,不读活 catalog。按 catalog 生成会让「挂一个 MCP 服务端」改写系统
592
+ * 提示词首段,正好毁掉这一层要保住的前缀缓存。
593
+ *
590
594
  * @param input - 传输工具名、发现工具名与程序形态。
591
595
  * @returns 说明段正文。
592
596
  */
@@ -624,9 +628,18 @@ function buildCodemodeSectionText(input) {
624
628
  "",
625
629
  python ? retained ? "- A retained Python REPL: top-level await and a final expression. Its variables and imported modules survive into your later programs in this session." : "- Async Python program: top-level await works; print or evaluate a final expression to produce output." : retained ? "- A retained TypeScript REPL: top-level declarations, await, and a final expression. Its variables and imported modules survive into your later programs in this session." : "- Erasable TypeScript, run as the body of an async function: top-level await and return work.",
626
630
  "- Only what you return or console.log reaches the conversation; every intermediate result stays in the program, so filter and aggregate there instead of returning raw payloads.",
627
- "- File, search, and shell tool results are named-field objects, never bare strings (whereas `job_list` returns a bare array): `glob` answers `{ root, paths }`, `grep` answers `{ matches }` with matches shaped `{ path, lineNumber, line }`, `read` answers `{ path, offset, lines, totalLines }` with `lines` items shaped `{ number, text }`, and `win_bash` answers `{ stdout, stderr, exitCode }`. A named-field object carries its list or text inside those fields — `r.lines.map((l) => l.text)` — so never call `.map`, `.filter`, `.split`, `.splitlines` or `in` on the envelope itself.",
631
+ "- File, search, and shell tool results are named-field objects, never bare strings — read the field, not the envelope (`job_list` and `list_agents` are the only bare arrays):",
632
+ " `read` → `{ path, offset, lines: [{ number, text }], totalLines }` · `glob` → `{ root, paths }` · `grep` → `{ matches: [{ path, lineNumber, line }] }` · `edit` → `{ path, before, after }` · `write` → `{ path, operation, before, after }` · `win_bash` → `{ stdout, stderr, exitCode }` · `web_fetch` → `{ url, statusCode, body, truncated }` · `job_output` → `{ text, job }` · `read_image` → `{ path, image }` · `skill` → `{ name, content }`.",
633
+ python ? "- Read the field: `page[\"lines\"]`, `match[\"text\"]`. Calling `.split()`, `.splitlines()` or `in` on the envelope itself is always wrong." : "- Read the field: `r.lines.map((l) => l.text)`. Calling `.map`, `.filter`, `.split` or `in` on the envelope itself is always wrong.",
634
+ "- Anything not listed above (MCP tools, hidden plugin tools) answers with its **declared output schema**: call `" + describeTool + "({ name })` and read the top-level keys before picking a field. One discovery call is far cheaper than the `TypeError` that loses the whole program.",
628
635
  "- Discovery returns whole objects too: `" + listTool + "` answers `{ tools: [{ name, description, namespace }] }`, `" + searchTool + "` answers `{ tools: [{ name, namespace, description, parameters, output? }], fallback? }`, and `" + describeTool + "` answers `{ found, name?, namespace?, description?, parameters?, output? }`.",
629
636
  "- Tool argument objects and `return` values must be lossless JSON: **never leave `undefined` in an object property value** — `{ a: x.opt }` fails the call when `x.opt === undefined`. Omit the key, write `x.opt ?? null`, or print it with `console.log`.",
637
+ "- Parameter keys: `read`, `edit`, `write`, and `read_image` take `file_path` (not `path`); `glob` and `grep` take `path`; `win_bash` takes `workdir` (not `cwd`).",
638
+ ...python ? [] : [
639
+ "- Paths and regexes: write a Windows path with forward slashes (`C:/Users/...`) or wrap it in `String.raw`; a literal `C:\\Users` inside an ordinary string is read as the escape `\\U` and the program never starts (`Expected unicode escape`). Regex backslashes (`\\d`, `\\.`) have the same trap — double them or use `String.raw`.",
640
+ "- A program is not a module: `import('./src/x.ts')` and `import('vscode')` cannot resolve — the program runs in a fresh process with no module graph. Read files with `tools.read`; `await import('node:fs')` works only for Node built-ins.",
641
+ "- `process.env` is empty inside the program: `os.tmpdir()` and credentialed CLIs (`gh`, `git`, `npm`) spawned via `node:child_process` miss `TEMP`, `HOME`, `APPDATA`, and auth tokens. Run shell and CLI commands through `await tools.win_bash({ command })` instead."
642
+ ],
630
643
  "- Prefer batching multi-step read/search chains inside one " + run + " program instead of issuing many sequential single-tool turns.",
631
644
  "- Independent read-only calls may overlap under Promise.all; mutating calls run alone, in submission order.",
632
645
  "- A failed sub-call rejects; try/catch or Promise.allSettled it to keep the calls that succeeded. Completed calls are real side effects and are not rolled back.",
@@ -799,7 +812,10 @@ function createCodemodeExecutor(ctx, rules, hooks) {
799
812
  additionalContexts: [],
800
813
  concluded: false
801
814
  };
802
- }
815
+ },
816
+ ...typeof hooks.reset === "function" ? { async reset(agent) {
817
+ await hooks.reset(agent);
818
+ } } : {}
803
819
  };
804
820
  }
805
821
  /**
@@ -847,169 +863,6 @@ function sectionOrderOf(ctx) {
847
863
  }
848
864
  }
849
865
  //#endregion
850
- //#region src/host/codemode/retention.ts
851
- /**
852
- * 造一个常驻环境管理器。
853
- *
854
- * @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
855
- * @returns 管理句柄。
856
- */
857
- function createRetention(runtimeOf) {
858
- const live = /* @__PURE__ */ new Set();
859
- const released = /* @__PURE__ */ new WeakSet();
860
- const release = async (owner) => {
861
- if (released.has(owner)) return;
862
- released.add(owner);
863
- live.delete(owner);
864
- const runtime = runtimeOf();
865
- if (runtime?.release === void 0) return;
866
- try {
867
- await runtime.release(owner);
868
- } catch {}
869
- };
870
- return {
871
- owner(agent) {
872
- const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
873
- released.delete(owner);
874
- live.add(owner);
875
- return owner;
876
- },
877
- release,
878
- async dispose() {
879
- const owners = [...live];
880
- await Promise.all(owners.map((owner) => release(owner)));
881
- }
882
- };
883
- }
884
- /** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
885
- const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
886
- //#endregion
887
- //#region src/host/codemode/scope.ts
888
- /**
889
- * 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
890
- *
891
- * ## 为什么要这么绕
892
- *
893
- * 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
894
- * 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
895
- * 都不导出。能用的只有两件事:
896
- *
897
- * 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
898
- * 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
899
- *
900
- * 于是归属判定变成两次公开查询:
901
- *
902
- * - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
903
- * - 只有本预设视图能解析 → 本预设自己注册的;
904
- * - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
905
- *
906
- * 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
907
- * 纯名字匹配,绝不因为读不到归属就误遮蔽。
908
- *
909
- * @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
910
- */
911
- /** 内核写作用域标签用的符号描述。 */
912
- const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
913
- /**
914
- * 从一个 ctx 上找出「本包所在作用域」的 key。
915
- *
916
- * Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
917
- * 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
918
- * 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
919
- *
920
- * @param ctx - 本包的行上下文。
921
- * @returns 作用域 key;读不到时为 `undefined`。
922
- */
923
- function findScopeKey(ctx) {
924
- if (typeof ctx !== "object" || ctx === null) return void 0;
925
- return scanForScope(ctx, 0);
926
- }
927
- /** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
928
- function scanForScope(source, depth) {
929
- if (depth > 4) return void 0;
930
- let symbols;
931
- try {
932
- symbols = Object.getOwnPropertySymbols(source);
933
- } catch {
934
- return;
935
- }
936
- for (const symbol of symbols) {
937
- if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
938
- let value;
939
- try {
940
- value = source[symbol];
941
- } catch {
942
- continue;
943
- }
944
- if (typeof value === "object" && value !== null) return value;
945
- }
946
- const proto = Object.getPrototypeOf(source);
947
- if (proto === null || proto === Object.prototype) return void 0;
948
- return scanForScope(proto, depth + 1);
949
- }
950
- /**
951
- * 为一个名字清单建立归属视图。
952
- *
953
- * @param tools - 工具注册表(结构探测,不看类身份)。
954
- * @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
955
- * @param names - 要判定的名字(通常是本步的模型可见清单)。
956
- * @returns 归属视图;读不到任何归属时返回禁用视图。
957
- */
958
- function resolveCatalogView(tools, scopeKey, names) {
959
- const registry = asRegistry(tools);
960
- if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
961
- const globalNames = /* @__PURE__ */ new Set();
962
- const presetNames = /* @__PURE__ */ new Set();
963
- for (const name of names) {
964
- if (typeof name !== "string" || name.length === 0) continue;
965
- if (probes(registry, name, void 0)) globalNames.add(name);
966
- if (probes(registry, name, scopeKey)) presetNames.add(name);
967
- }
968
- return {
969
- enabled: true,
970
- globalNames,
971
- presetNames
972
- };
973
- }
974
- /**
975
- * 判定一个名字在**调用时**是否属于本预设之外的层。
976
- *
977
- * 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
978
- * 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
979
- *
980
- * @param tools - 工具注册表。
981
- * @param scopeKey - 本预设的作用域 key。
982
- * @param name - 工具名。
983
- * @returns 归属视图(只含这一个名字)。
984
- */
985
- function resolveNameView(tools, scopeKey, name) {
986
- const registry = asRegistry(tools);
987
- if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
988
- const globalNames = /* @__PURE__ */ new Set();
989
- const presetNames = /* @__PURE__ */ new Set();
990
- if (probes(registry, name, void 0)) globalNames.add(name);
991
- if (probes(registry, name, scopeKey)) presetNames.add(name);
992
- return {
993
- enabled: true,
994
- globalNames,
995
- presetNames
996
- };
997
- }
998
- /** 一次公开查询:解析得到定义时为 `true`。 */
999
- function probes(registry, name, scope) {
1000
- try {
1001
- return registry.get(name, scope) !== void 0;
1002
- } catch {
1003
- return false;
1004
- }
1005
- }
1006
- /** 结构探测:读得到 `get` 就当注册表用。 */
1007
- function asRegistry(tools) {
1008
- if (typeof tools !== "object" || tools === null) return void 0;
1009
- if (typeof tools.get !== "function") return void 0;
1010
- return tools;
1011
- }
1012
- //#endregion
1013
866
  //#region src/host/codemode/transport.ts
1014
867
  /**
1015
868
  * Code Mode 的程序传输工具 `code_run`。
@@ -1051,8 +904,41 @@ const catalogScope = new AsyncLocalStorage();
1051
904
  function currentCatalogSnapshot() {
1052
905
  return catalogScope.getStore();
1053
906
  }
907
+ /** 传输工具名(与 {@link DEFAULT_RUN_TOOL_NAME} 一致,导出方便测试引用)。 */
908
+ const TRANSPORT_TOOL_NAME = "code_run";
909
+ /** 进展汇报工具名:融合参数里救回来的简报用它补发。 */
910
+ const PROGRESS_UPDATE_TOOL = "progress_update";
1054
911
  /** 允许单次提权的目标沙箱模式(对齐 `@deepseek-ai/dsh-sandbox` 的 `ESCALATION_TARGETS`)。 */
1055
912
  const ESCALATION_TARGETS = Object.freeze(["workspace-write", "danger-full-access"]);
913
+ /**
914
+ * 子调用配额耗尽时的宿主中止原因(对齐 `@deepseek-ai/dsh-tool-executor-ptc` 的 `ToolCallLimitExceededError`)。
915
+ *
916
+ * 超出的那一次调用不会派发;结算阶段识别此错误并保留完整诊断,防止常驻模式下的泛化取消文案吞掉真实原因。
917
+ */
918
+ var ToolCallLimitExceededError = class extends Error {
919
+ constructor(limit, callNumber, callsPerMinute, runToolName = TRANSPORT_TOOL_NAME) {
920
+ super(`${runToolName} exceeded its limit of ${String(limit)} tool calls (${String(callsPerMinute)} per minute of its timeout); call ${String(callNumber)} was not dispatched. The ${runToolName} program may contain an infinite loop. Check loop termination or split the work into smaller ${runToolName} calls.`);
921
+ this.name = "ToolCallLimitExceededError";
922
+ }
923
+ };
924
+ /**
925
+ * 程序运行自身失败时抛出的结构化错误(对齐内核 `CodeRunFailedError`,`code: 'CODE_RUN_FAILED'`)。
926
+ */
927
+ var CodeRunFailedError = class extends HarnessError {
928
+ constructor(message) {
929
+ super(message, "CODE_RUN_FAILED");
930
+ this.name = "CodeRunFailedError";
931
+ }
932
+ };
933
+ /**
934
+ * 常驻环境重置不支持或失败时抛出的结构化错误(对齐内核 `CodeResetFailedError`,`code: 'CODE_RESET_FAILED'`)。
935
+ */
936
+ var CodeResetFailedError = class extends HarnessError {
937
+ constructor(message) {
938
+ super(message, "CODE_RESET_FAILED");
939
+ this.name = "CodeResetFailedError";
940
+ }
941
+ };
1056
942
  /** 与内核 `run_code` 一致的 `description` 参数说明。 */
1057
943
  const DESCRIPTION_PARAM_DESCRIPTION = "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\".";
1058
944
  function buildFlavor(languageName, execution) {
@@ -1177,24 +1063,25 @@ function createTransportDefinition(host, rules) {
1177
1063
  }
1178
1064
  },
1179
1065
  presentCall(args) {
1180
- if (typeof args !== "object" || args === null) return void 0;
1181
- const record = args;
1182
- const code = record["code"];
1183
- const description = record["description"];
1184
- if (typeof code !== "string" || typeof description !== "string" || description.trim().length === 0) return;
1185
- return {
1186
- card: "generic",
1187
- title: description,
1188
- kind: "execute",
1189
- rawInput: code
1190
- };
1066
+ try {
1067
+ const input = readArguments(args);
1068
+ return {
1069
+ card: "generic",
1070
+ title: input.description,
1071
+ kind: "execute",
1072
+ rawInput: input.code
1073
+ };
1074
+ } catch {
1075
+ return;
1076
+ }
1191
1077
  },
1192
1078
  async execute(args, exec) {
1193
1079
  const input = readArguments(args);
1194
1080
  const runtime = host.runtime();
1195
1081
  if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
1196
1082
  if (input.timeoutMs !== void 0 && runtime.timeout !== void 0 === false && isStrictTimeoutUnsupported(runtime)) throw new Error("timeoutMs is not available for this PTC runtime");
1197
- const catalog = host.catalog();
1083
+ await host.awaitPredecessors?.(exec.agent, exec.signal);
1084
+ const catalog = host.catalog(exec.agent, exec.toolSet);
1198
1085
  const runController = new AbortController();
1199
1086
  const onOuterAbort = () => {
1200
1087
  runController.abort(exec.signal.reason);
@@ -1202,6 +1089,7 @@ function createTransportDefinition(host, rules) {
1202
1089
  if (exec.signal.aborted) runController.abort(exec.signal.reason);
1203
1090
  else exec.signal.addEventListener("abort", onOuterAbort, { once: true });
1204
1091
  const nestedSignal = markNested(runController.signal) ?? runController.signal;
1092
+ if (input.narration !== void 0) await forwardNarration(host, input.narration, exec, nestedSignal);
1205
1093
  const owner = host.stateOwner(exec.agent);
1206
1094
  let maxSubCalls = rules.options.maxSubCalls;
1207
1095
  let attempts = 0;
@@ -1215,11 +1103,11 @@ function createTransportDefinition(host, rules) {
1215
1103
  if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " not dispatched");
1216
1104
  const n = ++attempts;
1217
1105
  if (n > maxSubCalls) {
1218
- const limitErr = /* @__PURE__ */ new Error("program sub-call limit exceeded: " + String(maxSubCalls) + " calls");
1106
+ const limitErr = new ToolCallLimitExceededError(maxSubCalls, n, rules.options.maxSubCallsPerMinute, runToolName);
1219
1107
  runController.abort(limitErr);
1220
1108
  throw limitErr;
1221
1109
  }
1222
- const parsed = losslessArgs(rawArgs);
1110
+ const parsed = normalizeSubCallArgs(definitions.get(entry.name), losslessArgs(rawArgs));
1223
1111
  const loggedArgs = losslessArgs(parsed);
1224
1112
  const callId = String(exec.callId) + NESTED_CALL_ID_MARKER + String(n);
1225
1113
  const dispatchLog = createDispatchLog(exec.agent, {
@@ -1229,8 +1117,8 @@ function createTransportDefinition(host, rules) {
1229
1117
  name: entry.name,
1230
1118
  arguments: loggedArgs
1231
1119
  });
1232
- const mode = isConcurrencySafe(definitions.get(entry.name), parsed) ? "parallel" : "exclusive";
1233
- return lane.schedule(mode, {
1120
+ return lane.schedule({
1121
+ classify: () => isConcurrencySafe(definitions.get(entry.name), parsed) ? "parallel" : "exclusive",
1234
1122
  start() {
1235
1123
  dispatchLog.start();
1236
1124
  },
@@ -1245,35 +1133,39 @@ function createTransportDefinition(host, rules) {
1245
1133
  /**
1246
1134
  * 提交阶段:按**提交顺序**逐条执行,落在会话里的副作用因此与提交顺序一致。
1247
1135
  *
1248
- * 内核把这几步全部放在同一条有序 lane 里(`ptc.ts` 的 commit 游标):回放内容
1249
- * 投影、settle 事件、图片与附加上下文、`concludeTurn`、放行互斥屏障。并发的
1250
- * body 谁先跑完都不影响这一串的顺序——否则界面里的子调用记录会错位,延迟上下文
1251
- * 也会插到错误的位置。
1136
+ * 对齐内核 `ptc.ts:537-634`:延迟上下文(图片与 `additionalContexts`)与 `concludeTurn`
1137
+ * 按提交顺序同步生效后,**立刻**向程序返回子调用结果,避免慢速日志落盘监听器阻塞程序
1138
+ * 后续执行;`shapeDispatchLog` 与 `tool/ptc-dispatch` 事件通过带背压的有序 `logWork`
1139
+ * 队列异步结算,并在 `lane.drain()` 中完整排空。
1252
1140
  */
1253
- async commit(parked) {
1141
+ commit(parked, enqueueLog) {
1254
1142
  if (!parked.ok) {
1255
- dispatchLog.settle({ error: messageOf$1(parked.error) });
1143
+ enqueueLog(async () => {
1144
+ dispatchLog.settle({ error: messageOf$2(parked.error) });
1145
+ });
1256
1146
  throw parked.error;
1257
1147
  }
1258
1148
  const outcome = parked.value;
1259
1149
  const isError = outcome.error !== void 0;
1260
- const content = await shapeDispatchContent(host, {
1261
- agent: exec.agent,
1262
- name: entry.name,
1263
- arguments: loggedArgs,
1264
- isError,
1265
- content: clipContent(outcome.content)
1266
- });
1267
- dispatchLog.settle({
1268
- ...outcome,
1269
- content
1270
- });
1271
1150
  if (!isError && Array.isArray(outcome.content) && hasImageBlock(outcome.content)) {
1272
1151
  const imageMsg = host.createImageContext?.(outcome.content);
1273
1152
  if (imageMsg !== void 0) exec.deferContext?.(imageMsg);
1274
1153
  }
1275
1154
  if (Array.isArray(outcome.additionalContexts)) for (const contextMsg of outcome.additionalContexts) exec.deferContext?.(contextMsg);
1276
1155
  if (!isError && outcome.concluded === true) exec.concludeTurn?.();
1156
+ enqueueLog(async () => {
1157
+ const content = await shapeDispatchContent(host, {
1158
+ agent: exec.agent,
1159
+ name: entry.name,
1160
+ arguments: loggedArgs,
1161
+ isError,
1162
+ content: clipContent(outcome.content)
1163
+ });
1164
+ dispatchLog.settle({
1165
+ ...outcome,
1166
+ content
1167
+ });
1168
+ });
1277
1169
  if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " result discarded");
1278
1170
  if (outcome.error !== void 0) throw new Error(outcome.error);
1279
1171
  return outcome.value !== void 0 ? outcome.value : outcome.text ?? null;
@@ -1298,7 +1190,7 @@ function createTransportDefinition(host, rules) {
1298
1190
  * `stateOwner`,避免把单次授权固化到常驻环境中(对齐内核 `ptc.ts`)。
1299
1191
  */
1300
1192
  let approvedEscalatedMode;
1301
- /** 这次运行在入口那边的登记;结算时释放(重跑会重新登记一次)。 */
1193
+ /** 这次运行在入口那边的登记;结算时释放(重跑原位更新 mode,外层 finally 统一释放)。 */
1302
1194
  let untrackRun = () => {};
1303
1195
  const attempt = async () => {
1304
1196
  const authority = await host.runAuthority?.(exec.agent, nestedSignal);
@@ -1325,11 +1217,11 @@ function createTransportDefinition(host, rules) {
1325
1217
  };
1326
1218
  escalated = modeOfPolicy(effectivePolicy) !== modeOfPolicy(standingPolicy);
1327
1219
  }
1328
- untrackRun();
1329
1220
  const runMode = modeOfPolicy(effectivePolicy);
1330
1221
  untrackRun = host.trackRun?.({
1331
1222
  agent: exec.agent,
1332
1223
  controller: runController,
1224
+ signal: nestedSignal,
1333
1225
  ...runMode === void 0 ? {} : { mode: runMode }
1334
1226
  }) ?? (() => {});
1335
1227
  const request = {
@@ -1368,10 +1260,17 @@ function createTransportDefinition(host, rules) {
1368
1260
  untrackRun();
1369
1261
  }
1370
1262
  if (result.error !== void 0) {
1263
+ const limitError = runController.signal.reason instanceof ToolCallLimitExceededError ? runController.signal.reason : void 0;
1264
+ if (result.error.kind === "abort" && exec.signal.aborted && limitError === void 0) {
1265
+ const abortError = new HarnessError(runtime.retainsState ? `tool call aborted: ${result.error.message}` : "tool call aborted", "ABORTED");
1266
+ abortError.name = "AbortError";
1267
+ throw abortError;
1268
+ }
1371
1269
  const logsText = Array.isArray(result.logs) && result.logs.length > 0 ? "\nCaptured output:\n" + result.logs.join("\n") : "";
1372
1270
  const sandboxText = result.sandbox === void 0 ? "" : "\nFile sandbox: " + result.sandbox.mode + (result.sandbox.enforcement === void 0 ? "" : "; enforcement: " + result.sandbox.enforcement) + (result.sandbox.denied ? "; operation denied" : "") + ".";
1373
1271
  const hint = result.sandbox?.denied ? escalationGuidance(runtime.sandboxMode) : "";
1374
- throw new Error("program failed (" + result.error.kind + "): " + result.error.message + logsText + sandboxText + hint);
1272
+ const failureText = limitError === void 0 || limitError.message === result.error.message ? result.error.message : `${limitError.message}\n${result.error.message}`;
1273
+ throw new CodeRunFailedError("program failed (" + result.error.kind + "): " + failureText + logsText + sandboxText + hint);
1375
1274
  }
1376
1275
  return {
1377
1276
  logs: [...result.logs ?? []],
@@ -1504,32 +1403,257 @@ function validateEscalationArgs(sandboxPermissions, justification) {
1504
1403
  justification
1505
1404
  };
1506
1405
  }
1406
+ /** 程序源码可能出现的键名。模型偶尔把源码写进 `program` / `source` / `script`。 */
1407
+ const CODE_KEYS = Object.freeze([
1408
+ "code",
1409
+ "program",
1410
+ "source",
1411
+ "script",
1412
+ "code_run",
1413
+ "codecode",
1414
+ "program_source",
1415
+ "programSource"
1416
+ ]);
1417
+ /** 模型把「先汇报再执行」写成一次调用时,简报落在 `progress_update` 的那两个键上。 */
1418
+ const NARRATION_TITLE_KEY = "title";
1419
+ /** 见 {@link NARRATION_TITLE_KEY}。 */
1420
+ const NARRATION_MESSAGE_KEY = "message";
1421
+ /** 回显原始参数时最多带多少字符。 */
1422
+ const RAW_EXCERPT_LIMIT = 120;
1423
+ /** 一个值是不是普通对象。 */
1424
+ function isRecord(value) {
1425
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1426
+ }
1427
+ /** 尝试解析一段 JSON 文本。 */
1428
+ function tryJson(text) {
1429
+ try {
1430
+ return JSON.parse(text);
1431
+ } catch {
1432
+ return;
1433
+ }
1434
+ }
1435
+ /**
1436
+ * 切出一段文本里的顶层平衡 JSON 对象片段。
1437
+ *
1438
+ * 模型偶尔把两次调用的参数写进同一段文本(`{"title":…,"message":…}{"code":…}`),
1439
+ * 整段 `JSON.parse` 必然失败;按花括号配对切出来的每一段却都还能解析,程序因此可救。
1440
+ *
1441
+ * @param text - 待切分的原始参数文本。
1442
+ * @returns 按出现顺序排列的片段(字符串内的花括号不算)。
1443
+ */
1444
+ function topLevelJsonObjects(text) {
1445
+ const fragments = [];
1446
+ let depth = 0;
1447
+ let start = -1;
1448
+ let inString = false;
1449
+ let escaped = false;
1450
+ for (let index = 0; index < text.length; index += 1) {
1451
+ const ch = text[index];
1452
+ if (inString) {
1453
+ if (escaped) escaped = false;
1454
+ else if (ch === "\\") escaped = true;
1455
+ else if (ch === "\"") inString = false;
1456
+ continue;
1457
+ }
1458
+ if (ch === "\"") {
1459
+ inString = true;
1460
+ continue;
1461
+ }
1462
+ if (ch === "{") {
1463
+ if (depth === 0) start = index;
1464
+ depth += 1;
1465
+ continue;
1466
+ }
1467
+ if (ch !== "}") continue;
1468
+ if (depth === 0) continue;
1469
+ depth -= 1;
1470
+ if (depth === 0 && start >= 0) {
1471
+ fragments.push(text.slice(start, index + 1));
1472
+ start = -1;
1473
+ }
1474
+ }
1475
+ return fragments;
1476
+ }
1477
+ /** 从一个参数对象里读出程序源码。 */
1478
+ function codeOf(record) {
1479
+ for (const key of CODE_KEYS) {
1480
+ const value = record[key];
1481
+ if (typeof value === "string" && value.trim().length > 0) return value;
1482
+ }
1483
+ }
1484
+ /** 从一个参数对象里读出进展简报(`title` / `message` 至少有一个)。 */
1485
+ function narrationOf(record) {
1486
+ const title = stringOf(record[NARRATION_TITLE_KEY]);
1487
+ const message = stringOf(record[NARRATION_MESSAGE_KEY]);
1488
+ if (title === void 0 && message === void 0) return void 0;
1489
+ return {
1490
+ title: title ?? message.slice(0, 40),
1491
+ message: message ?? title
1492
+ };
1493
+ }
1494
+ /** 一个非空字符串(**不裁剪**内容,源码要原样送出)。 */
1495
+ function stringOf(value) {
1496
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
1497
+ }
1498
+ /** 错误回显里的一段原始文本。 */
1499
+ function excerpt(text) {
1500
+ const flat = text.replace(/[\r\n]+/g, "⏎").replace(/\s+/g, " ").trim();
1501
+ return flat.length <= RAW_EXCERPT_LIMIT ? flat : flat.slice(0, RAW_EXCERPT_LIMIT) + "…";
1502
+ }
1503
+ /** 参数完全不成形时的错误。 */
1504
+ function invalidArgumentsError(args, raw, keys) {
1505
+ if (args === null) return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received null)");
1506
+ if (typeof args !== "string") return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received " + typeof args + ")");
1507
+ if (keys !== void 0 && keys.length > 0) return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received keys: " + keys.join(", ") + ")");
1508
+ if (raw.trim().length === 0) return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received an empty string)");
1509
+ return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received a string that is not a JSON object: " + excerpt(raw) + ")");
1510
+ }
1511
+ /** 参数里没有程序源码、又不是可救形态时的错误。 */
1512
+ function missingCodeError(record, raw) {
1513
+ const keys = Object.keys(record);
1514
+ const note = keys.length === 0 ? "" : " (received keys: " + keys.join(", ") + (raw.trim().length > 0 ? "; raw: " + excerpt(raw) : "") + "; pass program source in \"code\")";
1515
+ return /* @__PURE__ */ new Error("codemode: invalid code: expected a non-empty string" + note);
1516
+ }
1517
+ /** 一段文本像不像程序源码(模型把源码直接发成字符串时判这个)。 */
1518
+ function looksLikeProgram(text) {
1519
+ return text.includes("\n") || /\b(await|return|const|let|function|import|console\.)\b/.test(text);
1520
+ }
1521
+ /** 从源码首行兜底出一句描述。 */
1522
+ function descriptionFromCode(code) {
1523
+ const cleaned = (code.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0) ?? "").replace(/^(\/\/|#)\s*/, "").slice(0, 80);
1524
+ return cleaned.length > 0 ? cleaned : "code program";
1525
+ }
1526
+ /**
1527
+ * 把工具收到的原始参数读成一个对象。
1528
+ *
1529
+ * ## 为什么要容错
1530
+ *
1531
+ * 模型有四种真实存在的发送形态,照搬内核 PTC 的严格 schema 会把它们全判死:
1532
+ *
1533
+ * 1. **原生对象**(正常);
1534
+ * 2. **字符串里的 JSON**(执行器把解析失败的文本原样传下来);
1535
+ * 3. **两段 JSON 拼接**——`{"title":…,"message":…}{"code":…}`,模型想在同一次调用里
1536
+ * 既汇报又执行,整段解析必失败,但按花括号切开后两段都合法(实测 7/7 可救);
1537
+ * 4. **只有一段纯程序**(模型直接把源码当参数发)。
1538
+ *
1539
+ * @param args - 工具收到的原始参数。
1540
+ * @returns 参数对象、可选的简报与来源形态。
1541
+ */
1542
+ function parseArguments(args) {
1543
+ if (isRecord(args)) return {
1544
+ record: args,
1545
+ origin: "object",
1546
+ raw: ""
1547
+ };
1548
+ if (typeof args !== "string") throw invalidArgumentsError(args, "");
1549
+ const raw = args;
1550
+ const trimmed = raw.trim();
1551
+ if (trimmed.length === 0) throw invalidArgumentsError(args, raw);
1552
+ const parsed = tryJson(trimmed);
1553
+ if (isRecord(parsed)) {
1554
+ const narration = narrationOf(parsed);
1555
+ return {
1556
+ record: parsed,
1557
+ origin: "json",
1558
+ raw,
1559
+ ...narration === void 0 ? {} : { narration }
1560
+ };
1561
+ }
1562
+ if (Array.isArray(parsed)) {
1563
+ const hit = parsed.find((entry) => isRecord(entry) && codeOf(entry) !== void 0);
1564
+ if (isRecord(hit)) {
1565
+ const narration = narrationOf(hit);
1566
+ return {
1567
+ record: hit,
1568
+ origin: "json",
1569
+ raw,
1570
+ ...narration === void 0 ? {} : { narration }
1571
+ };
1572
+ }
1573
+ }
1574
+ const fragments = topLevelJsonObjects(trimmed);
1575
+ let chosen;
1576
+ let narration;
1577
+ for (const fragment of fragments) {
1578
+ const value = tryJson(fragment);
1579
+ if (!isRecord(value)) continue;
1580
+ if (codeOf(value) !== void 0) {
1581
+ chosen = value;
1582
+ narration ??= narrationOf(value);
1583
+ continue;
1584
+ }
1585
+ narration ??= narrationOf(value);
1586
+ }
1587
+ if (chosen !== void 0) return {
1588
+ record: chosen,
1589
+ origin: fragments.length > 1 ? "fused" : "json",
1590
+ raw,
1591
+ ...narration === void 0 ? {} : { narration }
1592
+ };
1593
+ if (looksLikeProgram(trimmed)) return {
1594
+ record: { code: raw },
1595
+ origin: "bare",
1596
+ raw
1597
+ };
1598
+ throw invalidArgumentsError(args, raw, keysOf(fragments));
1599
+ }
1600
+ /** 把若干 JSON 片段里出现过的键并起来(错误回显用)。 */
1601
+ function keysOf(fragments) {
1602
+ const keys = /* @__PURE__ */ new Set();
1603
+ for (const fragment of fragments) {
1604
+ const value = tryJson(fragment);
1605
+ if (!isRecord(value)) continue;
1606
+ for (const key of Object.keys(value)) keys.add(key);
1607
+ }
1608
+ return [...keys];
1609
+ }
1507
1610
  /** 校验并读出工具参数。 */
1508
1611
  function readArguments(args) {
1509
- if (typeof args !== "object" || args === null) throw new Error("codemode: expected an arguments object");
1510
- const record = args;
1511
- const code = record["code"];
1512
- const description = record["description"];
1513
- if (typeof code !== "string" || code.trim().length === 0) {
1514
- const received = Object.keys(record);
1515
- throw new Error("codemode: invalid code: expected a non-empty string" + (received.length === 0 ? "" : " (received keys: " + received.join(", ") + "; pass program source in \"code\")"));
1516
- }
1517
- if (typeof description !== "string" || description.trim().length === 0) throw new Error("codemode: invalid description: expected a non-empty string");
1612
+ const parsed = parseArguments(args);
1613
+ const record = parsed.record;
1614
+ const code = codeOf(record);
1615
+ if (code === void 0) throw missingCodeError(record, parsed.raw);
1616
+ const description = stringOf(record["description"])?.trim() ?? stringOf(record["desc"])?.trim() ?? stringOf(record["summary"])?.trim() ?? parsed.narration?.title ?? descriptionFromCode(code);
1518
1617
  const escalation = validateEscalationArgs(record["sandbox_permissions"], record["justification"]);
1519
1618
  const timeoutMs = record["timeoutMs"];
1520
- if (timeoutMs === void 0) return {
1521
- code,
1522
- description,
1523
- ...escalation
1524
- };
1525
- if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
1619
+ if (timeoutMs !== void 0) {
1620
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
1621
+ }
1526
1622
  return {
1527
1623
  code,
1528
1624
  description,
1529
- timeoutMs,
1530
- ...escalation
1625
+ ...timeoutMs === void 0 ? {} : { timeoutMs },
1626
+ ...escalation,
1627
+ ...parsed.narration === void 0 ? {} : { narration: parsed.narration }
1531
1628
  };
1532
1629
  }
1630
+ /**
1631
+ * 把救回来的进展简报补发成一次 `progress_update`。
1632
+ *
1633
+ * 简报只影响界面,因此这里的任何失败都被吞掉:发不出去(没有投影、没有服务端、外层已取消)
1634
+ * 绝不能连累这次程序。
1635
+ *
1636
+ * @param host - 运行环境句柄。
1637
+ * @param narration - 从融合参数里救回来的简报。
1638
+ * @param exec - 本次执行上下文。
1639
+ * @param signal - 已带嵌套标记的取消信号。
1640
+ */
1641
+ async function forwardNarration(host, narration, exec, signal) {
1642
+ try {
1643
+ if (!host.catalog().some((entry) => entry.name === PROGRESS_UPDATE_TOOL)) return;
1644
+ await host.callTool({
1645
+ name: PROGRESS_UPDATE_TOOL,
1646
+ args: {
1647
+ title: narration.title,
1648
+ message: narration.message
1649
+ },
1650
+ callId: String(exec.callId) + ":ptc:0",
1651
+ signal,
1652
+ agent: exec.agent,
1653
+ toolSet: exec.toolSet
1654
+ });
1655
+ } catch {}
1656
+ }
1533
1657
  /** 子调用参数必须是可无损 JSON 化的值。 */
1534
1658
  function losslessArgs(value) {
1535
1659
  if (value === void 0) return {};
@@ -1539,29 +1663,95 @@ function losslessArgs(value) {
1539
1663
  throw new Error("codemode: tool arguments must be lossless JSON");
1540
1664
  }
1541
1665
  }
1542
- /** 一个工具的调用是否可以与兄弟调用重叠(只有明确 true 才算)。 */
1543
- function isConcurrencySafe(entry, args) {
1544
- if (entry === void 0 || typeof entry.isConcurrencySafe !== "function") return false;
1545
- try {
1546
- return entry.isConcurrencySafe(args) === true;
1547
- } catch {
1548
- return false;
1666
+ /** 内置按单文件 `file_path` 寻址的工具名。 */
1667
+ const FILE_PATH_TOOLS = /* @__PURE__ */ new Set([
1668
+ "read",
1669
+ "edit",
1670
+ "write",
1671
+ "read_image"
1672
+ ]);
1673
+ /** 内置按目录/目标 `path` 寻址的检索工具名。 */
1674
+ const SEARCH_PATH_TOOLS = /* @__PURE__ */ new Set(["glob", "grep"]);
1675
+ /**
1676
+ * 宿主层子调用入参别名容错:把模型在 `code_run` 内高频写错的参数键名映射到工具真实声明的键名。
1677
+ *
1678
+ * 审计证据(`session-e190c243` seq 171-182 等):模型在 `Promise.all` 里容易把 `tools.read` 的
1679
+ * `file_path` 写成 `glob`/`grep` 的 `path`(或反过来),或者把 snake_case 写成 camelCase,导致整段
1680
+ * 程序因单个属性名不匹配而抛 `ToolCallError: invalid arguments: missing required property "file_path"`。
1681
+ *
1682
+ * 规则:仅当目标规范键尚未提供(`=== undefined`)、别名键存在且类型匹配、且别名键本身不是该工具声明的
1683
+ * 独立参数时,才把值搬运到规范键并删掉多余别名键(防止 `additionalProperties: false` 拒收)。
1684
+ *
1685
+ * @param entry - 工具清单条目(含参数 schema)。
1686
+ * @param value - 已脱离引用的 JSON 参数值。
1687
+ * @returns 归一化后的参数值。
1688
+ */
1689
+ function normalizeSubCallArgs(entry, value) {
1690
+ if (!isRecord(value)) return value;
1691
+ const name = entry?.name ?? "";
1692
+ const schemaProps = isRecord(entry?.parameters) && isRecord(entry.parameters.properties) ? entry.parameters.properties : void 0;
1693
+ const declares = (key) => schemaProps !== void 0 && Object.prototype.hasOwnProperty.call(schemaProps, key);
1694
+ const out = { ...value };
1695
+ const remap = (target, aliases, predicate) => {
1696
+ if (out[target] !== void 0) return;
1697
+ for (const alias of aliases) {
1698
+ if (declares(alias)) continue;
1699
+ const candidate = out[alias];
1700
+ if (predicate(candidate)) {
1701
+ out[target] = candidate;
1702
+ delete out[alias];
1703
+ return;
1704
+ }
1705
+ }
1706
+ };
1707
+ const isString = (v) => typeof v === "string";
1708
+ const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
1709
+ const isBoolean = (v) => typeof v === "boolean";
1710
+ if (FILE_PATH_TOOLS.has(name) || declares("file_path") && !declares("path")) remap("file_path", ["path", "filePath"], isString);
1711
+ else if (SEARCH_PATH_TOOLS.has(name) || declares("path") && !declares("file_path")) remap("path", ["file_path", "filePath"], isString);
1712
+ if (name === "edit" || declares("old_string") || declares("new_string")) {
1713
+ remap("old_string", ["oldString"], isString);
1714
+ remap("new_string", ["newString"], isString);
1715
+ remap("replace_all", ["replaceAll"], isBoolean);
1716
+ }
1717
+ if (name === "win_bash" || declares("workdir")) {
1718
+ remap("workdir", [
1719
+ "cwd",
1720
+ "workingDirectory",
1721
+ "working_directory"
1722
+ ], isString);
1723
+ remap("timeoutMs", ["timeout_ms"], isNumber);
1724
+ remap("run_in_background", ["runInBackground"], isBoolean);
1725
+ }
1726
+ if (name === "job_output" || name === "job_kill" || declares("job_id")) {
1727
+ remap("job_id", ["jobId"], isString);
1728
+ if (name === "job_output" || declares("timeout_ms")) remap("timeout_ms", ["timeoutMs"], isNumber);
1729
+ }
1730
+ return out;
1731
+ }
1732
+ /** 一个工具的调用是否可以与兄弟调用重叠(只有明确 true 才算)。 */
1733
+ function isConcurrencySafe(entry, args) {
1734
+ if (entry === void 0 || typeof entry.isConcurrencySafe !== "function") return false;
1735
+ try {
1736
+ return entry.isConcurrencySafe(args) === true;
1737
+ } catch {
1738
+ return false;
1549
1739
  }
1550
1740
  }
1551
1741
  /**
1552
- * 子调用的并发、提交顺序与互斥屏障(对齐内核 `ptc.ts` 的 ordered commit lane)。
1742
+ * 子调用的并发、提交顺序与互斥屏障(对齐内核 `ptc.ts` 的 ordered commit lane + `logWork` 背压)。
1553
1743
  *
1744
+ * - `classify()` 在队列头部真正出队启动时惰性求值,保证前序调用产生的状态变更可被观察;
1554
1745
  * - `start()`(记 start 事件 + 启动 body)在 lane 里按提交顺序执行:`parallel` body 之间
1555
1746
  * 可以并发重叠,最多 `maxParallel` 个同时在飞;
1556
- * - `commit()`(记 settle 事件 + 转发图片/附加上下文 + `concludeTurn` + 放行互斥屏障)
1557
- * 按**提交顺序**逐条执行:前一条没提交完,后一条的结果不会先落到会话里;
1747
+ * - `commit()`(转发图片/附加上下文 + `concludeTurn` + 放行互斥屏障)按**提交顺序**逐条执行,
1748
+ * 并在同步完成上下文挂载后**立刻**将结果返回给程序;`shapeDispatchLog` 与 settle 事件作为
1749
+ * 有序后台 `logWork` 执行,超出 `maxParallel` 时在提交车道施加背压;
1558
1750
  * - `exclusive` 调用等所有在飞 body 结算后独占运行,并独占到自己 commit 完成才放行后续;
1559
1751
  * - 排队期间若外层运行已中止,直接放弃且不启动任务(未启动调用零 `start` 事件);
1560
- * - `drain()` 等到 lane 彻底静止(所有 body 结算、所有 commit 完成)才返回。
1752
+ * - `drain()` 等到 lane 彻底静止且所有后台 `logWork` 落盘完成才返回。
1561
1753
  *
1562
- * 程序拿到的值在 commit 里结算(与内核同):body 结束不保证顺序,提交才是顺序。
1563
- *
1564
- * @param maxParallel - 同时在飞的上限。
1754
+ * @param maxParallel - 同时在飞的上限(同时作为 `logWork` 背压上限)。
1565
1755
  * @param isAborted - 外层运行是否已中止。
1566
1756
  * @returns 调度入口与排空。
1567
1757
  */
@@ -1570,6 +1760,8 @@ function createCommitLane(maxParallel, isAborted) {
1570
1760
  const queue = [];
1571
1761
  const commitQueue = [];
1572
1762
  const inFlight = /* @__PURE__ */ new Set();
1763
+ const logWork = /* @__PURE__ */ new Set();
1764
+ let logTail = Promise.resolve();
1573
1765
  let exclusiveActive = false;
1574
1766
  let driving = false;
1575
1767
  let driverRun = Promise.resolve();
@@ -1579,6 +1771,13 @@ function createCommitLane(maxParallel, isAborted) {
1579
1771
  wake = void 0;
1580
1772
  release?.();
1581
1773
  };
1774
+ const enqueueLog = (work) => {
1775
+ logTail = logTail.then(work, work).catch(() => void 0);
1776
+ const tracked = logTail.finally(() => {
1777
+ logWork.delete(tracked);
1778
+ });
1779
+ logWork.add(tracked);
1780
+ };
1582
1781
  /**
1583
1782
  * 唯一的有序车道:每一轮先提交队首已结算的调用,再在有空位时启动下一个排队调用,
1584
1783
  * 否则睡到「有 body 结算或有新提交」为止。三条队列全空即静止。
@@ -1606,8 +1805,10 @@ function createCommitLane(maxParallel, isAborted) {
1606
1805
  head.abandon();
1607
1806
  continue;
1608
1807
  }
1609
- if (!exclusiveActive && (head.mode === "exclusive" ? inFlight.size === 0 : inFlight.size < cap)) {
1610
- if (head.mode === "exclusive") exclusiveActive = true;
1808
+ const mode = head.classify();
1809
+ if (!exclusiveActive && (mode === "exclusive" ? inFlight.size === 0 : inFlight.size < cap)) {
1810
+ head.mode = mode;
1811
+ if (mode === "exclusive") exclusiveActive = true;
1611
1812
  queue.shift();
1612
1813
  commitQueue.push(head);
1613
1814
  head.start();
@@ -1630,11 +1831,18 @@ function createCommitLane(maxParallel, isAborted) {
1630
1831
  return driverRun;
1631
1832
  };
1632
1833
  return {
1633
- schedule(mode, task) {
1834
+ schedule(task) {
1634
1835
  return new Promise((resolve, reject) => {
1635
1836
  let parked;
1636
1837
  const entry = {
1637
- mode,
1838
+ mode: "exclusive",
1839
+ classify() {
1840
+ try {
1841
+ return task.classify ? task.classify() : "exclusive";
1842
+ } catch {
1843
+ return "exclusive";
1844
+ }
1845
+ },
1638
1846
  settled: false,
1639
1847
  flight: Promise.resolve(),
1640
1848
  start() {
@@ -1659,10 +1867,11 @@ function createCommitLane(maxParallel, isAborted) {
1659
1867
  const result = parked;
1660
1868
  if (result === void 0) return;
1661
1869
  try {
1662
- resolve(await task.commit(result));
1870
+ resolve(await task.commit(result, enqueueLog));
1663
1871
  } catch (error) {
1664
1872
  reject(error);
1665
1873
  }
1874
+ while (logWork.size > cap) await Promise.race(logWork);
1666
1875
  },
1667
1876
  abandon() {
1668
1877
  try {
@@ -1679,6 +1888,7 @@ function createCommitLane(maxParallel, isAborted) {
1679
1888
  },
1680
1889
  async drain() {
1681
1890
  await drive();
1891
+ while (logWork.size > 0) await Promise.allSettled([...logWork]);
1682
1892
  }
1683
1893
  };
1684
1894
  }
@@ -1697,7 +1907,7 @@ async function shapeDispatchContent(host, dispatch) {
1697
1907
  }
1698
1908
  }
1699
1909
  /** 一个抛出值的消息。 */
1700
- function messageOf$1(error) {
1910
+ function messageOf$2(error) {
1701
1911
  return error instanceof Error ? error.message : String(error);
1702
1912
  }
1703
1913
  const JSON_INDENT = " ";
@@ -1790,6 +2000,358 @@ function renderProgramValue(value) {
1790
2000
  return chunks.join("");
1791
2001
  }
1792
2002
  //#endregion
2003
+ //#region src/host/codemode/retention.ts
2004
+ /**
2005
+ * 常驻执行环境的生命周期与会话级调用协调(对齐内核 `ptc.ts` 与 `PtcCalls`)。
2006
+ *
2007
+ * 部署把 `@deepseek-ai/dsh-ptc-runtime-node` 配成 `executionMode: persistent-in-memory` 时,
2008
+ * 同一个 agent 的多次 `code_run` 会共用一个常驻 REPL:顶层变量、函数与 `import` 的模块
2009
+ * 跨调用存活。可以不去用,但**不能只注册不释放**——常驻环境没有空闲过期,忘了释放会让子进程
2010
+ * 活到进程结束。
2011
+ *
2012
+ * 所以这里把「传了 owner」与「注册了释放」写进同一个动作:结构性无法表达「传了却没释放」。
2013
+ * 清理有三条路,覆盖所有生命周期终点与显式重置:
2014
+ *
2015
+ * 1. 挂在 **agent.ctx** 上的释放(对齐 `ptc.ts` 的 `stateOwners`):展开 Cordis 包装链后按原初
2016
+ * runtime provider 去重登记 `agent.ctx.effect`,即使本包早已卸载,agent 销毁也会释放它的环境;
2017
+ * 2. 挂在**插件**上的 teardown:卸载或热重载本包时先中止并排空在飞调用,再释放所有遗留环境;
2018
+ * 3. 会话级 `PtcCalls` 协调:新调用进入前等待同会话已取消的前序调用与进行中的 `reset` 彻底排空,
2019
+ * 避免前一次取消的收尾清理撞上后一次调用的 `assertIdle()`。
2020
+ *
2021
+ * @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/retention
2022
+ */
2023
+ /** Cordis 服务包装链指向原初实现的符号描述。 */
2024
+ const CORDIS_ORIGINAL_DESCRIPTION = "cordis.original";
2025
+ /**
2026
+ * 造一个常驻环境与会话调用协调管理器。
2027
+ *
2028
+ * @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
2029
+ * @returns 管理句柄。
2030
+ */
2031
+ function createRetention(runtimeOf) {
2032
+ const live = /* @__PURE__ */ new Set();
2033
+ const released = /* @__PURE__ */ new WeakSet();
2034
+ const stateOwners = /* @__PURE__ */ new WeakMap();
2035
+ const calls = /* @__PURE__ */ new Map();
2036
+ const resets = /* @__PURE__ */ new Map();
2037
+ let disposed = false;
2038
+ const release = async (owner, options) => {
2039
+ const strict = options?.strict === true;
2040
+ if (!strict && released.has(owner)) return;
2041
+ released.add(owner);
2042
+ live.delete(owner);
2043
+ const runtime = runtimeOf();
2044
+ if (runtime?.release === void 0) return;
2045
+ try {
2046
+ await runtime.release(owner);
2047
+ } catch (error) {
2048
+ if (strict) {
2049
+ released.delete(owner);
2050
+ live.add(owner);
2051
+ throw error;
2052
+ }
2053
+ }
2054
+ };
2055
+ /** 在 `agent.ctx` 上按原初 runtime provider 去重挂一次释放钩子(对齐 `ptc.ts:669-684`)。 */
2056
+ const bindAgentLifecycle = (owner) => {
2057
+ const runtime = runtimeOf();
2058
+ if (runtime === void 0 || typeof runtime !== "object" || runtime === null) return;
2059
+ const ctx = owner.ctx;
2060
+ if (typeof ctx?.effect !== "function") return;
2061
+ let runtimes = stateOwners.get(owner);
2062
+ if (runtimes === void 0) {
2063
+ runtimes = /* @__PURE__ */ new WeakSet();
2064
+ stateOwners.set(owner, runtimes);
2065
+ }
2066
+ const provider = unwrapOriginalProvider(runtime);
2067
+ if (runtimes.has(provider)) return;
2068
+ try {
2069
+ ctx.effect(() => () => release(owner), "PTC retained environment");
2070
+ runtimes.add(provider);
2071
+ } catch {}
2072
+ };
2073
+ return {
2074
+ owner(agent) {
2075
+ const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
2076
+ released.delete(owner);
2077
+ live.add(owner);
2078
+ if (owner !== fallbackOwner) bindAgentLifecycle(owner);
2079
+ return owner;
2080
+ },
2081
+ release,
2082
+ async awaitPredecessors(agent, signal) {
2083
+ const key = sessionKeyOf(agent);
2084
+ if (key !== void 0) for (;;) {
2085
+ const reset = resets.get(key);
2086
+ if (reset !== void 0) {
2087
+ try {
2088
+ await reset.done;
2089
+ } catch (error) {
2090
+ throw new CodeRunFailedError("Program environment unavailable: " + messageOf$1(error) + ". Ask the host to reset it before running more code.");
2091
+ }
2092
+ continue;
2093
+ }
2094
+ const pending = [...calls.get(key) ?? []].filter((call) => call.signal.aborted);
2095
+ if (pending.length === 0) break;
2096
+ await Promise.all(pending.map((call) => call.done));
2097
+ }
2098
+ if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "tool call aborted"));
2099
+ if (disposed) throw new Error("PTC tool executor disposed");
2100
+ },
2101
+ trackRun(run) {
2102
+ const key = sessionKeyOf(run.agent);
2103
+ if (key === void 0) return () => void 0;
2104
+ let bucket = calls.get(key);
2105
+ if (bucket === void 0) {
2106
+ bucket = /* @__PURE__ */ new Set();
2107
+ calls.set(key, bucket);
2108
+ }
2109
+ for (const existing of bucket) if (existing.controller === run.controller) {
2110
+ existing.mode = run.mode;
2111
+ return () => {
2112
+ const current = calls.get(key);
2113
+ if (current !== void 0) {
2114
+ current.delete(existing);
2115
+ if (current.size === 0) calls.delete(key);
2116
+ }
2117
+ existing.resolveDone();
2118
+ };
2119
+ }
2120
+ let resolveDone = () => void 0;
2121
+ const done = new Promise((resolve) => {
2122
+ resolveDone = resolve;
2123
+ });
2124
+ const record = {
2125
+ controller: run.controller,
2126
+ signal: run.signal ?? run.controller.signal,
2127
+ ...run.mode === void 0 ? {} : { mode: run.mode },
2128
+ done,
2129
+ resolveDone
2130
+ };
2131
+ bucket.add(record);
2132
+ return () => {
2133
+ const current = calls.get(key);
2134
+ if (current !== void 0) {
2135
+ current.delete(record);
2136
+ if (current.size === 0) calls.delete(key);
2137
+ }
2138
+ record.resolveDone();
2139
+ };
2140
+ },
2141
+ cancelOnModeChange(session, event) {
2142
+ if (typeof event !== "object" || event === null) return;
2143
+ const record = event;
2144
+ if (record.type !== "sandbox/mode") return;
2145
+ const mode = (typeof record.data === "object" && record.data !== null ? record.data : void 0)?.mode;
2146
+ if (typeof mode !== "string") return;
2147
+ const key = sessionKeyOf(session);
2148
+ if (key === void 0) return;
2149
+ for (const call of calls.get(key) ?? []) if (call.mode !== void 0 && call.mode !== mode) call.controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
2150
+ },
2151
+ reset(agent, notify) {
2152
+ if (disposed) return Promise.reject(/* @__PURE__ */ new Error("PTC tool executor disposed"));
2153
+ const key = sessionKeyOf(agent) ?? fallbackOwner;
2154
+ const previous = resets.get(key);
2155
+ if (previous !== void 0 && !previous.settled) return previous.done;
2156
+ let resolveDone = () => void 0;
2157
+ let rejectDone = () => void 0;
2158
+ const done = new Promise((resolve, reject) => {
2159
+ resolveDone = resolve;
2160
+ rejectDone = reject;
2161
+ });
2162
+ done.catch(() => void 0);
2163
+ const resetRecord = {
2164
+ done,
2165
+ settled: false
2166
+ };
2167
+ resets.set(key, resetRecord);
2168
+ const active = [...calls.get(key) ?? []];
2169
+ for (const call of active) call.controller.abort(/* @__PURE__ */ new Error("PTC environment reset"));
2170
+ const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
2171
+ Promise.allSettled([Promise.resolve().then(() => release(owner, { strict: true })), ...active.map((call) => call.done)]).then((results) => {
2172
+ const failure = results.find((result) => result.status === "rejected");
2173
+ if (failure !== void 0 && failure.status === "rejected") throw failure.reason;
2174
+ notify?.();
2175
+ }).then(() => {
2176
+ resetRecord.settled = true;
2177
+ resets.delete(key);
2178
+ resolveDone();
2179
+ }, (error) => {
2180
+ resetRecord.settled = true;
2181
+ rejectDone(error);
2182
+ });
2183
+ return done;
2184
+ },
2185
+ async dispose() {
2186
+ disposed = true;
2187
+ const activeCalls = [...calls.values()].flatMap((bucket) => [...bucket]);
2188
+ for (const call of activeCalls) call.controller.abort(/* @__PURE__ */ new Error("PTC tool executor disposed"));
2189
+ await Promise.allSettled([...activeCalls.map((call) => call.done), ...[...resets.values()].map((reset) => reset.done)]);
2190
+ const owners = [...live];
2191
+ await Promise.all(owners.map((owner) => release(owner)));
2192
+ }
2193
+ };
2194
+ }
2195
+ /**
2196
+ * 展开 Cordis 服务代理上的 `[symbols.original]` 链,定位原初 provider 对象。
2197
+ */
2198
+ function unwrapOriginalProvider(runtime) {
2199
+ let current = runtime;
2200
+ for (let depth = 0; depth < 8; depth += 1) {
2201
+ let next;
2202
+ for (const sym of Object.getOwnPropertySymbols(current)) if (sym.description === CORDIS_ORIGINAL_DESCRIPTION) {
2203
+ next = current[sym];
2204
+ break;
2205
+ }
2206
+ if (typeof next !== "object" || next === null || next === current) break;
2207
+ current = next;
2208
+ }
2209
+ return current;
2210
+ }
2211
+ /**
2212
+ * 一个会话的身份键(优先用 `session.id`,无 id 时退回会话对象本身)。
2213
+ */
2214
+ function sessionKeyOf(value) {
2215
+ if (typeof value !== "object" || value === null) return void 0;
2216
+ const nested = value.session;
2217
+ const session = typeof nested === "object" && nested !== null ? nested : value;
2218
+ const id = session.id;
2219
+ if (typeof id === "string" && id.length > 0) return id;
2220
+ return session;
2221
+ }
2222
+ /** 一个抛出值的消息。 */
2223
+ function messageOf$1(error) {
2224
+ return error instanceof Error ? error.message : String(error);
2225
+ }
2226
+ /** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
2227
+ const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
2228
+ //#endregion
2229
+ //#region src/host/codemode/scope.ts
2230
+ /**
2231
+ * 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
2232
+ *
2233
+ * ## 为什么要这么绕
2234
+ *
2235
+ * 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
2236
+ * 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
2237
+ * 都不导出。能用的只有两件事:
2238
+ *
2239
+ * 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
2240
+ * 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
2241
+ *
2242
+ * 于是归属判定变成两次公开查询:
2243
+ *
2244
+ * - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
2245
+ * - 只有本预设视图能解析 → 本预设自己注册的;
2246
+ * - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
2247
+ *
2248
+ * 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
2249
+ * 纯名字匹配,绝不因为读不到归属就误遮蔽。
2250
+ *
2251
+ * @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
2252
+ */
2253
+ /** 内核写作用域标签用的符号描述。 */
2254
+ const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
2255
+ /**
2256
+ * 从一个 ctx 上找出「本包所在作用域」的 key。
2257
+ *
2258
+ * Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
2259
+ * 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
2260
+ * 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
2261
+ *
2262
+ * @param ctx - 本包的行上下文。
2263
+ * @returns 作用域 key;读不到时为 `undefined`。
2264
+ */
2265
+ function findScopeKey(ctx) {
2266
+ if (typeof ctx !== "object" || ctx === null) return void 0;
2267
+ return scanForScope(ctx, 0);
2268
+ }
2269
+ /** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
2270
+ function scanForScope(source, depth) {
2271
+ if (depth > 4) return void 0;
2272
+ let symbols;
2273
+ try {
2274
+ symbols = Object.getOwnPropertySymbols(source);
2275
+ } catch {
2276
+ return;
2277
+ }
2278
+ for (const symbol of symbols) {
2279
+ if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
2280
+ let value;
2281
+ try {
2282
+ value = source[symbol];
2283
+ } catch {
2284
+ continue;
2285
+ }
2286
+ if (typeof value === "object" && value !== null) return value;
2287
+ }
2288
+ const proto = Object.getPrototypeOf(source);
2289
+ if (proto === null || proto === Object.prototype) return void 0;
2290
+ return scanForScope(proto, depth + 1);
2291
+ }
2292
+ /**
2293
+ * 为一个名字清单建立归属视图。
2294
+ *
2295
+ * @param tools - 工具注册表(结构探测,不看类身份)。
2296
+ * @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
2297
+ * @param names - 要判定的名字(通常是本步的模型可见清单)。
2298
+ * @returns 归属视图;读不到任何归属时返回禁用视图。
2299
+ */
2300
+ function resolveCatalogView(tools, scopeKey, names) {
2301
+ const registry = asRegistry(tools);
2302
+ if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
2303
+ const globalNames = /* @__PURE__ */ new Set();
2304
+ const presetNames = /* @__PURE__ */ new Set();
2305
+ for (const name of names) {
2306
+ if (typeof name !== "string" || name.length === 0) continue;
2307
+ if (probes(registry, name, void 0)) globalNames.add(name);
2308
+ if (probes(registry, name, scopeKey)) presetNames.add(name);
2309
+ }
2310
+ return {
2311
+ enabled: true,
2312
+ globalNames,
2313
+ presetNames
2314
+ };
2315
+ }
2316
+ /**
2317
+ * 判定一个名字在**调用时**是否属于本预设之外的层。
2318
+ *
2319
+ * 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
2320
+ * 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
2321
+ *
2322
+ * @param tools - 工具注册表。
2323
+ * @param scopeKey - 本预设的作用域 key。
2324
+ * @param name - 工具名。
2325
+ * @returns 归属视图(只含这一个名字)。
2326
+ */
2327
+ function resolveNameView(tools, scopeKey, name) {
2328
+ const registry = asRegistry(tools);
2329
+ if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
2330
+ const globalNames = /* @__PURE__ */ new Set();
2331
+ const presetNames = /* @__PURE__ */ new Set();
2332
+ if (probes(registry, name, void 0)) globalNames.add(name);
2333
+ if (probes(registry, name, scopeKey)) presetNames.add(name);
2334
+ return {
2335
+ enabled: true,
2336
+ globalNames,
2337
+ presetNames
2338
+ };
2339
+ }
2340
+ /** 一次公开查询:解析得到定义时为 `true`。 */
2341
+ function probes(registry, name, scope) {
2342
+ try {
2343
+ return registry.get(name, scope) !== void 0;
2344
+ } catch {
2345
+ return false;
2346
+ }
2347
+ }
2348
+ /** 结构探测:读得到 `get` 就当注册表用。 */
2349
+ function asRegistry(tools) {
2350
+ if (typeof tools !== "object" || tools === null) return void 0;
2351
+ if (typeof tools.get !== "function") return void 0;
2352
+ return tools;
2353
+ }
2354
+ //#endregion
1793
2355
  //#region src/entry-codemode.ts
1794
2356
  /**
1795
2357
  * Code Mode 预设行 —— 本包自己的「原生工具 + 程序传输」呈现方式。
@@ -1881,52 +2443,8 @@ function apply(ctx, config = {}) {
1881
2443
  if (typeof context.effect === "function") context.effect(() => () => {
1882
2444
  retention.dispose();
1883
2445
  }, "codemode retained environments");
1884
- /** 在飞运行:会话键 → 该会话里正在跑的程序。 */
1885
- const activeRuns = /* @__PURE__ */ new Map();
1886
- /**
1887
- * 登记一只在飞运行,返回运行结算时的 disposer。
1888
- *
1889
- * 传输层每轮运行(含常驻环境换策略后的重跑)都登记一次,入口因此知道「这一轮跑在哪个
1890
- * 会话、当时的沙箱模式是什么」。会话切模式时,模式不匹配的运行必须停:常驻环境换策略
1891
- * 会抛错,而已经跑起来的那一轮不能继续用旧权限往下写。
1892
- */
1893
- const trackRun = (run) => {
1894
- const key = sessionKeyOf(run.agent);
1895
- if (key === void 0) return () => void 0;
1896
- const record = {
1897
- controller: run.controller,
1898
- ...run.mode === void 0 ? {} : { mode: run.mode }
1899
- };
1900
- let bucket = activeRuns.get(key);
1901
- if (bucket === void 0) {
1902
- bucket = /* @__PURE__ */ new Set();
1903
- activeRuns.set(key, bucket);
1904
- }
1905
- bucket.add(record);
1906
- return () => {
1907
- const current = activeRuns.get(key);
1908
- if (current === void 0) return;
1909
- current.delete(record);
1910
- if (current.size === 0) activeRuns.delete(key);
1911
- };
1912
- };
1913
- /**
1914
- * 会话的 `sandbox/mode` 事件:生效模式已经变了的在飞运行直接取消(内核 `PtcCalls` 同此)。
1915
- *
1916
- * 只取消「能确认模式变了」的那些:拿不到模式(运行时不管沙箱)或模式相同的运行一律不动。
1917
- */
1918
- const cancelOnModeChange = (session, event) => {
1919
- if (typeof event !== "object" || event === null) return;
1920
- const record = event;
1921
- if (record.type !== "sandbox/mode") return;
1922
- const mode = (typeof record.data === "object" && record.data !== null ? record.data : void 0)?.mode;
1923
- if (typeof mode !== "string") return;
1924
- const key = sessionKeyOf(session);
1925
- if (key === void 0) return;
1926
- for (const run of activeRuns.get(key) ?? []) if (run.mode !== void 0 && run.mode !== mode) run.controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
1927
- };
1928
2446
  if (typeof context.on === "function") try {
1929
- const off = context.on("session/event", cancelOnModeChange, { global: true });
2447
+ const off = context.on("session/event", retention.cancelOnModeChange, { global: true });
1930
2448
  if (typeof context.effect === "function") context.effect(() => () => {
1931
2449
  if (typeof off === "function") off();
1932
2450
  }, "codemode sandbox mode cancellation");
@@ -1953,10 +2471,27 @@ function apply(ctx, config = {}) {
1953
2471
  };
1954
2472
  const scopeKey = findScopeKey(ctx);
1955
2473
  if (rules.options.nativeScope && scopeKey === void 0) log("warn", "[codemode] no registry scope tag on this context: masking falls back to name rules only (" + rules.options.hiddenPrefixes.join(", ") + "). Mount this row inside an agent preset to hide every tool the preset does not own.");
1956
- /** 最近一次 describe 收到的完整定义集(含被遮蔽的)。 */
2474
+ /** 最近一次 describe 收到的完整定义集(含被遮蔽的),作为无 `tools.definitions` 时的回退快照。 */
1957
2475
  let catalog = [];
1958
2476
  const debugRing = [];
1959
2477
  let firstDescribeLogged = false;
2478
+ /**
2479
+ * 解析当前调用方 agent 与步骤准入下的工具清单(对齐内核 `ptc.ts:662`)。
2480
+ *
2481
+ * 优先通过 `ctx.tools.definitions(agent, toolSet)` 实时读取调用方自己的可见集合(含
2482
+ * `agent.ctx` 专属工具与 `tools.restrict` 过滤),避免多会话并发时被另一个会话的
2483
+ * `describe()` 覆盖;测试替身未提供 `definitions` 时回退到最近一次 `catalog`。
2484
+ */
2485
+ const resolveCatalog = (agent, toolSet) => {
2486
+ if (agent !== void 0 && typeof tools.definitions === "function") try {
2487
+ const defs = tools.definitions(agent, toolSet);
2488
+ if (Array.isArray(defs) && defs.length > 0) {
2489
+ const entries = defs.map(toCatalogEntry).filter((entry) => entry !== void 0);
2490
+ if (entries.length > 0) return entries;
2491
+ }
2492
+ } catch {}
2493
+ return catalog;
2494
+ };
1960
2495
  const escalateSandbox = async (input) => {
1961
2496
  const record = typeof input.standingPolicy === "object" && input.standingPolicy !== null ? input.standingPolicy : void 0;
1962
2497
  const effectiveMode = typeof record?.["mode"] === "string" ? record["mode"] : void 0;
@@ -1989,10 +2524,11 @@ function apply(ctx, config = {}) {
1989
2524
  }
1990
2525
  };
1991
2526
  const host = {
1992
- catalog: () => catalog,
2527
+ catalog: (agent, toolSet) => resolveCatalog(agent, toolSet),
1993
2528
  runtime: runtimeOf,
1994
2529
  stateOwner: (agent) => retention.owner(agent),
1995
2530
  releaseOwner: (owner) => retention.release(owner),
2531
+ awaitPredecessors: (agent, signal) => retention.awaitPredecessors(agent, signal),
1996
2532
  runAuthority,
1997
2533
  escalateSandbox,
1998
2534
  createImageContext(content) {
@@ -2001,7 +2537,7 @@ function apply(ctx, config = {}) {
2001
2537
  source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
2002
2538
  });
2003
2539
  },
2004
- trackRun,
2540
+ trackRun: (run) => retention.trackRun(run),
2005
2541
  shapeDispatchLog,
2006
2542
  async callTool(request) {
2007
2543
  const call = {
@@ -2065,6 +2601,25 @@ function apply(ctx, config = {}) {
2065
2601
  onCatalog: (definitions) => {
2066
2602
  catalog = definitions.map(toCatalogEntry).filter((entry) => entry !== void 0);
2067
2603
  },
2604
+ async reset(agent) {
2605
+ const runtime = runtimeOf();
2606
+ if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
2607
+ if (!runtime.retainsState) throw new CodeResetFailedError("The PTC runtime does not retain an environment");
2608
+ try {
2609
+ await retention.reset(agent, () => {
2610
+ const inject = typeof agent === "object" && agent !== null ? agent.inject : void 0;
2611
+ if (typeof inject === "function") inject.call(agent, createUserMessage({
2612
+ content: [{
2613
+ type: "text",
2614
+ text: "Program environment reset; retained variables cleared. Completed tool effects remain."
2615
+ }],
2616
+ source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
2617
+ }));
2618
+ });
2619
+ } catch (error) {
2620
+ throw new CodeResetFailedError("Program environment reset failed: " + messageOf(error) + ". State may not be cleared; retry the host reset before running more code.");
2621
+ }
2622
+ },
2068
2623
  onDescribe: (decision) => {
2069
2624
  const hidden = summarizeHidden(decision.masked);
2070
2625
  debugRing.push({
@@ -2145,24 +2700,6 @@ function errorInfoOf(result) {
2145
2700
  function nonEmptyString(value) {
2146
2701
  return typeof value === "string" && value.length > 0 ? value : void 0;
2147
2702
  }
2148
- /**
2149
- * 一个会话的身份键。
2150
- *
2151
- * 优先用会话 id(与内核 `session/event` 监听器里的 `session.id` 对齐);取不到 id 时退回
2152
- * 会话对象本身——调用方 agent 上的 `session` 与事件里的会话是同一个对象,靠身份比对同样
2153
- * 成立。连会话都没有时返回 `undefined`,这类运行不参与取消。
2154
- *
2155
- * @param value - 一个 agent,或一个会话对象。
2156
- * @returns 会话 id / 会话对象,或 `undefined`。
2157
- */
2158
- function sessionKeyOf(value) {
2159
- if (typeof value !== "object" || value === null) return void 0;
2160
- const nested = value.session;
2161
- const session = typeof nested === "object" && nested !== null ? nested : value;
2162
- const id = session.id;
2163
- if (typeof id === "string" && id.length > 0) return id;
2164
- return session;
2165
- }
2166
2703
  /** 把内容块里的文本拼起来。 */
2167
2704
  function textOf(content) {
2168
2705
  if (!Array.isArray(content)) return "";