@zergai/ztc 0.3.0-beta.1 → 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/zmux-darwin-arm64 +0 -0
  2. package/bin/ztc-tui-darwin-arm64 +0 -0
  3. package/bin/zterm-darwin-arm64 +0 -0
  4. package/dist/App.js +16 -2
  5. package/dist/agent/agent.d.ts +20 -8
  6. package/dist/agent/agent.js +92 -40
  7. package/dist/agent/backends/gemini.js +1 -1
  8. package/dist/agent/backends/openai_format.js +2 -2
  9. package/dist/agent/backends/openai_responses.js +1 -1
  10. package/dist/agent/backends/types.d.ts +1 -0
  11. package/dist/agent/commands/config.js +15 -1
  12. package/dist/agent/commands/settings.js +23 -0
  13. package/dist/agent/commands/status.js +3 -0
  14. package/dist/agent/commands/types.d.ts +2 -0
  15. package/dist/agent/completion_state.js +2 -2
  16. package/dist/agent/factory.js +7 -1
  17. package/dist/agent/request_encoder.js +3 -0
  18. package/dist/agent/runtime/command_jobs.d.ts +47 -0
  19. package/dist/agent/runtime/command_jobs.js +268 -0
  20. package/dist/agent/runtime/execution_context.d.ts +16 -0
  21. package/dist/agent/runtime/execution_context.js +20 -0
  22. package/dist/agent/runtime/recovery.d.ts +9 -0
  23. package/dist/agent/runtime/recovery.js +71 -0
  24. package/dist/agent/runtime/tool_runtime.js +3 -1
  25. package/dist/agent/runtime/tool_scheduler.d.ts +6 -0
  26. package/dist/agent/runtime/tool_scheduler.js +57 -0
  27. package/dist/agent/subagent.js +30 -12
  28. package/dist/agent/tool_response_store.d.ts +2 -0
  29. package/dist/agent/tool_response_store.js +1 -0
  30. package/dist/agent/tools/deferred.js +2 -0
  31. package/dist/agent/tools/file.js +2 -0
  32. package/dist/agent/tools/pane.js +139 -42
  33. package/dist/agent/tools/recall_tool_response.js +1 -0
  34. package/dist/agent/tools/registry.js +3 -1
  35. package/dist/agent/tools/search.js +1 -0
  36. package/dist/agent/tools/shell.d.ts +2 -0
  37. package/dist/agent/tools/shell.js +57 -2
  38. package/dist/agent/tools/types.d.ts +4 -0
  39. package/dist/app/agent_runner.js +1 -1
  40. package/dist/app/ordered_transcript.d.ts +13 -0
  41. package/dist/app/ordered_transcript.js +55 -0
  42. package/dist/cli_ratatui.d.ts +2 -7
  43. package/dist/cli_ratatui.js +53 -34
  44. package/dist/config/execution.d.ts +12 -0
  45. package/dist/config/execution.js +13 -0
  46. package/dist/config/types.d.ts +1 -0
  47. package/dist/config.d.ts +1 -0
  48. package/dist/config.js +5 -0
  49. package/dist/emulation/tool_adapters.js +5 -2
  50. package/dist/headless.d.ts +1 -18
  51. package/dist/headless.js +15 -0
  52. package/dist/plugin/runtime.d.ts +1 -1
  53. package/dist/types.d.ts +3 -0
  54. package/dist/ui/ratatui/headless_pane_manager.d.ts +2 -19
  55. package/dist/ui/ratatui/headless_pane_manager.js +41 -34
  56. package/dist/ui/ratatui/pane_input.d.ts +4 -0
  57. package/dist/ui/ratatui/pane_input.js +15 -0
  58. package/dist/ui/ratatui/pane_relay.d.ts +3 -0
  59. package/dist/ui/ratatui/pane_relay.js +20 -2
  60. package/dist/ui/ratatui/protocol.d.ts +17 -0
  61. package/dist/ui/ratatui/protocol.js +1 -0
  62. package/dist/utils/shell.d.ts +2 -0
  63. package/dist/utils/shell.js +54 -11
  64. package/dist/web/mirror_stream.d.ts +2 -1
  65. package/dist/web/mirror_stream.js +7 -0
  66. package/dist/web/pane_registry.js +8 -2
  67. package/dist/web/view_plugin.d.ts +1 -0
  68. package/dist/zlm-embed.mjs +384 -118
  69. package/package.json +2 -2
@@ -555,9 +555,9 @@ function incompleteModelResponseMessage(stopReason, maxTokens, outputTokens) {
555
555
  const exhausted = reason === "incomplete" && maxTokens !== void 0 && outputTokens !== void 0 && outputTokens >= maxTokens;
556
556
  if (limited || exhausted) {
557
557
  if (maxTokens === void 0) {
558
- return "Model reached the provider's output limit (including reasoning). Set maxTokens within the model's limit or lower reasoning effort, then retry; the incomplete response was not accepted.";
558
+ return "Model reached the provider's output limit (including reasoning). Use /config max-tokens <number> within the model's limit or lower reasoning effort, then retry; completed tool work is retained and the incomplete response was not accepted.";
559
559
  }
560
- return `Model reached the output limit (maxTokens: ${maxTokens}, including reasoning). Increase maxTokens and retry; the incomplete response was not accepted.`;
560
+ return `Model reached the output limit (maxTokens: ${maxTokens}, including reasoning). Set /config max-tokens <number> or /config max-tokens default, then retry. Completed tool work is retained; the incomplete response was not accepted.`;
561
561
  }
562
562
  return INCOMPLETE_MODEL_RESPONSE_MESSAGE;
563
563
  }
@@ -825,7 +825,7 @@ function buildOpenAIMessages(request) {
825
825
  } else if (block.type === "tool_use") {
826
826
  toolUses.push({ id: block.id, name: block.name, input: block.input });
827
827
  } else if (block.type === "tool_result") {
828
- toolResults.push({ tool_use_id: block.tool_use_id, content: block.content });
828
+ toolResults.push({ tool_use_id: block.tool_use_id, content: block.content, is_error: block.is_error });
829
829
  } else if (block.type === "image") {
830
830
  mediaParts.push({
831
831
  type: "image_url",
@@ -863,7 +863,7 @@ function buildOpenAIMessages(request) {
863
863
  out.push({
864
864
  role: "tool",
865
865
  tool_call_id: tr.tool_use_id,
866
- content: resultText
866
+ content: tr.is_error && !/^error\b/i.test(resultText) ? `Error: ${resultText}` : resultText
867
867
  });
868
868
  }
869
869
  if (textParts.length > 0 || mediaParts.length > 0) {
@@ -1385,7 +1385,7 @@ function buildResponsesInput(messages) {
1385
1385
  input.push({
1386
1386
  type: "function_call_output",
1387
1387
  call_id: block.tool_use_id,
1388
- output: resultText
1388
+ output: block.is_error && !/^error\b/i.test(resultText) ? `Error: ${resultText}` : resultText
1389
1389
  });
1390
1390
  break;
1391
1391
  }
@@ -1808,7 +1808,7 @@ var init_gemini = __esm({
1808
1808
  }
1809
1809
  if (block.type === "tool_result") {
1810
1810
  const resultText = typeof block.content === "string" ? block.content : block.content.map((b) => b.type === "text" ? b.text : "[image]").join("\n");
1811
- return { text: `Function result: ${resultText}` };
1811
+ return { text: `Function ${block.tool_use_id} ${block.is_error ? "error" : "result"}: ${resultText}` };
1812
1812
  }
1813
1813
  if (block.type === "tool_use") {
1814
1814
  return { functionCall: { name: block.name, args: block.input } };
@@ -5661,6 +5661,8 @@ var init_deferred = __esm({
5661
5661
  "list_directory",
5662
5662
  "search",
5663
5663
  "run_command",
5664
+ "read_command",
5665
+ "stop_command",
5664
5666
  "apply_patch",
5665
5667
  // Emulation compatibility aliases. They only appear in the request when
5666
5668
  // the active emulation adds matching tools to the runtime tool list.
@@ -5917,6 +5919,7 @@ var init_tool_response_store = __esm({
5917
5919
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5918
5920
  turn: this.turnCounter++,
5919
5921
  partial,
5922
+ ...context?.isError === void 0 ? {} : { is_error: context.isError },
5920
5923
  preview
5921
5924
  };
5922
5925
  const sourcePath = context?.sourcePath ?? null;
@@ -7235,6 +7238,10 @@ var init_config = __esm({
7235
7238
  ...this.cliOverrides.model !== void 0 ? { model: this.cliOverrides.model } : {}
7236
7239
  };
7237
7240
  }
7241
+ getMaxTokensSource() {
7242
+ if (Object.prototype.hasOwnProperty.call(this.sessionOverrides, "maxTokens")) return "session override";
7243
+ return this.config.maxTokens === void 0 ? "provider default" : `saved profile (${this.getConfigPath()})`;
7244
+ }
7238
7245
  getConfigScope() {
7239
7246
  return this.configScope;
7240
7247
  }
@@ -9308,8 +9315,8 @@ init_backends();
9308
9315
  init_token_usage();
9309
9316
  init_transport_errors();
9310
9317
  init_defaults();
9311
- import { randomUUID as randomUUID5 } from "node:crypto";
9312
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
9318
+ import { randomUUID as randomUUID6 } from "node:crypto";
9319
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
9313
9320
  import { extname } from "path";
9314
9321
 
9315
9322
  // src/config/api_retry.ts
@@ -9348,9 +9355,244 @@ function resolveApiRetrySettings(config) {
9348
9355
  };
9349
9356
  }
9350
9357
 
9358
+ // src/config/execution.ts
9359
+ function resolveExecutionSettings(settings = {}) {
9360
+ const count = (value, fallback, max, min = 1) => Number.isSafeInteger(value) && value >= min && value <= max ? value : fallback;
9361
+ return {
9362
+ shellPanePolicy: settings.shellPanePolicy === "auto" ? "auto" : "on_request",
9363
+ readConcurrency: count(settings.readConcurrency, 4, 32),
9364
+ maxCallsPerResponse: count(settings.maxCallsPerResponse, 16, 256),
9365
+ recovery: settings.recovery === "pause" ? "pause" : "automatic",
9366
+ repeatedFailureLimit: count(settings.repeatedFailureLimit, 3, 20),
9367
+ recoveryAttempts: count(settings.recoveryAttempts, 1, 5, 0),
9368
+ interruptBackgroundJobs: settings.interruptBackgroundJobs === true,
9369
+ maxBackgroundJobs: count(settings.maxBackgroundJobs, 4, 32)
9370
+ };
9371
+ }
9372
+
9373
+ // src/agent/runtime/recovery.ts
9374
+ function normalizeFailure(content) {
9375
+ const text = content.toLowerCase();
9376
+ if (/not_executed/.test(text)) return "";
9377
+ for (const [pattern, cause] of [
9378
+ [/heredoc|unexpected end of file|unterminated/, "unfinished shell input"],
9379
+ [/permission denied|eacces/, "permission denied"],
9380
+ [/not found|enoent|unknown tool|unavailable/, "capability or file unavailable"],
9381
+ [/timeout|timed out/, "timeout"],
9382
+ [/syntaxerror|syntax error|invalid.*syntax/, "syntax error"]
9383
+ ]) if (pattern.test(text)) return cause;
9384
+ const exit = text.match(/command failed \(exit (\d+)\)/);
9385
+ if (exit) return `command exit ${exit[1]}`;
9386
+ return text.split("\n")[0].replace(/['"`][^'"`]*['"`]/g, "<value>").replace(/\d+/g, "#").trim().slice(0, 240);
9387
+ }
9388
+ function createRecoveryGuard(settings) {
9389
+ const limits = resolveExecutionSettings(settings);
9390
+ let cause = "";
9391
+ let failures = 0;
9392
+ let attempts = 0;
9393
+ let neutralSteps = 0;
9394
+ const observations = /* @__PURE__ */ new Set();
9395
+ return (results) => {
9396
+ let failed = false;
9397
+ let progress = false;
9398
+ let neutral = false;
9399
+ for (const result of results) {
9400
+ if (result.isError) {
9401
+ const next = normalizeFailure(result.content);
9402
+ if (!next) continue;
9403
+ failed = true;
9404
+ failures = next === cause ? failures + 1 : 1;
9405
+ cause = next;
9406
+ } else {
9407
+ const signature = `${result.name}\0${result.content}`;
9408
+ const acknowledgement = /^(Input (?:delivered|queued|sent)|Sent input|\(Terminal buffer is empty\))/.test(result.content) || /"status":"running"/.test(result.content) && /"output":""/.test(result.content);
9409
+ neutral ||= acknowledgement;
9410
+ if (result.content.trim() && !acknowledgement && !observations.has(signature)) progress = true;
9411
+ observations.add(signature);
9412
+ }
9413
+ }
9414
+ if (progress && !failed) {
9415
+ failures = 0;
9416
+ cause = "";
9417
+ attempts = 0;
9418
+ neutralSteps = 0;
9419
+ }
9420
+ if (failures < limits.repeatedFailureLimit) return "continue";
9421
+ if (!failed && neutral && attempts > 0 && neutralSteps++ < 1) return "continue";
9422
+ if (limits.recovery === "pause" || attempts >= limits.recoveryAttempts) return "stop";
9423
+ attempts++;
9424
+ neutralSteps = 0;
9425
+ return "recover";
9426
+ };
9427
+ }
9428
+
9429
+ // src/agent/runtime/command_jobs.ts
9430
+ import { randomUUID as randomUUID5, createHash as createHash3 } from "node:crypto";
9431
+ import { appendFileSync as appendFileSync4, closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, readSync, readdirSync, rmSync, rmdirSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
9432
+ import { homedir as homedir6 } from "node:os";
9433
+ import { join as join11 } from "node:path";
9434
+
9435
+ // src/utils/subprocess_env.ts
9436
+ var USER_SUBPROCESS_STRIPPED_ZTC_ENV = [
9437
+ "ZERG_ROUTER_BASE_URL",
9438
+ "ZTC_AUTH_TOKEN",
9439
+ "ZTC_CHILD_ENTRYPOINT",
9440
+ "ZTC_COLONY_CHILD",
9441
+ "ZTC_COLONY_PARENT_MIRROR_PORT",
9442
+ "ZTC_COLONY_WORKTREE_ID",
9443
+ "ZTC_MIRROR_PORT",
9444
+ "ZTC_MIRROR_INTERNAL_TOKEN",
9445
+ "ZTC_MIRROR_REQUIRE_AUTH",
9446
+ "ZTC_MIRROR_SHARED_SECRET",
9447
+ "ZTC_NAVIGATOR_GROUP",
9448
+ "ZTC_PANE_ID",
9449
+ "ZTC_PANE_RELAY",
9450
+ "ZTC_PARENT_PID"
9451
+ ];
9452
+ function buildUserSubprocessEnv(source = process.env) {
9453
+ const env = { ...source };
9454
+ for (const key of USER_SUBPROCESS_STRIPPED_ZTC_ENV) {
9455
+ delete env[key];
9456
+ }
9457
+ return env;
9458
+ }
9459
+
9460
+ // src/utils/shell.ts
9461
+ var DOCKER_CONTAINER = process.env.ZTC_DOCKER_CONTAINER;
9462
+ var DOCKER_WORKDIR = process.env.ZTC_DOCKER_WORKDIR || "/app";
9463
+
9464
+ // src/agent/runtime/command_jobs.ts
9465
+ var MAX_LOG_BYTES = 16 * 1024 * 1024;
9466
+ var RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
9467
+ var sessions = /* @__PURE__ */ new Map();
9468
+ function directory(sessionId) {
9469
+ return join11(
9470
+ process.env.ZTC_CONFIG_DIR || join11(homedir6(), ".ztc"),
9471
+ "command-jobs",
9472
+ createHash3("sha256").update(sessionId).digest("hex").slice(0, 32)
9473
+ );
9474
+ }
9475
+ function save(sessionId, receipt) {
9476
+ writeFileSync5(join11(directory(sessionId), `${receipt.job_id}.json`), JSON.stringify(receipt), { mode: 384 });
9477
+ }
9478
+ function resolveJob(sessionId, jobId) {
9479
+ if (!/^[0-9a-f-]{36}$/.test(jobId)) throw new Error("Command job not found in this session");
9480
+ const live = sessions.get(sessionId)?.get(jobId);
9481
+ if (live) return live;
9482
+ let receipt;
9483
+ try {
9484
+ receipt = JSON.parse(readFileSync6(join11(directory(sessionId), `${jobId}.json`), "utf8"));
9485
+ } catch {
9486
+ throw new Error("Command job not found in this session");
9487
+ }
9488
+ if (receipt.status === "running") {
9489
+ receipt = { ...receipt, status: "interrupted", ended_at: (/* @__PURE__ */ new Date()).toISOString(), error: "Owning ZTC process ended; inspect output before retrying." };
9490
+ save(sessionId, receipt);
9491
+ }
9492
+ return { receipt, controller: new AbortController(), done: Promise.resolve(), loggedBytes: statSync(receipt.log_path).size };
9493
+ }
9494
+ function listCommandJobs(sessionId) {
9495
+ const jobs = [...sessions.get(sessionId)?.values() ?? []];
9496
+ return [...jobs.filter((job) => job.receipt.status === "running"), ...jobs.filter((job) => job.receipt.status !== "running").slice(-28)].map((job) => ({ ...job.receipt }));
9497
+ }
9498
+ async function stopCommandJob(sessionId, jobId) {
9499
+ const job = resolveJob(sessionId, jobId);
9500
+ if (job.receipt.status === "running") {
9501
+ job.controller.abort();
9502
+ await job.done;
9503
+ }
9504
+ return { ...job.receipt };
9505
+ }
9506
+ async function interruptCommandJobs(sessionId) {
9507
+ await Promise.all(listCommandJobs(sessionId).filter((job) => job.status === "running").map((job) => stopCommandJob(sessionId, job.job_id)));
9508
+ }
9509
+
9510
+ // src/agent/runtime/execution_context.ts
9511
+ function describeExecutionContext(context) {
9512
+ const names = new Set(context.tools);
9513
+ const shell = ["run_command", "Bash", "exec_command", "shell_command"].filter((name) => names.has(name));
9514
+ const panes = ["create_terminal_pane", "read_terminal_pane", "write_terminal_pane"].filter((name) => names.has(name));
9515
+ return [
9516
+ "## Current execution capabilities (host facts; refreshed each request)",
9517
+ `Mode: ${context.mode}. Cwd: ${context.cwd}. Config write scope: ${context.configScope}.`,
9518
+ `Output cap: ${context.maxTokens ?? "provider default"} (source: ${context.outputLimitSource}). Use /config max-tokens <number|default> to change it.`,
9519
+ `Shell: ${shell.length ? shell.join(", ") : "unavailable in the current tool surface"}.`,
9520
+ `Mux: ${context.mux ? "connected" : "unavailable"}. Pane tools: ${panes.join(", ") || "unavailable in this mode"}.`,
9521
+ `Shell-pane policy: ${context.settings.shellPanePolicy === "on_request" ? "create a shell pane only when the user explicitly requests a visible or interactive shell; otherwise use run_command" : "may create shell panes automatically when useful"}.`,
9522
+ `Registered tools: ${context.tools.slice(0, 80).join(", ")}. Deferred schemas can be discovered with tool_search when available.`,
9523
+ `Batch limit: ${context.settings.maxCallsPerResponse}. Read concurrency: ${context.settings.readConcurrency}. Inspect a failed or running command before submitting dependent work.`,
9524
+ "For multiline scripts use run_command stdin or write_file and execute the file. Pane text supports multiline submission; raw Ctrl+C (U+0003, submit=false) cancels stuck shell input. A write acknowledgement does not prove command success.",
9525
+ `Managed commands: ${context.jobs.length ? JSON.stringify(context.jobs.map(({ job_id, status, cwd, exit_code }) => ({ job_id, status, cwd, exit_code }))) : "none"}. Background commands ${context.settings.interruptBackgroundJobs ? "are cancelled" : "survive"} on turn interruption; application shutdown stops owned local jobs. Remote cleanup belongs to the launched command.`,
9526
+ "If a tool is unavailable, inspect these current facts and mode restrictions. Do not infer that the session never had shell access from a previous mode or a failed invocation."
9527
+ ].join("\n");
9528
+ }
9529
+
9530
+ // src/agent/runtime/tool_effects.ts
9531
+ init_capabilities();
9532
+ function resolveToolEffect(tool) {
9533
+ if (tool.effect) return tool.effect;
9534
+ const capabilities = new Set(tool.capabilities ?? []);
9535
+ if (capabilities.has("shell_exec" /* SHELL_EXEC */) || capabilities.has("network" /* NETWORK */) || capabilities.has("computer_control" /* COMPUTER_CONTROL */) || capabilities.has("ml_inference" /* ML_INFERENCE */)) {
9536
+ return "external";
9537
+ }
9538
+ if (capabilities.has("file_write" /* FILE_WRITE */)) return "write";
9539
+ return "read";
9540
+ }
9541
+
9542
+ // src/agent/runtime/tool_scheduler.ts
9543
+ async function scheduleToolBatch(calls, tools, settings, execute, blocksFollowing, signal, exclusiveId) {
9544
+ const limits = resolveExecutionSettings(settings);
9545
+ const registered = new Map(tools.map((tool) => [tool.definition.name, tool]));
9546
+ const parallel = (call) => {
9547
+ const tool = registered.get(call.name);
9548
+ return tool?.parallelSafe === true && resolveToolEffect(tool) === "read";
9549
+ };
9550
+ const results = [];
9551
+ let effectsBlocked = false;
9552
+ let stopped = calls.length > limits.maxCallsPerResponse ? `batch_limit: ${calls.length} calls exceeds ${limits.maxCallsPerResponse}; submit a smaller batch after inspecting results.` : "";
9553
+ for (let index = 0; index < calls.length; ) {
9554
+ const call = calls[index];
9555
+ if (signal?.aborted) stopped = "cancelled: the turn was interrupted.";
9556
+ if (stopped) {
9557
+ results.push(await execute(call, `not_executed: ${stopped}`));
9558
+ index++;
9559
+ continue;
9560
+ }
9561
+ if (effectsBlocked && !parallel(call)) {
9562
+ results.push(await execute(call, "not_executed: a previous call failed or is still running; inspect its result before continuing effects."));
9563
+ index++;
9564
+ continue;
9565
+ }
9566
+ if (exclusiveId && call.id !== exclusiveId) {
9567
+ results.push(await execute(call, "not_executed: exclusive safety transition must run alone."));
9568
+ index++;
9569
+ continue;
9570
+ }
9571
+ let end = index + 1;
9572
+ if (!exclusiveId && parallel(call)) {
9573
+ while (end < calls.length && end - index < limits.readConcurrency && parallel(calls[end])) end++;
9574
+ }
9575
+ const batch = await Promise.all(calls.slice(index, end).map((item) => execute(item)));
9576
+ results.push(...batch);
9577
+ if (batch.some(blocksFollowing)) {
9578
+ effectsBlocked = true;
9579
+ }
9580
+ index = end;
9581
+ }
9582
+ return results;
9583
+ }
9584
+ function commandStillRunning(content) {
9585
+ try {
9586
+ const result = JSON.parse(content);
9587
+ return result.status === "running" && typeof result.job_id === "string";
9588
+ } catch {
9589
+ return false;
9590
+ }
9591
+ }
9592
+
9351
9593
  // src/agent/runtime/policy.ts
9352
9594
  init_capabilities();
9353
- import { homedir as homedir6 } from "os";
9595
+ import { homedir as homedir7 } from "os";
9354
9596
  import { resolve as resolve4, isAbsolute as isAbsolute2, sep } from "path";
9355
9597
  var AllowAllPolicy = class {
9356
9598
  evaluateTool() {
@@ -9497,7 +9739,7 @@ function extractPathArgs(args) {
9497
9739
  return paths;
9498
9740
  }
9499
9741
  function normalizeRoot(path, cwd) {
9500
- const expanded = path === "~" ? homedir6() : path.replace(/^~(?=\/|$)/, homedir6());
9742
+ const expanded = path === "~" ? homedir7() : path.replace(/^~(?=\/|$)/, homedir7());
9501
9743
  return resolve4(isAbsolute2(expanded) ? expanded : resolve4(cwd, expanded));
9502
9744
  }
9503
9745
  function isWithinRoot(path, root) {
@@ -9617,8 +9859,8 @@ init_tool_response_store();
9617
9859
 
9618
9860
  // src/utils/mirror_media.ts
9619
9861
  import { tmpdir as tmpdir2 } from "os";
9620
- import { join as join11, resolve as resolve5, sep as sep2 } from "path";
9621
- var MIRROR_MEDIA_ROOT = join11(tmpdir2(), "ztc-mirror-media");
9862
+ import { join as join12, resolve as resolve5, sep as sep2 } from "path";
9863
+ var MIRROR_MEDIA_ROOT = join12(tmpdir2(), "ztc-mirror-media");
9622
9864
  function isMirrorMediaPath(localPath) {
9623
9865
  if (!localPath) return false;
9624
9866
  const root = resolve5(MIRROR_MEDIA_ROOT);
@@ -9711,6 +9953,7 @@ var RequestEncoder = class {
9711
9953
  const verdict = this.deps.selectorVerdict;
9712
9954
  const rerendered = toolResults.map((r) => ({
9713
9955
  tool_use_id: r.tool_use_id,
9956
+ ...r.is_error === void 0 ? {} : { is_error: r.is_error },
9714
9957
  // Coerce to '' as a last resort: a tool_result entry that carries a
9715
9958
  // tool_use_id but no content must still encode as a valid tool_result
9716
9959
  // block (content must be a string for the API), not as undefined —
@@ -9807,6 +10050,7 @@ var RequestEncoder = class {
9807
10050
  return {
9808
10051
  type: "tool_result",
9809
10052
  tool_use_id: result.tool_use_id,
10053
+ ...result.is_error === void 0 ? {} : { is_error: result.is_error },
9810
10054
  content: [
9811
10055
  { type: "text", text: parsed.text },
9812
10056
  {
@@ -9830,6 +10074,7 @@ var RequestEncoder = class {
9830
10074
  return {
9831
10075
  type: "tool_result",
9832
10076
  tool_use_id: result.tool_use_id,
10077
+ ...result.is_error === void 0 ? {} : { is_error: result.is_error },
9833
10078
  content
9834
10079
  };
9835
10080
  });
@@ -9844,13 +10089,13 @@ init_tool_compression();
9844
10089
  // src/agent/runtime/tool_runtime.ts
9845
10090
  init_capabilities();
9846
10091
  init_config();
9847
- import { basename as basename3, dirname as dirname6, join as join13, relative as relative2, resolve as resolve7 } from "path";
10092
+ import { basename as basename3, dirname as dirname6, join as join14, relative as relative2, resolve as resolve7 } from "path";
9848
10093
  import { realpathSync } from "node:fs";
9849
10094
  import { appendFile } from "node:fs/promises";
9850
10095
 
9851
10096
  // src/context/intents_loader.ts
9852
- import { appendFileSync as appendFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync5 } from "fs";
9853
- import { basename as basename2, dirname as dirname5, join as join12, relative, resolve as resolve6 } from "path";
10097
+ import { appendFileSync as appendFileSync5, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync7, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
10098
+ import { basename as basename2, dirname as dirname5, join as join13, relative, resolve as resolve6 } from "path";
9854
10099
  var DEFAULT_MAX_BYTES = 32 * 1024;
9855
10100
  var DEFAULT_FILE_INTENT_THRESHOLD_LINES = 400;
9856
10101
  function loadIntentsContext(cwd = process.cwd(), settings, options = {}) {
@@ -9910,7 +10155,7 @@ function maybeAutoUpdateIntents(options) {
9910
10155
  const relChanged = relative(options.cwd, absolutePath) || basename2(absolutePath);
9911
10156
  const note = `- ${(/* @__PURE__ */ new Date()).toISOString()}: tool \`${options.toolName}\` changed \`${relChanged}\`. Keep this file's higher-level intent current if behavior changed.
9912
10157
  `;
9913
- appendFileSync4(target, note, "utf-8");
10158
+ appendFileSync5(target, note, "utf-8");
9914
10159
  updated.add(relative(options.cwd, target) || basename2(target));
9915
10160
  }
9916
10161
  return [...updated];
@@ -9921,7 +10166,7 @@ function buildIntentCandidates(cwd, relatedPaths, includeHierarchy) {
9921
10166
  if (includeHierarchy) {
9922
10167
  const roots = findRootToCwd(resolve6(cwd));
9923
10168
  for (const dir of roots) {
9924
- pushUnique(candidates, seen, join12(dir, ".ztc-intents.md"));
10169
+ pushUnique(candidates, seen, join13(dir, ".ztc-intents.md"));
9925
10170
  }
9926
10171
  }
9927
10172
  for (const relatedPath of relatedPaths) {
@@ -9935,7 +10180,7 @@ function findRootToCwd(cwd) {
9935
10180
  let current = cwd;
9936
10181
  for (; ; ) {
9937
10182
  chain.push(current);
9938
- if (existsSync4(join12(current, ".git"))) {
10183
+ if (existsSync4(join13(current, ".git"))) {
9939
10184
  break;
9940
10185
  }
9941
10186
  const parent = dirname5(current);
@@ -9950,21 +10195,21 @@ function selectIntentTarget(cwd, absolutePath, threshold) {
9950
10195
  if (isLargeFile(absolutePath, threshold)) {
9951
10196
  return fileIntentPath(absolutePath);
9952
10197
  }
9953
- const dir = existsSync4(absolutePath) && statSync(absolutePath).isDirectory() ? absolutePath : dirname5(absolutePath);
10198
+ const dir = existsSync4(absolutePath) && statSync2(absolutePath).isDirectory() ? absolutePath : dirname5(absolutePath);
9954
10199
  const normalizedCwd = resolve6(cwd);
9955
10200
  const normalizedDir = resolve6(dir);
9956
10201
  if (!isInside(normalizedDir, normalizedCwd)) {
9957
- return join12(normalizedCwd, ".ztc-intents.md");
10202
+ return join13(normalizedCwd, ".ztc-intents.md");
9958
10203
  }
9959
- return join12(normalizedDir, ".ztc-intents.md");
10204
+ return join13(normalizedDir, ".ztc-intents.md");
9960
10205
  }
9961
10206
  function ensureIntentFile(path) {
9962
- mkdirSync3(dirname5(path), { recursive: true });
10207
+ mkdirSync4(dirname5(path), { recursive: true });
9963
10208
  if (existsSync4(path)) {
9964
10209
  return;
9965
10210
  }
9966
10211
  const title = basename2(path) === ".ztc-intents.md" ? "ZTC Folder Intents" : `ZTC File Intents for ${basename2(path).replace(/^\.ztc-intents-/, "").replace(/\.md$/, "")}`;
9967
- writeFileSync5(path, [
10212
+ writeFileSync6(path, [
9968
10213
  `# ${title}`,
9969
10214
  "",
9970
10215
  "Purpose:",
@@ -9974,14 +10219,14 @@ function ensureIntentFile(path) {
9974
10219
  ].join("\n") + "\n", "utf-8");
9975
10220
  }
9976
10221
  function fileIntentPath(absolutePath) {
9977
- return join12(dirname5(absolutePath), `.ztc-intents-${basename2(absolutePath)}.md`);
10222
+ return join13(dirname5(absolutePath), `.ztc-intents-${basename2(absolutePath)}.md`);
9978
10223
  }
9979
10224
  function isLargeFile(path, threshold) {
9980
10225
  try {
9981
- if (!existsSync4(path) || statSync(path).isDirectory()) {
10226
+ if (!existsSync4(path) || statSync2(path).isDirectory()) {
9982
10227
  return false;
9983
10228
  }
9984
- const raw = readFileSync6(path, "utf-8");
10229
+ const raw = readFileSync7(path, "utf-8");
9985
10230
  return raw.split("\n").length >= threshold;
9986
10231
  } catch {
9987
10232
  return false;
@@ -9992,7 +10237,7 @@ function loadBudgetedFile(path, maxBytes) {
9992
10237
  return null;
9993
10238
  }
9994
10239
  try {
9995
- const raw = readFileSync6(path, "utf-8");
10240
+ const raw = readFileSync7(path, "utf-8");
9996
10241
  const bytes = Buffer.byteLength(raw, "utf-8");
9997
10242
  if (bytes <= maxBytes) {
9998
10243
  return {
@@ -10028,18 +10273,6 @@ function isInside(path, root) {
10028
10273
  return path === root || path.startsWith(`${root}/`);
10029
10274
  }
10030
10275
 
10031
- // src/agent/runtime/tool_effects.ts
10032
- init_capabilities();
10033
- function resolveToolEffect(tool) {
10034
- if (tool.effect) return tool.effect;
10035
- const capabilities = new Set(tool.capabilities ?? []);
10036
- if (capabilities.has("shell_exec" /* SHELL_EXEC */) || capabilities.has("network" /* NETWORK */) || capabilities.has("computer_control" /* COMPUTER_CONTROL */) || capabilities.has("ml_inference" /* ML_INFERENCE */)) {
10037
- return "external";
10038
- }
10039
- if (capabilities.has("file_write" /* FILE_WRITE */)) return "write";
10040
- return "read";
10041
- }
10042
-
10043
10276
  // src/agent/runtime/tool_runtime.ts
10044
10277
  function getTool(name, tools) {
10045
10278
  return tools.find((t) => t.definition.name === name);
@@ -10049,7 +10282,7 @@ function isHostControlPlaneWrite(tool, args, cwd) {
10049
10282
  try {
10050
10283
  const noFollowLeaf = (path) => {
10051
10284
  const absolute = resolve7(path);
10052
- return join13(realpathSync(dirname6(absolute)), basename3(absolute));
10285
+ return join14(realpathSync(dirname6(absolute)), basename3(absolute));
10053
10286
  };
10054
10287
  return noFollowLeaf(resolve7(cwd, args.path)) === noFollowLeaf(tool.hostControlPlaneWritePath);
10055
10288
  } catch {
@@ -10058,7 +10291,7 @@ function isHostControlPlaneWrite(tool, args, cwd) {
10058
10291
  }
10059
10292
  async function executeTool(name, args, tools, context) {
10060
10293
  const tool = getTool(name, tools);
10061
- if (!tool) throw new Error("Unknown tool: " + name);
10294
+ if (!tool) throw new Error(`Unknown tool: ${name}. It is unavailable in the current mode or provider surface. Inspect current execution capabilities and use an available tool; shell execution requires run_command or a registered alias.`);
10062
10295
  return tool.execute(args, context);
10063
10296
  }
10064
10297
  function buildHookText(messages) {
@@ -10117,7 +10350,7 @@ ${intentContext}`;
10117
10350
  async function recordToolTrace(entry) {
10118
10351
  const configured = process.env.ZTC_TOOL_TRACE;
10119
10352
  if (!configured) return;
10120
- const target = configured === "1" ? join13(process.cwd(), "ztc-tool-trace.jsonl") : configured;
10353
+ const target = configured === "1" ? join14(process.cwd(), "ztc-tool-trace.jsonl") : configured;
10121
10354
  try {
10122
10355
  await appendFile(target, `${JSON.stringify(entry)}
10123
10356
  `, "utf-8");
@@ -10325,6 +10558,7 @@ async function executeToolWithRuntime(name, args, options) {
10325
10558
  if (timer) clearTimeout(timer);
10326
10559
  });
10327
10560
  }
10561
+ if (tool?.isErrorResult?.(output)) throw new Error(output);
10328
10562
  const TOOL_OUTPUT_CAP = 8e4;
10329
10563
  output = augmentReadResultWithIntent(name, effectiveArgs, output, cwd);
10330
10564
  let resultContent = output;
@@ -10399,31 +10633,6 @@ async function executeToolWithRuntime(name, args, options) {
10399
10633
  }
10400
10634
  }
10401
10635
 
10402
- // src/utils/subprocess_env.ts
10403
- var USER_SUBPROCESS_STRIPPED_ZTC_ENV = [
10404
- "ZERG_ROUTER_BASE_URL",
10405
- "ZTC_AUTH_TOKEN",
10406
- "ZTC_CHILD_ENTRYPOINT",
10407
- "ZTC_COLONY_CHILD",
10408
- "ZTC_COLONY_PARENT_MIRROR_PORT",
10409
- "ZTC_COLONY_WORKTREE_ID",
10410
- "ZTC_MIRROR_PORT",
10411
- "ZTC_MIRROR_INTERNAL_TOKEN",
10412
- "ZTC_MIRROR_REQUIRE_AUTH",
10413
- "ZTC_MIRROR_SHARED_SECRET",
10414
- "ZTC_NAVIGATOR_GROUP",
10415
- "ZTC_PANE_ID",
10416
- "ZTC_PANE_RELAY",
10417
- "ZTC_PARENT_PID"
10418
- ];
10419
- function buildUserSubprocessEnv(source = process.env) {
10420
- const env = { ...source };
10421
- for (const key of USER_SUBPROCESS_STRIPPED_ZTC_ENV) {
10422
- delete env[key];
10423
- }
10424
- return env;
10425
- }
10426
-
10427
10636
  // src/utils/compaction.ts
10428
10637
  init_provider_ids();
10429
10638
  init_context();
@@ -10593,6 +10802,9 @@ var Agent = class _Agent {
10593
10802
  maxTokens: config.maxTokens,
10594
10803
  routePlan: config.routePlan,
10595
10804
  maxIterations: config.maxIterations || 10,
10805
+ execution: resolveExecutionSettings(config.execution),
10806
+ sessionId: config.sessionId ?? randomUUID6(),
10807
+ runtimeFacts: config.runtimeFacts ?? {},
10596
10808
  apiRetry: resolveApiRetrySettings({ apiRetry: config.apiRetry }),
10597
10809
  llmRequestTimeoutMs: resolveLlmRequestTimeoutMs(
10598
10810
  config.llmRequestTimeoutMs,
@@ -10821,7 +11033,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
10821
11033
  const ext = extname(path).toLowerCase();
10822
11034
  const mediaType = this.getImageMediaType(ext);
10823
11035
  if (!mediaType) return null;
10824
- const buffer = readFileSync7(path);
11036
+ const buffer = readFileSync8(path);
10825
11037
  return {
10826
11038
  type: "image",
10827
11039
  mediaType,
@@ -10881,7 +11093,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
10881
11093
  const ext = extname(path).toLowerCase();
10882
11094
  const mediaType = this.getVideoMediaType(ext);
10883
11095
  if (!mediaType) return null;
10884
- const buffer = readFileSync7(path);
11096
+ const buffer = readFileSync8(path);
10885
11097
  return {
10886
11098
  type: "video",
10887
11099
  mediaType,
@@ -10915,7 +11127,8 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
10915
11127
  if (!Array.isArray(parsed)) return null;
10916
11128
  return parsed.filter((item) => typeof item.tool_use_id === "string").map((item) => ({
10917
11129
  tool_use_id: item.tool_use_id,
10918
- content: typeof item.content === "string" ? item.content : String(item.content ?? "")
11130
+ content: typeof item.content === "string" ? item.content : String(item.content ?? ""),
11131
+ is_error: item.is_error
10919
11132
  }));
10920
11133
  } catch {
10921
11134
  return null;
@@ -10996,7 +11209,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
10996
11209
  [truncated \u2014 ${content.length} chars total]`;
10997
11210
  }
10998
11211
  emittedToolResultIds.add(tc.id);
10999
- return { tool_use_id: tc.id, content };
11212
+ return { tool_use_id: tc.id, content, is_error: Boolean(tc.error) || tc.result == null };
11000
11213
  });
11001
11214
  if (toolResults.length === 0) continue;
11002
11215
  const syntheticReason = hasStoredResults ? "synth_tui_tool_results" : "repair_orphaned_tool_use";
@@ -11011,12 +11224,30 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11011
11224
  return repaired;
11012
11225
  }
11013
11226
  buildRequest(messages, availableTools = this.config.tools) {
11227
+ if (this.config.pinTaskHeader) {
11228
+ const facts = describeExecutionContext({ ...this.getExecutionContext(), tools: availableTools.map((tool) => tool.definition.name) });
11229
+ const latestFacts = [...messages].reverse().find((message) => typeof message.metadata?.executionContext === "string")?.metadata?.executionContext;
11230
+ if (facts !== latestFacts) {
11231
+ const latestUser = [...messages].reverse().find((message) => message.role === "user");
11232
+ if (latestUser) latestUser.metadata = { ...latestUser.metadata, executionContext: facts };
11233
+ }
11234
+ }
11014
11235
  const repairedMessages = this.repairConversationHistory(messages);
11236
+ const contextMessages = this.config.pinTaskHeader ? repairedMessages.flatMap((message) => {
11237
+ const facts = message.metadata?.executionContext;
11238
+ return typeof facts === "string" ? [message, {
11239
+ id: `${message.id}_execution`,
11240
+ role: "user",
11241
+ content: facts,
11242
+ timestamp: message.timestamp,
11243
+ metadata: { synthetic: true }
11244
+ }] : [message];
11245
+ }) : repairedMessages;
11015
11246
  return {
11016
11247
  model: this.config.model,
11017
11248
  maxTokens: this.config.maxTokens,
11018
11249
  system: this.systemWithTaskHeader(messages),
11019
- messages: this.formatMessages(repairedMessages),
11250
+ messages: this.formatMessages(contextMessages),
11020
11251
  tools: getRequestToolDefinitions(
11021
11252
  availableTools,
11022
11253
  this.discoveredTools,
@@ -11211,11 +11442,9 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11211
11442
  */
11212
11443
  _taskHeader = null;
11213
11444
  /**
11214
- * Drop the pinned task so the NEXT conversation re-captures its own first user
11215
- * message. Wired into the /clear-only path (clearTransientState), never into
11445
+ * Drop the explicitly pinned task for the next conversation. Wired into the /clear-only path (clearTransientState), never into
11216
11446
  * /compact — the pin must survive compaction (that is the whole point of R7).
11217
- * Null (not '') so systemWithTaskHeader recaptures rather than treating it as
11218
- * "no task found."
11447
+ * Null represents no explicitly pinned goal.
11219
11448
  */
11220
11449
  clearPinnedTask() {
11221
11450
  this._taskHeader = null;
@@ -11230,23 +11459,28 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11230
11459
  * for the whole run. A driver that starts or switches a goal calls this so
11231
11460
  * request.system and the goal's per-turn re-injection name the same objective.
11232
11461
  *
11233
- * A blank value falls back to recapturing, matching clearPinnedTask.
11462
+ * A blank value clears the pin, matching clearPinnedTask.
11234
11463
  */
11235
11464
  setPinnedTask(task) {
11236
11465
  const trimmed = task.trim();
11237
11466
  this._taskHeader = trimmed.length > 0 ? trimmed : null;
11238
11467
  }
11239
11468
  /** Append the pinned task header (R7) to the static system prompt. */
11240
- systemWithTaskHeader(messages) {
11241
- if (!this.config.pinTaskHeader) {
11242
- return this.config.systemPrompt;
11243
- }
11244
- if (this._taskHeader === null) {
11245
- const firstUser = messages.find(
11246
- (m) => m.role === "user" && !m.metadata?.synthetic && !m.metadata?.toolResult && typeof m.content === "string" && m.content.trim().length > 0
11247
- );
11248
- this._taskHeader = firstUser ? firstUser.content.trim().slice(0, 2e3) : "";
11249
- }
11469
+ getExecutionContext() {
11470
+ return {
11471
+ mode: this.config.runtimeFacts.mode ?? "normal",
11472
+ cwd: this._cwd,
11473
+ tools: this.getConfiguredToolNames(),
11474
+ mux: this.config.runtimeFacts.mux ?? false,
11475
+ configScope: this.config.runtimeFacts.configScope ?? "host configuration",
11476
+ maxTokens: this.config.maxTokens,
11477
+ outputLimitSource: this.config.runtimeFacts.outputLimitSource ?? (this.config.maxTokens === void 0 ? "provider default" : "explicit configuration"),
11478
+ settings: resolveExecutionSettings(this.config.execution),
11479
+ jobs: listCommandJobs(this.config.sessionId)
11480
+ };
11481
+ }
11482
+ systemWithTaskHeader(_messages) {
11483
+ if (!this.config.pinTaskHeader) return this.config.systemPrompt;
11250
11484
  return pinnedTaskSystem(this.config.systemPrompt, this._taskHeader);
11251
11485
  }
11252
11486
  /**
@@ -11255,6 +11489,17 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11255
11489
  * the same core loop used by unhosted ZTC.
11256
11490
  */
11257
11491
  async run(messages, signal, takeSteering) {
11492
+ const onInterrupt = () => {
11493
+ if (this.config.execution.interruptBackgroundJobs) void interruptCommandJobs(this.config.sessionId);
11494
+ };
11495
+ signal?.addEventListener("abort", onInterrupt, { once: true });
11496
+ try {
11497
+ return await this.runBound(messages, signal, takeSteering);
11498
+ } finally {
11499
+ signal?.removeEventListener("abort", onInterrupt);
11500
+ }
11501
+ }
11502
+ async runBound(messages, signal, takeSteering) {
11258
11503
  const binding = this.#conversationBinding;
11259
11504
  if (!binding) return this.runCore(messages, signal, takeSteering);
11260
11505
  const userMessage = [...messages].reverse().find(
@@ -11293,7 +11538,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11293
11538
  }
11294
11539
  const result = await this.runCore(workingMessages, signal);
11295
11540
  const assistantMessage = {
11296
- id: `host_assistant_${randomUUID5()}`,
11541
+ id: `host_assistant_${randomUUID6()}`,
11297
11542
  role: "assistant",
11298
11543
  content: result.content,
11299
11544
  timestamp: /* @__PURE__ */ new Date(),
@@ -11334,7 +11579,6 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11334
11579
  }
11335
11580
  const allToolCalls = [];
11336
11581
  const turnMessages = [];
11337
- let acceptedSteering = false;
11338
11582
  let iterations = 0;
11339
11583
  this._consecutiveAutoContinues = 0;
11340
11584
  let finalContent = "";
@@ -11344,6 +11588,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11344
11588
  let runError = null;
11345
11589
  let stopReason = "max_iterations";
11346
11590
  const applyRepeatGuard = createRepeatGuard();
11591
+ const recovery = createRecoveryGuard(this.config.execution);
11347
11592
  try {
11348
11593
  while (iterations < this.config.maxIterations) {
11349
11594
  if (signal?.aborted) {
@@ -11445,7 +11690,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11445
11690
  data: { tool: toolBlock.name }
11446
11691
  });
11447
11692
  const guarded = applyRepeatGuard(toolBlock.name, toolBlock.input, content);
11448
- return { toolCall, toolResult: { tool_use_id: toolBlock.id, content: guarded } };
11693
+ return { toolCall, toolResult: { tool_use_id: toolBlock.id, content: guarded, is_error: false } };
11449
11694
  };
11450
11695
  const recordToolError = (toolBlock, toolCall, error, effectiveArgs) => {
11451
11696
  toolCall.completedAt = /* @__PURE__ */ new Date();
@@ -11462,7 +11707,8 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11462
11707
  toolCall,
11463
11708
  toolResult: {
11464
11709
  tool_use_id: toolBlock.id,
11465
- content: applyRepeatGuard(toolBlock.name, toolBlock.input, `Error: ${error}`)
11710
+ content: applyRepeatGuard(toolBlock.name, toolBlock.input, `Error: ${error}`),
11711
+ is_error: true
11466
11712
  }
11467
11713
  };
11468
11714
  };
@@ -11475,7 +11721,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11475
11721
  const exclusiveTransitionId = toolUseBlocks.find(
11476
11722
  (toolBlock) => toolBlock.name === "plan_mode"
11477
11723
  )?.id ?? null;
11478
- const toolPromises = toolUseBlocks.map(async (toolBlock) => {
11724
+ const combined = await scheduleToolBatch(toolUseBlocks, this.config.tools, this.config.execution, async (toolBlock, skipReason) => {
11479
11725
  const toolCall = {
11480
11726
  id: toolBlock.id,
11481
11727
  name: toolBlock.name,
@@ -11489,6 +11735,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11489
11735
  data: { tool: toolBlock.name }
11490
11736
  });
11491
11737
  this.emit({ type: "tool_start", tool: toolBlock.name, args: toolBlock.input, toolCallId: toolBlock.id });
11738
+ if (skipReason) return recordToolError(toolBlock, toolCall, skipReason);
11492
11739
  if (exclusiveTransitionId !== null && toolBlock.id !== exclusiveTransitionId) {
11493
11740
  return recordToolError(
11494
11741
  toolBlock,
@@ -11530,6 +11777,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11530
11777
  context: {
11531
11778
  cwd: this._cwd,
11532
11779
  toolCallId: toolBlock.id,
11780
+ sessionId: this.config.sessionId,
11533
11781
  signal,
11534
11782
  toolResponseStore: this.toolResponseStore,
11535
11783
  discoveredTools: this.discoveredTools,
@@ -11556,6 +11804,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11556
11804
  context: {
11557
11805
  cwd: this._cwd,
11558
11806
  toolCallId: toolBlock.id,
11807
+ sessionId: this.config.sessionId,
11559
11808
  signal,
11560
11809
  toolResponseStore: this.toolResponseStore,
11561
11810
  discoveredTools: this.discoveredTools,
@@ -11579,8 +11828,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11579
11828
  const error = err.message;
11580
11829
  return recordToolError(toolBlock, toolCall, error);
11581
11830
  }
11582
- });
11583
- const combined = await Promise.all(toolPromises);
11831
+ }, (result) => result.toolCall.status === "error" || commandStillRunning(result.toolResult.content), signal, exclusiveTransitionId);
11584
11832
  allToolCalls.push(...combined.map((r) => r.toolCall));
11585
11833
  const partial = Boolean(signal?.aborted);
11586
11834
  const toolResults = combined.map((r) => {
@@ -11591,20 +11839,14 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11591
11839
  partial,
11592
11840
  {
11593
11841
  sourcePath: extractSourcePath(r.toolCall.name, r.toolCall.args),
11594
- dedupable: r.toolCall.status === "complete"
11842
+ dedupable: r.toolCall.status === "complete",
11843
+ isError: r.toolResult.is_error
11595
11844
  }
11596
11845
  );
11597
- return { tool_use_id: r.toolResult.tool_use_id, content: rendered };
11846
+ return { tool_use_id: r.toolResult.tool_use_id, content: rendered, is_error: r.toolResult.is_error };
11598
11847
  });
11599
- if (signal?.aborted) {
11600
- throw new AgentError("Cancelled", "CANCELLED");
11601
- }
11602
11848
  this._consecutiveAutoContinues = 0;
11603
11849
  const textContent = response.content.filter(this.isTextBlock).map((b) => b.text).join("\n");
11604
- if (textContent && textContent.trim()) {
11605
- if (!this._lastResponseStreamed) await this.emitStreamText(textContent, signal);
11606
- lastToolTurnText = textContent;
11607
- }
11608
11850
  const toolUseMessage = {
11609
11851
  id: `assistant_${Date.now()}`,
11610
11852
  role: "assistant",
@@ -11624,6 +11866,30 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11624
11866
  };
11625
11867
  messages.push(toolResultMessage);
11626
11868
  turnMessages.push(toolResultMessage);
11869
+ this.emit({ type: "history_checkpoint", messages: [...turnMessages] });
11870
+ if (signal?.aborted) throw new AgentError("Cancelled", "CANCELLED");
11871
+ if (textContent && textContent.trim() && !signal?.aborted) {
11872
+ if (!this._lastResponseStreamed) await this.emitStreamText(textContent, signal);
11873
+ lastToolTurnText = textContent;
11874
+ }
11875
+ const recoveryAction = recovery(combined.map((result) => ({
11876
+ name: result.toolCall.name,
11877
+ content: result.toolResult.content,
11878
+ isError: result.toolResult.is_error
11879
+ })));
11880
+ if (recoveryAction === "stop") throw new AgentError("Automatic recovery budget exhausted after repeated tool failures. Inspect the retained results and current capabilities before retrying.", "RECOVERY_EXHAUSTED");
11881
+ if (recoveryAction === "recover") {
11882
+ const recoveryMessage = {
11883
+ id: `recovery_${randomUUID6()}`,
11884
+ role: "user",
11885
+ content: "[Recovery] Repeated failures share the same cause. Stop that sequence. Inspect current capabilities and active jobs, then make one changed recovery attempt. Use file/stdin tools for scripts and raw Ctrl+C for stuck shell input, then read the pane in the same response or immediately afterward. A silent running-job poll allows one further inspection before stopping; do not repeat empty polls. Continue authorized work; use ask_user only for a concrete missing prerequisite. Do not repeat an acknowledgement as proof of success.",
11886
+ metadata: { synthetic: true },
11887
+ timestamp: /* @__PURE__ */ new Date()
11888
+ };
11889
+ messages.push(recoveryMessage);
11890
+ turnMessages.push(recoveryMessage);
11891
+ this.emit({ type: "history_checkpoint", messages: [...turnMessages] });
11892
+ }
11627
11893
  if (combined.length === 1 && combined[0].toolCall.status === "complete") {
11628
11894
  const call = combined[0].toolCall;
11629
11895
  const tool = this.config.tools.find((tool2) => tool2.definition.name === call.name);
@@ -11651,7 +11917,6 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11651
11917
  if (steering.length > 0) {
11652
11918
  messages.push(...steering);
11653
11919
  turnMessages.push(...steering);
11654
- acceptedSteering = true;
11655
11920
  }
11656
11921
  }
11657
11922
  } catch (err) {
@@ -11694,7 +11959,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11694
11959
  usage: totalUsage || void 0,
11695
11960
  stopReason,
11696
11961
  ...finalReasoning ? { reasoning: finalReasoning } : {},
11697
- ...acceptedSteering ? { turnMessages } : {}
11962
+ turnMessages
11698
11963
  };
11699
11964
  }
11700
11965
  /**
@@ -11732,6 +11997,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11732
11997
  context: {
11733
11998
  cwd: this._cwd,
11734
11999
  toolCallId,
12000
+ sessionId: this.config.sessionId,
11735
12001
  signal: options.signal,
11736
12002
  toolResponseStore: this.toolResponseStore,
11737
12003
  discoveredTools: this.discoveredTools,
@@ -11776,7 +12042,7 @@ INVESTIGATION STRATEGY \u2014 before making tool calls to explore or debug, foll
11776
12042
  const { maxAttempts, baseDelayMs: baseDelay } = this.config.apiRetry;
11777
12043
  const timeoutMs = this.config.llmRequestTimeoutMs;
11778
12044
  if (this.backend.handlesTransportRetries?.(request) === true) {
11779
- request = { ...request, transportTurnId: request.transportTurnId ?? randomUUID5() };
12045
+ request = { ...request, transportTurnId: request.transportTurnId ?? randomUUID6() };
11780
12046
  }
11781
12047
  const useStream = process.env.ZTC_NO_STREAM !== "1" && !this.backend.streamsSyntheticallyFor?.(request);
11782
12048
  let answerOutputEmitted = false;
@@ -12007,20 +12273,20 @@ init_capabilities();
12007
12273
  // src/skills/loader.ts
12008
12274
  init_logger();
12009
12275
  init_extension_discovery();
12010
- import { homedir as homedir7 } from "os";
12011
- import { join as join14, resolve as resolve8, basename as basename4 } from "path";
12276
+ import { homedir as homedir8 } from "os";
12277
+ import { join as join15, resolve as resolve8, basename as basename4 } from "path";
12012
12278
  import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
12013
12279
  var MAX_DEPTH = 4;
12014
12280
  function defaultMarkdownRoots() {
12015
12281
  const explicitRoots = extensionPathsFromEnvironment("ZTC_SKILL_PATHS");
12016
12282
  if (isExplicitExtensionDiscovery()) return explicitRoots;
12017
- const home = homedir7();
12283
+ const home = homedir8();
12018
12284
  return [
12019
12285
  ...explicitRoots,
12020
- join14(process.cwd(), ".ztc", "skills"),
12021
- join14(process.cwd(), ".claude", "skills"),
12022
- join14(home, ".ztc", "skills"),
12023
- join14(home, ".claude", "skills")
12286
+ join15(process.cwd(), ".ztc", "skills"),
12287
+ join15(process.cwd(), ".claude", "skills"),
12288
+ join15(home, ".ztc", "skills"),
12289
+ join15(home, ".claude", "skills")
12024
12290
  ];
12025
12291
  }
12026
12292
  async function walk(dir, depth = 0) {
@@ -12030,7 +12296,7 @@ async function walk(dir, depth = 0) {
12030
12296
  const results = [];
12031
12297
  for (const entry of entries) {
12032
12298
  if (entry.name.startsWith(".")) continue;
12033
- const full = join14(dir, entry.name);
12299
+ const full = join15(dir, entry.name);
12034
12300
  if (entry.isDirectory()) {
12035
12301
  results.push(...await walk(full, depth + 1));
12036
12302
  } else if (entry.isFile() && entry.name.toLowerCase() === "skill.md") {
@@ -12177,7 +12443,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
12177
12443
 
12178
12444
  // src/skills/bundled/init-context.ts
12179
12445
  import { readdir as readdir3, readFile as readFile5, stat } from "fs/promises";
12180
- import { join as join15 } from "path";
12446
+ import { join as join16 } from "path";
12181
12447
  var MAX_ENTRIES = 60;
12182
12448
  var TOP_LEVEL_DIRS_OF_INTEREST = [
12183
12449
  "src",
@@ -12238,7 +12504,7 @@ async function safeReadDir(path) {
12238
12504
  async function detectManifests(cwd) {
12239
12505
  const found = [];
12240
12506
  for (const { file, label } of MANIFESTS) {
12241
- if (await fileExists(join15(cwd, file))) {
12507
+ if (await fileExists(join16(cwd, file))) {
12242
12508
  found.push(`- ${file} \u2014 ${label}`);
12243
12509
  }
12244
12510
  }
@@ -12246,7 +12512,7 @@ async function detectManifests(cwd) {
12246
12512
  }
12247
12513
  async function readClaudeMd(cwd) {
12248
12514
  for (const name of CLAUDE_MD_CANDIDATES) {
12249
- const path = join15(cwd, name);
12515
+ const path = join16(cwd, name);
12250
12516
  if (await fileExists(path)) {
12251
12517
  try {
12252
12518
  const contents = await readFile5(path, "utf-8");
@@ -12505,7 +12771,7 @@ var invokeSkillTool = makeInvokeSkillTool(resolveAmbientSkills);
12505
12771
  // src/zlm.ts
12506
12772
  var ZLM_EMBED_API_VERSION = "zerglang.ztc-embed/1";
12507
12773
  var ZLM_STDIO_PROTOCOL = "zerglang.ztc-host/1";
12508
- var ZLM_ZTC_VERSION = true ? "0.3.0-beta.1" : "development";
12774
+ var ZLM_ZTC_VERSION = true ? "0.4.0-beta.1" : "development";
12509
12775
  var ZLM_MAX_HOST_TOOLS = 64;
12510
12776
  var ZLM_MAX_SYSTEM_PROMPT_BYTES = 128 * 1024;
12511
12777
  var ZLM_HOST_APPROVAL_CAPABILITIES = Object.freeze([