toyon 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/CHANGELOG.md +36 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/daemon.js +659 -213
  4. package/dist/shell/assets/{Editor-Dq4a0gFv.js → Editor-v7DPr3mY.js} +82 -82
  5. package/dist/shell/assets/Editor-v7DPr3mY.js.br +0 -0
  6. package/dist/shell/assets/Editor-v7DPr3mY.js.gz +0 -0
  7. package/dist/shell/assets/{XTerm-BfuBg96X.js → XTerm-CxU9R8q7.js} +1 -1
  8. package/dist/shell/assets/XTerm-CxU9R8q7.js.br +0 -0
  9. package/dist/shell/assets/XTerm-CxU9R8q7.js.gz +0 -0
  10. package/dist/shell/assets/{cssMode-An1EUPnJ.js → cssMode-DD5SBqGU.js} +1 -1
  11. package/dist/shell/assets/cssMode-DD5SBqGU.js.br +0 -0
  12. package/dist/shell/assets/cssMode-DD5SBqGU.js.gz +0 -0
  13. package/dist/shell/assets/{freemarker2-PYOY2M7v.js → freemarker2-CZ3XZhjv.js} +1 -1
  14. package/dist/shell/assets/freemarker2-CZ3XZhjv.js.br +0 -0
  15. package/dist/shell/assets/freemarker2-CZ3XZhjv.js.gz +0 -0
  16. package/dist/shell/assets/{grammar-e2yQ-Xdq.js → grammar-DqAbMw7d.js} +1 -1
  17. package/dist/shell/assets/grammar-DqAbMw7d.js.br +0 -0
  18. package/dist/shell/assets/grammar-DqAbMw7d.js.gz +0 -0
  19. package/dist/shell/assets/{handlebars-BtSTwhBl.js → handlebars-CzZa_iMI.js} +1 -1
  20. package/dist/shell/assets/handlebars-CzZa_iMI.js.br +0 -0
  21. package/dist/shell/assets/handlebars-CzZa_iMI.js.gz +0 -0
  22. package/dist/shell/assets/{html-lmman0qi.js → html-uU_fKUCO.js} +1 -1
  23. package/dist/shell/assets/html-uU_fKUCO.js.br +0 -0
  24. package/dist/shell/assets/html-uU_fKUCO.js.gz +0 -0
  25. package/dist/shell/assets/{htmlMode-CYUUrmIv.js → htmlMode-DmuqvqLQ.js} +1 -1
  26. package/dist/shell/assets/htmlMode-DmuqvqLQ.js.br +0 -0
  27. package/dist/shell/assets/htmlMode-DmuqvqLQ.js.gz +0 -0
  28. package/dist/shell/assets/index-DMZqXgxq.js +295 -0
  29. package/dist/shell/assets/index-DMZqXgxq.js.br +0 -0
  30. package/dist/shell/assets/index-DMZqXgxq.js.gz +0 -0
  31. package/dist/shell/assets/index-LFCDUSQF.css +1 -0
  32. package/dist/shell/assets/index-LFCDUSQF.css.br +0 -0
  33. package/dist/shell/assets/index-LFCDUSQF.css.gz +0 -0
  34. package/dist/shell/assets/{javascript-CHVOacyY.js → javascript-CvDukmsa.js} +1 -1
  35. package/dist/shell/assets/{jsonMode-WaLJvHyp.js → jsonMode-C8ZGvKb7.js} +1 -1
  36. package/dist/shell/assets/jsonMode-C8ZGvKb7.js.br +0 -0
  37. package/dist/shell/assets/jsonMode-C8ZGvKb7.js.gz +0 -0
  38. package/dist/shell/assets/{liquid-Bhzd8TLd.js → liquid-CSFER4cS.js} +1 -1
  39. package/dist/shell/assets/liquid-CSFER4cS.js.br +0 -0
  40. package/dist/shell/assets/liquid-CSFER4cS.js.gz +0 -0
  41. package/dist/shell/assets/{lspLanguageFeatures-DS22S_fR.js → lspLanguageFeatures-Bnq8PCF_.js} +1 -1
  42. package/dist/shell/assets/lspLanguageFeatures-Bnq8PCF_.js.br +0 -0
  43. package/dist/shell/assets/lspLanguageFeatures-Bnq8PCF_.js.gz +0 -0
  44. package/dist/shell/assets/{mdx-1x45Dt5R.js → mdx-DN7UGs67.js} +1 -1
  45. package/dist/shell/assets/mdx-DN7UGs67.js.br +0 -0
  46. package/dist/shell/assets/mdx-DN7UGs67.js.gz +0 -0
  47. package/dist/shell/assets/{python-B0fHYRyj.js → python-Cdtmy77f.js} +1 -1
  48. package/dist/shell/assets/python-Cdtmy77f.js.br +0 -0
  49. package/dist/shell/assets/python-Cdtmy77f.js.gz +0 -0
  50. package/dist/shell/assets/{razor-C48vGnF3.js → razor-BzMJ8H5n.js} +1 -1
  51. package/dist/shell/assets/razor-BzMJ8H5n.js.br +0 -0
  52. package/dist/shell/assets/razor-BzMJ8H5n.js.gz +0 -0
  53. package/dist/shell/assets/toyon-mono-bold-1tbrivW1.woff2 +0 -0
  54. package/dist/shell/assets/toyon-mono-regular-BUtE4EFW.woff2 +0 -0
  55. package/dist/shell/assets/{tsMode-2SIN2sKE.js → tsMode-B_VeTgWt.js} +1 -1
  56. package/dist/shell/assets/tsMode-B_VeTgWt.js.br +0 -0
  57. package/dist/shell/assets/tsMode-B_VeTgWt.js.gz +0 -0
  58. package/dist/shell/assets/{xml-DPJN-pu8.js → xml-Cdv7rs1C.js} +1 -1
  59. package/dist/shell/assets/xml-Cdv7rs1C.js.br +0 -0
  60. package/dist/shell/assets/xml-Cdv7rs1C.js.gz +0 -0
  61. package/dist/shell/assets/{yaml-CDzmW7EM.js → yaml-Co_E2Q5j.js} +1 -1
  62. package/dist/shell/assets/yaml-Co_E2Q5j.js.br +0 -0
  63. package/dist/shell/assets/yaml-Co_E2Q5j.js.gz +0 -0
  64. package/dist/shell/index.html +2 -2
  65. package/package.json +1 -1
  66. package/dist/shell/assets/Editor-Dq4a0gFv.js.br +0 -0
  67. package/dist/shell/assets/Editor-Dq4a0gFv.js.gz +0 -0
  68. package/dist/shell/assets/XTerm-BfuBg96X.js.br +0 -0
  69. package/dist/shell/assets/XTerm-BfuBg96X.js.gz +0 -0
  70. package/dist/shell/assets/cssMode-An1EUPnJ.js.br +0 -0
  71. package/dist/shell/assets/cssMode-An1EUPnJ.js.gz +0 -0
  72. package/dist/shell/assets/freemarker2-PYOY2M7v.js.br +0 -0
  73. package/dist/shell/assets/freemarker2-PYOY2M7v.js.gz +0 -0
  74. package/dist/shell/assets/grammar-e2yQ-Xdq.js.br +0 -0
  75. package/dist/shell/assets/grammar-e2yQ-Xdq.js.gz +0 -0
  76. package/dist/shell/assets/handlebars-BtSTwhBl.js.br +0 -0
  77. package/dist/shell/assets/handlebars-BtSTwhBl.js.gz +0 -0
  78. package/dist/shell/assets/html-lmman0qi.js.br +0 -0
  79. package/dist/shell/assets/html-lmman0qi.js.gz +0 -0
  80. package/dist/shell/assets/htmlMode-CYUUrmIv.js.br +0 -0
  81. package/dist/shell/assets/htmlMode-CYUUrmIv.js.gz +0 -0
  82. package/dist/shell/assets/index-BDzHAr7c.css +0 -1
  83. package/dist/shell/assets/index-BDzHAr7c.css.br +0 -0
  84. package/dist/shell/assets/index-BDzHAr7c.css.gz +0 -0
  85. package/dist/shell/assets/index-o7SjsIYr.js +0 -294
  86. package/dist/shell/assets/index-o7SjsIYr.js.br +0 -0
  87. package/dist/shell/assets/index-o7SjsIYr.js.gz +0 -0
  88. package/dist/shell/assets/jsonMode-WaLJvHyp.js.br +0 -0
  89. package/dist/shell/assets/jsonMode-WaLJvHyp.js.gz +0 -0
  90. package/dist/shell/assets/liquid-Bhzd8TLd.js.br +0 -0
  91. package/dist/shell/assets/liquid-Bhzd8TLd.js.gz +0 -0
  92. package/dist/shell/assets/lspLanguageFeatures-DS22S_fR.js.br +0 -0
  93. package/dist/shell/assets/lspLanguageFeatures-DS22S_fR.js.gz +0 -0
  94. package/dist/shell/assets/mdx-1x45Dt5R.js.br +0 -0
  95. package/dist/shell/assets/mdx-1x45Dt5R.js.gz +0 -0
  96. package/dist/shell/assets/python-B0fHYRyj.js.br +0 -0
  97. package/dist/shell/assets/python-B0fHYRyj.js.gz +0 -0
  98. package/dist/shell/assets/razor-C48vGnF3.js.br +0 -0
  99. package/dist/shell/assets/razor-C48vGnF3.js.gz +0 -0
  100. package/dist/shell/assets/toyon-mono-bold-u3Ky0k2u.woff2 +0 -0
  101. package/dist/shell/assets/toyon-mono-regular-BC3CdGld.woff2 +0 -0
  102. package/dist/shell/assets/tsMode-2SIN2sKE.js.br +0 -0
  103. package/dist/shell/assets/tsMode-2SIN2sKE.js.gz +0 -0
  104. package/dist/shell/assets/xml-DPJN-pu8.js.br +0 -0
  105. package/dist/shell/assets/xml-DPJN-pu8.js.gz +0 -0
  106. package/dist/shell/assets/yaml-CDzmW7EM.js.br +0 -0
  107. package/dist/shell/assets/yaml-CDzmW7EM.js.gz +0 -0
package/dist/daemon.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // ../daemon/src/index.ts
3
3
  import { rmSync as rmSync10, writeFileSync as writeFileSync14 } from "fs";
4
4
  import { homedir as homedir15 } from "os";
5
- import { dirname as dirname16, join as join37 } from "path";
5
+ import { dirname as dirname17, join as join37 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // ../shared/src/protocol/events.ts
@@ -14,6 +14,7 @@ var TOYON_MCP_SERVER = "toyon";
14
14
  var HANDOFF_TOOL = "handoff";
15
15
  var WORKTREES_TOOL = "worktrees";
16
16
  var OWN_TOOL = "own";
17
+ var STOP_TOOL = "stop";
17
18
  function coalesce(entries) {
18
19
  const out = [];
19
20
  for (const entry of entries) {
@@ -6787,7 +6788,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6787
6788
  }
6788
6789
 
6789
6790
  // ../shared/src/protocol/limits.ts
6790
- var PROTOCOL_VERSION = 149;
6791
+ var PROTOCOL_VERSION = 151;
6791
6792
  var PAGE_ERROR_MAX_CHARS = 2000;
6792
6793
  var PAGE_TITLE_MAX_CHARS = 1000;
6793
6794
  var FILE_MAX_CHARS = 5000000;
@@ -7216,6 +7217,8 @@ var clientMsgSchema = discriminatedUnion("t", [
7216
7217
  object({ t: literal("continue"), worktreeId: id }),
7217
7218
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
7218
7219
  object({ t: literal("reveal-path"), path: relPath }),
7220
+ object({ t: literal("copy-file"), worktreeId: id, path: relPath }),
7221
+ object({ t: literal("copy-file-path"), path: relPath }),
7219
7222
  object({ t: literal("reveal-attachment"), worktreeId: id, file: string2().min(1).max(120) }),
7220
7223
  object({ t: literal("reveal-transcript"), worktreeId: id }),
7221
7224
  object({ t: literal("stop-agent"), worktreeId: id }),
@@ -8844,7 +8847,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8844
8847
  // ../daemon/package.json
8845
8848
  var package_default = {
8846
8849
  name: "@toyon/daemon",
8847
- version: "0.24.0",
8850
+ version: "0.25.0",
8848
8851
  private: true,
8849
8852
  type: "module",
8850
8853
  main: "src/index.ts",
@@ -12766,10 +12769,18 @@ function envLevel() {
12766
12769
  }
12767
12770
  var threshold = LEVELS[envLevel()];
12768
12771
  var BUGS = [TypeError, RangeError, ReferenceError, SyntaxError];
12772
+ function details(e) {
12773
+ const d = e.data?.details;
12774
+ return typeof d === "string" && d ? `: ${d.split(`
12775
+ `)[0]}` : "";
12776
+ }
12769
12777
  function fmtError(level, e) {
12770
- const line = `${e.name}: ${e.message}`;
12778
+ const head = `${e.name}: ${e.message}`;
12779
+ const line = head + details(e);
12771
12780
  const whole = level === "error" || threshold <= LEVELS.debug || BUGS.some((c) => e instanceof c);
12772
- return whole ? e.stack ?? line : line;
12781
+ if (!whole || !e.stack)
12782
+ return line;
12783
+ return e.stack.startsWith(head) ? line + e.stack.slice(head.length) : e.stack;
12773
12784
  }
12774
12785
  function fmtExtra(level, extra) {
12775
12786
  if (extra === undefined)
@@ -13657,6 +13668,13 @@ function isAuthRequired(e) {
13657
13668
  return e instanceof RequestError && e.code === -32000;
13658
13669
  }
13659
13670
 
13671
+ // ../daemon/src/agent/acp/errors.ts
13672
+ function errorWords(e) {
13673
+ const details = e instanceof RequestError ? e.data?.details : undefined;
13674
+ const message = typeof details === "string" && details ? details : e instanceof Error ? e.message : String(e);
13675
+ return message.replace(/^Internal error: /, "");
13676
+ }
13677
+
13660
13678
  // ../daemon/src/agent/acp/options.ts
13661
13679
  var OPTION_FIELDS = { model: "model", thought_level: "effort" };
13662
13680
  var OPTION_CATEGORIES = Object.keys(OPTION_FIELDS);
@@ -13846,7 +13864,7 @@ class AgentReadiness {
13846
13864
  }
13847
13865
  if (init.agentCapabilities?.sessionCapabilities?.close) {
13848
13866
  await link.conn.agent.request(methods.agent.session.close, { sessionId: s.sessionId }).catch((e) => {
13849
- log.debug(tag, `session/close failed: ${e instanceof Error ? e.message : String(e)}`);
13867
+ log.debug(tag, `session/close failed: ${errorWords(e)}`);
13850
13868
  });
13851
13869
  }
13852
13870
  } catch (e) {
@@ -13913,16 +13931,15 @@ class AgentReadiness {
13913
13931
  } catch (e) {
13914
13932
  if (e instanceof UserError)
13915
13933
  throw e;
13916
- throw new UserError(`${spec.name} could not be signed out: ${e instanceof Error ? e.message : String(e)}`);
13934
+ throw new UserError(`${spec.name} could not be signed out: ${errorWords(e)}`);
13917
13935
  } finally {
13918
13936
  await link.kill();
13919
13937
  }
13920
13938
  }
13921
13939
  }
13922
13940
  function firstLine(e) {
13923
- const details = e instanceof RequestError ? e.data?.details : undefined;
13924
- const message = typeof details === "string" ? details : e instanceof Error ? e.message : String(e);
13925
- return message.replace(/^Internal error: /, "").split(`
13941
+ const message = errorWords(e);
13942
+ return message.split(`
13926
13943
  `)[0] ?? message;
13927
13944
  }
13928
13945
  function sameLimits(a, b) {
@@ -14929,7 +14946,7 @@ var BUILTIN_AGENTS = [
14929
14946
  run: {
14930
14947
  kind: "npm-bin",
14931
14948
  pkg: "@toyon-dev/claude-agent-acp",
14932
- version: "0.99.1-toyon.1",
14949
+ version: "0.106.0-toyon.1",
14933
14950
  forkOf: "@agentclientprotocol/claude-agent-acp",
14934
14951
  bin: "claude-agent-acp"
14935
14952
  },
@@ -15124,8 +15141,14 @@ class AgentRegistry {
15124
15141
  }
15125
15142
  if (!this.packageName(spec))
15126
15143
  return { reason: `no build for ${this.host.platform} ${this.host.arch}` };
15127
- if (this.installed(spec))
15128
- return { reason: boxed };
15144
+ if (this.installed(spec)) {
15145
+ const owed = boxed || this.installing.has(spec.id) ? undefined : this.installErrors.get(spec.id);
15146
+ const runs = owed ? this.installedVersion(spec) : null;
15147
+ if (!owed || !runs || runs === spec.run.version)
15148
+ return { reason: boxed };
15149
+ const behind = { runs, pinned: spec.run.version };
15150
+ return { reason: null, install: { failed: owed.why, ...owed.held ? { held: true } : {}, behind } };
15151
+ }
15129
15152
  if (this.installing.has(spec.id))
15130
15153
  return { reason: "installing" };
15131
15154
  const failed = this.installErrors.get(spec.id);
@@ -15697,6 +15720,13 @@ function backgroundStart(input, output) {
15697
15720
  const m = STARTED.exec(output);
15698
15721
  return m ? { taskId: m[1], file: m[2] } : null;
15699
15722
  }
15723
+ var STOP_TOOL2 = "TaskStop";
15724
+ function stoppedTask(name, input) {
15725
+ if (name !== STOP_TOOL2)
15726
+ return null;
15727
+ const id = input && typeof input === "object" ? input.task_id : undefined;
15728
+ return typeof id === "string" ? id : null;
15729
+ }
15700
15730
  function spawnDetached(output) {
15701
15731
  return output.includes("Async agent launched successfully");
15702
15732
  }
@@ -15721,7 +15751,7 @@ function taskEnd(text, toolId) {
15721
15751
  return { status, exit: exit === undefined ? null : Number(exit) };
15722
15752
  }
15723
15753
  var POLL_MS = 500;
15724
- var DEFAULT_BASH_TIMEOUT_MS = 120000;
15754
+ var BACKGROUND_LIMIT_MS = 2 * 60 * 60000;
15725
15755
  var GRACE_MS = 60000;
15726
15756
  var READ_CHUNK = 1 << 20;
15727
15757
  var CARRY = 4000;
@@ -15735,14 +15765,13 @@ class BackgroundTasks {
15735
15765
  get size() {
15736
15766
  return this.watched.size;
15737
15767
  }
15738
- start(toolId, start, input) {
15768
+ start(toolId, start) {
15739
15769
  const logPath = sessionLogFor(start.file, this.deps.configDir);
15740
15770
  if (!logPath)
15741
15771
  return false;
15742
- const timeout = input?.timeout;
15743
- const ceiling = (typeof timeout === "number" ? timeout : DEFAULT_BASH_TIMEOUT_MS) + (this.deps.graceMs ?? GRACE_MS);
15744
15772
  const w = {
15745
15773
  toolId,
15774
+ taskId: start.taskId,
15746
15775
  file: start.file,
15747
15776
  log: logPath,
15748
15777
  read: 0,
@@ -15751,13 +15780,20 @@ class BackgroundTasks {
15751
15780
  decoder: new TextDecoder,
15752
15781
  text: "",
15753
15782
  truncated: false,
15754
- ceiling: Date.now() + ceiling,
15783
+ ceiling: Date.now() + (this.deps.waitMs ?? BACKGROUND_LIMIT_MS + GRACE_MS),
15755
15784
  timer: null
15756
15785
  };
15757
15786
  this.watched.set(toolId, w);
15758
15787
  this.schedule(w);
15759
15788
  return true;
15760
15789
  }
15790
+ stop(taskId) {
15791
+ for (const w of [...this.watched.values()]) {
15792
+ if (w.taskId !== taskId)
15793
+ continue;
15794
+ fireAndForget("background", this.finish(w, null, "the agent stopped it", true), `ending ${w.toolId}`);
15795
+ }
15796
+ }
15761
15797
  endAll(reason) {
15762
15798
  for (const w of [...this.watched.values()]) {
15763
15799
  fireAndForget("background", this.finish(w, null, reason, true), `ending ${w.toolId}`);
@@ -16080,7 +16116,7 @@ function abandoned(memos, parent, opening = false) {
16080
16116
  }
16081
16117
  return out;
16082
16118
  }
16083
- function mapUpdate(update, memos, tag, sink, background, wakeup) {
16119
+ function mapUpdate(update, memos, tag, sink, background, wakeup, stopped) {
16084
16120
  switch (update.sessionUpdate) {
16085
16121
  case "agent_message_chunk": {
16086
16122
  const { parentToolId } = spawnOf(update._meta);
@@ -16127,7 +16163,7 @@ function mapUpdate(update, memos, tag, sink, background, wakeup) {
16127
16163
  ...spawn
16128
16164
  });
16129
16165
  if (update.status === "completed" || update.status === "failed")
16130
- out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup));
16166
+ out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup, stopped));
16131
16167
  return out;
16132
16168
  }
16133
16169
  case "tool_call_update": {
@@ -16184,7 +16220,7 @@ function mapUpdate(update, memos, tag, sink, background, wakeup) {
16184
16220
  if (update.rawOutput !== undefined)
16185
16221
  memo.rawOutput = update.rawOutput;
16186
16222
  if ((update.status === "completed" || update.status === "failed") && !memo.ended) {
16187
- out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup));
16223
+ out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup, stopped));
16188
16224
  }
16189
16225
  return out;
16190
16226
  }
@@ -16215,14 +16251,19 @@ function textOf(content, tag, what) {
16215
16251
  log.debug(tag, `acp: dropping non-text ${what} block (${content.type})`);
16216
16252
  return null;
16217
16253
  }
16218
- function endOf(toolId, memo, status, sink, background, wakeup) {
16254
+ function endOf(toolId, memo, status, sink, background, wakeup, stopped) {
16219
16255
  memo.ended = true;
16220
16256
  const output = summarizeToolOutput(memo.content, memo.rawOutput);
16221
16257
  if (status === "completed" && memo.kind === "execute" && background) {
16222
16258
  const start = backgroundStart(memo.input, output);
16223
- if (start && background(toolId, start, memo.input))
16259
+ if (start && background(toolId, start))
16224
16260
  return { type: "tool-update", toolId, background: true };
16225
16261
  }
16262
+ if (status === "completed" && stopped) {
16263
+ const taskId = stoppedTask(memo.name || memo.title, memo.input);
16264
+ if (taskId !== null)
16265
+ stopped(taskId);
16266
+ }
16226
16267
  if (status === "completed" && wakeup) {
16227
16268
  const name = memo.name || memo.title;
16228
16269
  const until = wakeupAt(name, memo.input);
@@ -16477,6 +16518,7 @@ var SYSTEM_APPEND = [
16477
16518
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists; `@<branch>` names another worktree of this project, and the context attached to the message says where its chat is on disk.",
16478
16519
  "Toyon knows every worktree by a title and a branch; its directory, `wt-xxxx`, is an id that outlives both. When you name this worktree or another in prose, use its title in quotes or its branch, never the directory: the context attached to each message gives this worktree's title and branch, and a sibling's when the message names it.",
16479
16520
  "To find which worktree of this project worked on a topic, a ticket or a feature, or to see what is open and what was archived, call the `worktrees` tool when you have one: it lists them by title and branch with where each chat is on disk, and with a `query` it keeps only those whose title, branch or chat mention it. `git worktree list` knows neither titles nor chats, so it cannot answer that.",
16521
+ "To stop the turn of a worktree made from a piece you handed off, when the user drops or pauses that work, call the `stop` tool when you have one, with the reason: it interrupts that turn and nothing else, and that chat says who stopped it and why. Stopping any other worktree, and archiving one, is the user's to do in Toyon; say so rather than looking for another way to reach its agent.",
16480
16522
  "To run a command this worktree still reaches on main here, call the `own` tool when you have one, before you edit what that command runs on; Toyon would switch it on the edit anyway, and calling first means the server is up when the edit lands.",
16481
16523
  `Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT", "api": {"cmd": "uv run uvicorn app.main:app --port $PORT", "setup": ["uv sync"], "remotes": {"stage": "https://api.stage.example.com"}}}, "check": "bun run check"}: setup runs once in every new worktree, teardown (optional) runs once when a worktree is removed, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, and check (optional) runs after each of your turns and must pass before the work can land. A run command may take the object form: cmd is the command; a command whose folder Toyon can place in the tree (or whose paths are listed) runs once in the main checkout and every worktree's page reaches it at its <NAME>_URL until the worktree changes those files, when it starts its own; "from": "copy" makes every worktree run its own and "from": "trunk" makes main run it whatever the paths say, with no change flipping a command that names no folder; its setup and teardown run in a worktree before it first runs that command there and when the worktree goes; remotes names addresses the page may point it at instead. The paragraph attached to each message says which commands this worktree reaches on main. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed. Both files may carry comments and trailing commas; leave any you find.`,
16482
16524
  "Toyon runs those commands itself in every worktree, and the user watches the result live in a preview beside this chat, so never start a dev server or open a browser of your own to check your work. Each message says where this worktree's preview answers; fetch a page from there when you want to see it.",
@@ -16641,16 +16683,22 @@ function handoffActedPrompt(pieces, outcomes, by = "plan", note) {
16641
16683
  const acted = parts.length ? `: ${parts.join("; ")}` : "";
16642
16684
  const siblings = started.length || queued.length || sent.length ? " The siblings read the list at its path in this worktree; never do their pieces yourself." : "";
16643
16685
  const said = note ? ` The person added: ${note}` : "";
16644
- return `The person answered the handoff card, and Toyon acted on the pieces you handed over${acted}. ${own}${said}${siblings}`;
16686
+ return `The person answered the handoff card, and Toyon acted on the pieces you handed over${acted}. ${own}${siblings}${said}`;
16645
16687
  }
16646
16688
  return `Toyon acted on the pieces you handed over with the plan's approval: ${parts.join("; ")}. ${own} The siblings read the plan at its path in this worktree; never do their pieces yourself.`;
16647
16689
  }
16648
16690
  function handoffDeclinedPrompt(pieces, note) {
16649
- const titles = pieces.map((p) => `"${p.title}"`).join(", ");
16650
- return `The person answered the handoff card with not now: nothing on the list you handed over (${titles}) was started, and none of it is yours to do from that list. They added: ${note}
16691
+ return `${handoffDeclinedContext(pieces)} They added: ${note}
16651
16692
 
16652
16693
  Read that as the word on what to change. Hand over a new list with the handoff tool if a split still fits, or go on without one.`;
16653
16694
  }
16695
+ function handoffDeclinedContext(pieces, failed) {
16696
+ const titles = pieces.map((p) => `"${p.title}"`).join(", ");
16697
+ if (failed === undefined) {
16698
+ return `The person answered the handoff card with not now: nothing on the list you handed over (${titles}) was started, and none of it is yours to do from that list.`;
16699
+ }
16700
+ return `Toyon could not finish acting on the list you handed over (${titles}): ${failed.replace(/\.$/, "")}. A piece may not have started; say so, and hand it over again if the split still fits.`;
16701
+ }
16654
16702
  function sentFromLine(path, by) {
16655
16703
  const who = by ? `, where ${by} was working` : "";
16656
16704
  return `This message was sent from a terminal in ${path} with \`toyon send\`${who}.`;
@@ -16675,29 +16723,30 @@ function pieceAskPrompt(text, project) {
16675
16723
  const where = project ? `in a new worktree of ${project.name} (${project.path}), another project open in Toyon. Propose it with the \`handoff\` tool: one piece, \`where\` set to \`worktree\` and \`project\` to "${project.path}", with a title (${NAME_SHAPE}) and a plan that project's agent builds from, with what is needed and why, how to reproduce it, and the paths here that depend on it.` : `in a new worktree of this project. Propose it with the \`handoff\` tool: one piece, \`where\` set to \`worktree\`, with a title (${NAME_SHAPE}) and a plan its agent builds from, with what is needed and why and the paths here that depend on it.`;
16676
16724
  return `The user wants part of this work done ${where}` + (own ? ` The user's own words for what it should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
16677
16725
  }
16678
- function pieceLandedContext(title, project, url, same, again, awaited) {
16679
- const where = url ? ` (${url})` : "";
16680
- if (awaited) {
16681
- return `The branch your own piece waited for, "${title}", has landed on this project's main${where}; that piece can go ahead now, and Toyon rebases onto main when this lands.`;
16682
- }
16683
- if (again) {
16684
- return `The branch this worktree waited for before it started, "${title}", landed again on this project's main${where} with follow-up commits; Toyon rebases onto main when this lands, and its latest commits are readable now if they touch your piece.`;
16685
- }
16686
- if (same) {
16687
- return `The sibling worktree "${title}", a piece of the work planned here, has landed on this project's main${where}; anything here that waited on it can take the real change now, and a later piece that waited for it has started.`;
16688
- }
16689
- return `The work this worktree handed off to ${project}, "${title}", has landed there${where}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
16726
+ var chatAt = (transcript) => `Its chat is at ${transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`;
16727
+ function pieceLandedContext(landed, chat) {
16728
+ const { title } = landed;
16729
+ const where = landed.url ? ` (${landed.url})` : "";
16730
+ const what = landed.subjects?.length ? ` What landed: ${landed.subjects.map((s) => `"${s}"`).join("; ")}.` : "";
16731
+ const read = chat ? ` ${chatAt(chat)} Read its agent's last replies there when what it checked or left undone matters here.` : "";
16732
+ const said = landed.awaited ? `The branch your own piece waited for, "${title}", has landed on this project's main${where}; that piece can go ahead now, and Toyon rebases onto main when this lands.` : landed.again ? `The branch this worktree waited for before it started, "${title}", landed again on this project's main${where} with follow-up commits; Toyon rebases onto main when this lands, and its latest commits are readable now if they touch your piece.` : landed.sent ? `The worktree this one sent a piece to, "${title}"${landed.same ? "" : ` in ${landed.repoName}`}, has landed on ${landed.same ? "this project's" : "that project's"} main${where}; the landing is that worktree's own, so check that it holds the piece before anything here relies on it.` : landed.same ? `The sibling worktree "${title}", a piece of the work planned here, has landed on this project's main${where}; anything here that waited on it can take the real change now, and a later piece that waited for it has started.` : `The work this worktree handed off to ${landed.repoName}, "${title}", has landed there${where}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
16733
+ return said + what + read;
16690
16734
  }
16691
16735
  function pieceWakePrompt(landed) {
16692
16736
  const quoted = (xs) => xs.map((x) => `"${x}"`).join(", ");
16693
16737
  const sentences = landed.map((l) => {
16694
16738
  const where = l.url ? ` (${l.url})` : "";
16695
16739
  const main = l.project ? `main in the project ${l.project}` : "this project's main";
16740
+ if (l.handed)
16741
+ return `The work this worktree handed off, "${l.title}" (${l.branch}), has landed on ${main}${where}.`;
16742
+ if (l.sent)
16743
+ return `The worktree this one sent a piece to, "${l.title}" (${l.branch}), has landed on ${main}${where}; check that the landing holds the piece.`;
16696
16744
  const own = l.pieces.length === 0 ? "your own piece" : `your own ${l.pieces.length === 1 ? "piece" : "pieces"} ${quoted(l.pieces)}`;
16697
16745
  return `The branch ${own} waited for, "${l.title}" (${l.branch}), has landed on ${main}${where}.`;
16698
16746
  });
16699
- const pieces = [...new Set(landed.flatMap((l) => l.pieces))];
16700
- const build = pieces.length === 0 ? "Do the piece that waited here now, and only that." : `Do ${quoted(pieces)} here now, and only ${pieces.length === 1 ? "that" : "those"}.`;
16747
+ const waited = landed.filter((l) => !l.handed && !l.sent);
16748
+ const pieces = [...new Set(waited.flatMap((l) => l.pieces))];
16749
+ const build = waited.length === 0 ? `Nothing here was set to wait for ${landed.length === 1 ? "it" : "them"}. If something here depends on what landed, such as a pinned version or a copy patched by hand, bring it in line now, and only that; otherwise change nothing. Then say in a line or two what landed and what you did with it.` : pieces.length === 0 ? "Do the piece that waited here now, and only that." : `Do ${quoted(pieces)} here now, and only ${pieces.length === 1 ? "that" : "those"}.`;
16701
16750
  const titles = quoted(landed.map((l) => l.title));
16702
16751
  const here = landed.some((l) => !l.project);
16703
16752
  const commits = `What ${titles} landed is readable on main now${here ? ", and Toyon rebases onto main when this lands" : ""}.`;
@@ -16718,7 +16767,7 @@ function archivedFate(a) {
16718
16767
  return "without landing, and git kept nothing of it";
16719
16768
  }
16720
16769
  function siblingParagraph(s) {
16721
- const chat = `Its chat is at ${s.transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`;
16770
+ const chat = chatAt(s.transcript);
16722
16771
  return s.archived ? `@${s.branch} was the worktree "${s.title}" in this project, archived on ${dayOf(s.archived.at)} ${archivedFate(s.archived)}. Its checkout at ${s.path} is gone. ${chat}` : `@${s.branch} is the worktree "${s.title}" in this project, checked out at ${s.path}. ${chat}`;
16723
16772
  }
16724
16773
  function worktreeContext(text, siblings) {
@@ -17074,16 +17123,22 @@ async function askRun(ctx, a, opened) {
17074
17123
  if (a.quick) {
17075
17124
  const model = readOptions(s.configOptions).get("model");
17076
17125
  const quick = model && quickModelFor(a.spec, model.ids, model.current);
17077
- if (model && quick) {
17078
- if (model.current !== quick) {
17126
+ let onQuick = !!quick && model?.current === quick;
17127
+ if (model && quick && !onQuick) {
17128
+ try {
17079
17129
  await ctx.request(methods.agent.session.setConfigOption, {
17080
17130
  sessionId: s.sessionId,
17081
17131
  configId: model.id,
17082
17132
  value: quick
17083
17133
  });
17134
+ onQuick = true;
17135
+ } catch (e) {
17136
+ log.warn(a.tag, `ask: ${a.spec.id} would not move to its quick model ${quick}`, e);
17084
17137
  }
17085
- } else if (a.quick === "require") {
17086
- log.debug(a.tag, `ask: ${a.spec.id} offers no quick model; not asked`);
17138
+ } else if (model && !quick)
17139
+ noteNotOffered(a, model.ids);
17140
+ if (!onQuick && a.quick === "require") {
17141
+ log.debug(a.tag, `ask: ${a.spec.id} has no quick model to run on; not asked`);
17087
17142
  return null;
17088
17143
  }
17089
17144
  }
@@ -17121,9 +17176,20 @@ ${a.prompt}`;
17121
17176
  fireAndForget(a.tag, cleanUp(ctx, a, s.sessionId), "ask: clean up side session");
17122
17177
  }
17123
17178
  }
17179
+ var notOffered = new Set;
17180
+ function noteNotOffered(a, offered) {
17181
+ const named = a.spec.quickModel;
17182
+ if (typeof named !== "string")
17183
+ return;
17184
+ const key = `${a.spec.id} ${named}`;
17185
+ if (notOffered.has(key))
17186
+ return;
17187
+ notOffered.add(key);
17188
+ log.warn(a.tag, `ask: ${a.spec.id} does not offer its quick model ${named}; it offers ${offered.join(", ")}`);
17189
+ }
17124
17190
  async function cleanUp(ctx, a, sessionId) {
17125
17191
  const quietly = (what) => (e) => {
17126
- log.debug(a.tag, `ask: session/${what} failed: ${e instanceof Error ? e.message : String(e)}`);
17192
+ log.debug(a.tag, `ask: session/${what} failed: ${errorWords(e)}`);
17127
17193
  };
17128
17194
  if (a.caps.close)
17129
17195
  await ctx.request(methods.agent.session.close, { sessionId }).catch(quietly("close"));
@@ -17300,7 +17366,7 @@ function steerOutcome(value) {
17300
17366
  // ../daemon/src/agent/acp/wire.ts
17301
17367
  var FLAG_NAMES = ["noStartSignal", "cutCallUnreported", "backgroundFinish", "modelIdOffList"];
17302
17368
  var CLAUDE = {
17303
- at: "0.99.1-toyon.1",
17369
+ at: "0.106.0-toyon.1",
17304
17370
  gaps: {
17305
17371
  partialInput: {
17306
17372
  gap: "A call's input arrives one top-level field at a time, as `tool_call_update`s carrying `rawInput` and " + "nothing else, and nothing marks a refine partial or the last one whole. The whole input of a read " + "or a plain command arrives the same way.",
@@ -17323,12 +17389,12 @@ var CLAUDE = {
17323
17389
  steerPriority: {
17324
17390
  gap: "The adapter picks the SDK priority of a steered message: `now`, which aborts the step the CLI is on " + "(since CLI 2.1.286 a foreground tool moves to the background instead), and `later` only while a " + "card is open, which waits for the generation after the tool result. Toyon cannot ask for `next`, " + "between steps, which is what a message sent mid-turn means.",
17325
17391
  here: 'session.ts asks for `priority: "next"` under `_meta.steering` on every steer, which the fork honours ' + "(upstream ignores the key and keeps `now`), and sends a card's follow-up (an image, a note, the " + "edited plan) before the card's reply resolves so it lands on `later`. No flag: the ask and the " + "ordering are harmless either way.",
17326
- upstream: "Asked upstream twice, agentclientprotocol/claude-agent-acp#1175 and #1350 (the better shape, " + "`_meta.steering.priority`), both unreviewed; still `now` at 0.99.1. The fork pin carries #1350 " + "(`pr/1350-steer-priority`) until one lands; back #1350 rather than open a third. The steering " + "extension itself says nothing about the running step.",
17392
+ upstream: "Asked upstream twice, agentclientprotocol/claude-agent-acp#1175 and #1350 (the better shape, " + "`_meta.steering.priority`), both unreviewed; still `now` at 0.106.0. The fork pin carries #1350 " + "(`pr/1350-steer-priority`) until one lands; back #1350 rather than open a third. The steering " + "extension itself says nothing about the running step.",
17327
17393
  open: false
17328
17394
  },
17329
17395
  backgroundFinish: {
17330
17396
  gap: "A command sent to the background returns at once with a task id and an output path, and the wire " + "never mentions it again.",
17331
- here: "background.ts tails the output file into the row and reads the finish off Claude Code's own session " + "log, on four strings toyon does not own. Flag: the sink mapUpdate keeps a background row open through.",
17397
+ here: "background.ts tails the output file into the row and reads the finish off Claude Code's own session " + "log, on four strings toyon does not own. A task the agent stops gets no notice there, and its row " + "ends on the TaskStop call naming it. Flag: the sink mapUpdate keeps a background row open through.",
17332
17398
  upstream: "Shipped: `async_task_spawned`, `async_task_progress`, `async_task_state_update` and " + "`_session/async_task/stop`, but only to a client declaring `_meta.jetbrains.air.capabilities` with " + "`asyncTasks` at initialize; no AIR-only key reaches any other client. Ask for a neutral capability, " + "or declare that meta.",
17333
17399
  open: true
17334
17400
  },
@@ -17363,9 +17429,9 @@ var CLAUDE = {
17363
17429
  open: true
17364
17430
  },
17365
17431
  authStatusMeta: {
17366
- gap: "Which credential the agent runs on arrives as `_auth/status_update`, advertised under " + "`agentCapabilities._meta.authStatus`, with no request method.",
17367
- here: "authstatus.ts. No flag.",
17368
- upstream: "Interim extension, on both builtin adapters; first-class fields when the RFD lands.",
17432
+ gap: "Which credential the agent runs on arrives as `_auth/status_update`, advertised under " + "`agentCapabilities._meta.authStatus`, with no request method. Since 0.104.0 the same identity is " + "also pushed as ACP's own `auth/status_update` and answered on `auth/status` (`auth.status`).",
17433
+ here: "authstatus.ts reads the private one. The daemon's SDK has no `auth/status_update`, so that push " + "meets no handler and is dropped. No flag.",
17434
+ upstream: "Interim extension, on both builtin adapters. Claude's first-class method has shipped; reading it " + "comes with an SDK that carries it.",
17369
17435
  open: true
17370
17436
  },
17371
17437
  steeringMeta: {
@@ -17381,7 +17447,7 @@ var CLAUDE = {
17381
17447
  open: true
17382
17448
  },
17383
17449
  droppedUpdates: {
17384
- gap: "Updates toyon gets and does not read: `plan` and `plan_update` (the task list), `compaction_update`, " + "`current_mode_update` beyond the plan check, `promptSuggestions` under `session_info_update._meta`, " + "and the title the adapter generates for the session. `notice` is withheld because toyon does not " + "declare `clientCapabilities.session.notices`.",
17450
+ gap: "Updates toyon gets and does not read: `plan` (the task list; `plan_update` and `plan_removed` go " + "to a client declaring `clientCapabilities.plan`), `compaction_update`, " + "`current_mode_update` beyond the plan check, `promptSuggestions` under `session_info_update._meta`, " + "and the title the adapter generates for the session. `notice` is withheld because toyon does not " + "declare `clientCapabilities.session.notices`.",
17385
17451
  here: "map.ts drops them at its default arm; session.ts reads the mode update for plan only. No flag.",
17386
17452
  upstream: "Shipped. Each is daemon work to read, not an ask.",
17387
17453
  open: true
@@ -17970,16 +18036,19 @@ class AcpSession {
17970
18036
  this.steered.splice(at, 1);
17971
18037
  return at >= 0;
17972
18038
  }
17973
- stop() {
18039
+ stop(by) {
17974
18040
  this.cancelAsks();
17975
18041
  if (!this.running) {
17976
18042
  if (this.own && this.live) {
17977
18043
  const { conn, sessionId } = this.live;
17978
18044
  fireAndForget(this.d.worktreeId, conn.ctx.notify(methods.agent.session.cancel, { sessionId }), "cancel");
17979
- this.endOwn("interrupted");
18045
+ this.endOwn("interrupted", by);
18046
+ return true;
17980
18047
  }
17981
- return;
18048
+ return false;
17982
18049
  }
18050
+ if (by && this.turn && !this.turn.stopping)
18051
+ this.turn.by = by;
17983
18052
  this.setStopping(true);
17984
18053
  if (this.steered.length > 0) {
17985
18054
  for (const item of this.steered)
@@ -17992,6 +18061,7 @@ class AcpSession {
17992
18061
  if (live) {
17993
18062
  fireAndForget(this.d.worktreeId, live.conn.ctx.notify(methods.agent.session.cancel, { sessionId: live.sessionId }), "cancel");
17994
18063
  }
18064
+ return true;
17995
18065
  }
17996
18066
  async close() {
17997
18067
  this.stopped = true;
@@ -18077,11 +18147,11 @@ class AcpSession {
18077
18147
  if (!turn?.open)
18078
18148
  return;
18079
18149
  turn.open = false;
18080
- this.turnOver(stopReason);
18150
+ this.turnOver(stopReason, stopReason === "interrupted" ? turn.by : undefined);
18081
18151
  this.setStopping(false);
18082
18152
  }
18083
- turnOver(stopReason) {
18084
- this.emit({ type: "turn-end", stopReason, ts: Date.now() });
18153
+ turnOver(stopReason, by) {
18154
+ this.emit({ type: "turn-end", stopReason, ...by ? { by } : {}, ts: Date.now() });
18085
18155
  this.showHeld();
18086
18156
  }
18087
18157
  async ask(system, prompt, opts = {}) {
@@ -18144,7 +18214,7 @@ class AcpSession {
18144
18214
  ...apiKey ? { _meta: { "api-key": { apiKey } } } : {}
18145
18215
  });
18146
18216
  } catch (e) {
18147
- throw new UserError(`${conn.spec.name} login failed: ${e instanceof Error ? e.message : String(e)}`);
18217
+ throw new UserError(`${conn.spec.name} login failed: ${errorWords(e)}`);
18148
18218
  }
18149
18219
  this.loggedIn();
18150
18220
  return { kind: "done" };
@@ -18191,12 +18261,12 @@ class AcpSession {
18191
18261
  rejectedCredential(e) {
18192
18262
  if (!this.conn?.authMethods.length)
18193
18263
  return false;
18194
- return REJECTED_CREDENTIAL_RE.test(e instanceof Error ? e.message : String(e));
18264
+ return REJECTED_CREDENTIAL_RE.test(errorWords(e));
18195
18265
  }
18196
18266
  describe(e) {
18197
18267
  if (isAuthRequired(e))
18198
18268
  return this.conn?.spec.loginHint ?? this.d.spec().loginHint;
18199
- const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
18269
+ const message = errorWords(e);
18200
18270
  return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
18201
18271
  }
18202
18272
  async record({ text, attachments, handed }) {
@@ -18261,10 +18331,10 @@ class AcpSession {
18261
18331
  this.d.preview?.(),
18262
18332
  this.d.tier?.(),
18263
18333
  this.d.pageErrors?.(),
18264
- ...this.landedSince()
18334
+ ...this.heardSince()
18265
18335
  ]);
18266
18336
  }
18267
- landedSince() {
18337
+ heardSince() {
18268
18338
  const entries = this.log.entries;
18269
18339
  let i = entries.length - 1;
18270
18340
  while (i >= 0 && !isMessage(entries[i].event))
@@ -18274,12 +18344,31 @@ class AcpSession {
18274
18344
  const e = entries[i].event;
18275
18345
  if (isMessage(e))
18276
18346
  break;
18277
- if (e.type === "piece-landed") {
18278
- out.unshift(pieceLandedContext(e.title, e.repoName, e.url, e.same, e.again, e.awaited));
18279
- }
18347
+ const said = e.type === "piece-landed" ? pieceLandedContext(e, this.d.chatOf?.(e.worktreeId)) : this.cardAnswered(e);
18348
+ if (said)
18349
+ out.unshift(said);
18280
18350
  }
18281
18351
  return out;
18282
18352
  }
18353
+ cardAnswered(e) {
18354
+ if (e.type !== "handoff" && e.type !== "handoff-declined")
18355
+ return;
18356
+ const list = this.proposed(e.id);
18357
+ if (!list || this.mcpTools === false)
18358
+ return;
18359
+ if (e.type === "handoff-declined" && e.outcome === "failed")
18360
+ return handoffDeclinedContext(list.pieces, e.reason);
18361
+ const withPlan = list.held && !this.log.entries.some(({ event }) => event.type === "handoff-shown" && event.id === e.id);
18362
+ if (withPlan)
18363
+ return;
18364
+ if (e.type === "handoff-declined") {
18365
+ return e.note || e.reason ? undefined : handoffDeclinedContext(list.pieces);
18366
+ }
18367
+ if (e.outcomes.some((o) => o.fate === "here" && !o.after))
18368
+ return;
18369
+ const go = this.log.entries.findLast(({ event }) => event.type === "handoff-go" && event.id === e.id)?.event;
18370
+ return handoffActedPrompt(list.pieces, e.outcomes, "card", go?.type === "handoff-go" ? go.note : undefined);
18371
+ }
18283
18372
  ensureConn(why) {
18284
18373
  if (this.conn)
18285
18374
  return Promise.resolve(this.conn);
@@ -18546,12 +18635,12 @@ class AcpSession {
18546
18635
  }
18547
18636
  }
18548
18637
  }
18549
- const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), wireFlags(live.conn.spec.id).backgroundFinish ? (toolId, start, input) => {
18550
- const taken = this.tasks.start(toolId, start, input);
18638
+ const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), wireFlags(live.conn.spec.id).backgroundFinish ? (toolId, start) => {
18639
+ const taken = this.tasks.start(toolId, start);
18551
18640
  if (taken)
18552
18641
  this.holdingChanged();
18553
18642
  return taken;
18554
- } : undefined, (toolId, until) => until === null ? this.wakeups.stop() : this.wakeups.start(toolId, until));
18643
+ } : undefined, (toolId, until) => until === null ? this.wakeups.stop() : this.wakeups.start(toolId, until), (taskId) => this.tasks.stop(taskId));
18555
18644
  this.forward(live, events);
18556
18645
  this.holdInput(live, params.update);
18557
18646
  }
@@ -18798,13 +18887,13 @@ class AcpSession {
18798
18887
  t.unref?.();
18799
18888
  this.own.settle = t;
18800
18889
  }
18801
- endOwn(stopReason) {
18890
+ endOwn(stopReason, by) {
18802
18891
  if (!this.own)
18803
18892
  return;
18804
18893
  if (this.own.settle)
18805
18894
  clearTimeout(this.own.settle);
18806
18895
  this.own = null;
18807
- this.turnOver(stopReason);
18896
+ this.turnOver(stopReason, by);
18808
18897
  this.syncStatus();
18809
18898
  this.maybeArmReaper();
18810
18899
  }
@@ -21128,30 +21217,57 @@ function startForward(opts) {
21128
21217
  var MAC_PRESSURE_CRITICAL = 4;
21129
21218
  var AVAILABLE_FLOOR = 0.15;
21130
21219
  var PAGING_FLOOR_BPS = 16 * 2 ** 20;
21220
+ var HARD_PAGING_BPS = 4 * PAGING_FLOOR_BPS;
21131
21221
  var paging = [];
21222
+ var readers = { darwin, linux };
21132
21223
  var unsupportedLogged = false;
21133
- async function memoryTight() {
21134
- const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
21135
- if (signal === null && !unsupportedLogged) {
21136
- unsupportedLogged = true;
21137
- log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
21224
+ var failures = 0;
21225
+ async function memoryTight(read = readers[process.platform]) {
21226
+ if (!read) {
21227
+ if (!unsupportedLogged) {
21228
+ unsupportedLogged = true;
21229
+ log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
21230
+ }
21231
+ return null;
21138
21232
  }
21139
- return signal;
21233
+ const reading = await read();
21234
+ if ("failed" in reading) {
21235
+ if (failures++ === 0) {
21236
+ log.warn("memory", `memory reading failed: ${reading.failed}; nothing sleeps for memory until it reads again`);
21237
+ }
21238
+ return null;
21239
+ }
21240
+ if (failures > 0) {
21241
+ log.info("memory", `memory reads again after ${failures} failed ${failures === 1 ? "check" : "checks"}`);
21242
+ failures = 0;
21243
+ }
21244
+ return reading;
21140
21245
  }
21246
+ var SYSCTL_NAMES = [
21247
+ "kern.memorystatus_vm_pressure_level",
21248
+ "kern.memorystatus_level",
21249
+ "vm.compressor.pages_compressed",
21250
+ "vm.compressor.pages_compressed_limit",
21251
+ "vm.compressor.segment.total",
21252
+ "vm.compressor.segment.limit"
21253
+ ];
21141
21254
  async function darwin() {
21142
- const [r, v] = await Promise.all([
21143
- run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/"),
21144
- run("vm_stat", [], "/")
21145
- ]);
21146
- if (!r.ok)
21147
- return null;
21148
- const [level, available] = r.out.split(/\s+/).map(Number);
21149
- if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
21150
- return null;
21255
+ const [r, v] = await Promise.all([run("sysctl", ["-n", ...SYSCTL_NAMES], "/"), run("vm_stat", [], "/")]);
21256
+ const read = parseSysctl(r.out);
21257
+ if (!read)
21258
+ return { failed: `sysctl: ${r.err || "nothing to read in what it printed"}` };
21151
21259
  const sample = v.ok ? parseVmStat(v.out, Date.now()) : null;
21152
21260
  if (sample)
21153
21261
  paging = [...paging, sample].slice(-3);
21154
- return darwinSignal(level, available, sustainedPaging(paging));
21262
+ return darwinSignal(read.level, read.available, sustainedPaging(paging), read.compressor);
21263
+ }
21264
+ function parseSysctl(text) {
21265
+ const [level, available, pages, pageLimit, segments, segmentLimit] = text.split(`
21266
+ `).map((line) => /^\d+$/.test(line.trim()) ? Number(line) : undefined);
21267
+ if (level === undefined || available === undefined)
21268
+ return null;
21269
+ const known = pages !== undefined && segments !== undefined && !!pageLimit && !!segmentLimit;
21270
+ return { level, available, compressor: known ? Math.max(pages / pageLimit, segments / segmentLimit) : null };
21155
21271
  }
21156
21272
  function parseVmStat(text, at) {
21157
21273
  const pageSize = text.match(/page size of (\d+) bytes/);
@@ -21175,20 +21291,24 @@ function sustainedPaging(samples) {
21175
21291
  }
21176
21292
  return Math.min(...rates);
21177
21293
  }
21178
- function darwinSignal(level, available, pagingBps = null) {
21294
+ function darwinSignal(level, available, pagingBps = null, compressor = null) {
21179
21295
  const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
21180
21296
  const swapping = pagingBps === null ? "" : `, swapping out ${Math.round(pagingBps / 2 ** 20)} MB/s`;
21297
+ const full = compressor === null ? "" : `, compressor ${Math.round(compressor * 100)}% full`;
21298
+ const share = available / 100;
21299
+ const room = compressor === null ? 1 : 1 - compressor;
21181
21300
  return {
21182
- tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100 || pagingBps !== null && pagingBps >= PAGING_FLOOR_BPS,
21183
- why: `memory pressure ${pressure} with ${available}% available${swapping}`
21301
+ tight: level === MAC_PRESSURE_CRITICAL || share < AVAILABLE_FLOOR || room < AVAILABLE_FLOOR || pagingBps !== null && pagingBps >= PAGING_FLOOR_BPS,
21302
+ hard: level === MAC_PRESSURE_CRITICAL || share <= AVAILABLE_FLOOR / 2 || room <= AVAILABLE_FLOOR / 2 || pagingBps !== null && pagingBps >= HARD_PAGING_BPS,
21303
+ why: `memory pressure ${pressure} with ${available}% available${swapping}${full}`
21184
21304
  };
21185
21305
  }
21186
21306
  async function linux() {
21187
21307
  let text;
21188
21308
  try {
21189
21309
  text = await Bun.file("/proc/meminfo").text();
21190
- } catch {
21191
- return null;
21310
+ } catch (e) {
21311
+ return { failed: `/proc/meminfo: ${e instanceof Error ? e.message : e}` };
21192
21312
  }
21193
21313
  const field = (name) => {
21194
21314
  const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
@@ -21197,9 +21317,13 @@ async function linux() {
21197
21317
  const total = field("MemTotal");
21198
21318
  const available = field("MemAvailable");
21199
21319
  if (!total || available === null)
21200
- return null;
21201
- const percent = Math.round(available / total * 100);
21202
- return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
21320
+ return { failed: "/proc/meminfo names no MemTotal or MemAvailable" };
21321
+ const share = available / total;
21322
+ return {
21323
+ tight: share < AVAILABLE_FLOOR,
21324
+ hard: share <= AVAILABLE_FLOOR / 2,
21325
+ why: `${Math.round(share * 100)}% of memory available`
21326
+ };
21203
21327
  }
21204
21328
  async function sampleCosts(pgids) {
21205
21329
  const costs = new Map;
@@ -22232,6 +22356,7 @@ function defaultAgent(wt, d, preview, onProcess) {
22232
22356
  archived: { at: a.archivedAt, landed: a.landed, restorable: a.restorable }
22233
22357
  }))
22234
22358
  ]),
22359
+ chatOf: (id) => d.chatFile?.(id),
22235
22360
  onProcess,
22236
22361
  spec,
22237
22362
  connect: (app, spec, prepared) => spawnAcp(app, d.agents.launch(spec, prepared), wt.path, wt.id),
@@ -22608,6 +22733,9 @@ class RuntimeRegistry {
22608
22733
  agentFor(id) {
22609
22734
  return this.runtimes.get(id)?.agent;
22610
22735
  }
22736
+ transcriptOf(id) {
22737
+ return (this.runtimes.get(id) ?? this.stopping.get(id))?.agent.transcript();
22738
+ }
22611
22739
  ensureAgent(wt) {
22612
22740
  const existing = this.runtimes.get(wt.id);
22613
22741
  if (existing)
@@ -23573,13 +23701,14 @@ function lacksQuickModel(spec, state) {
23573
23701
  const offered = state.cachedOptions(spec.id, "model");
23574
23702
  return offered.length > 0 && !offered.some((m) => m.id === quick);
23575
23703
  }
23704
+ var PLAN_READ_CHARS = 40000;
23576
23705
  var PIECES_SYSTEM = "You read plans and reply with only the requested JSON.";
23577
23706
  var piecesPrompt = (markdown) => [
23578
- "Below is a plan. List the parts of it the plan itself means to build apart from the rest: a step or section it marks as its own branch, a sibling worktree, a follow-up, or independent work. Also list the part it keeps in this worktree, if it says so.",
23579
- `Reply with ONLY a JSON array of 0 to 6 objects, each {"title": a name for the part, ${NAME_SHAPE}, "section": the exact heading text of that part, "plan": the plan for that part's agent in markdown, from the plan's own words: what to build, why, the files, and what done looks like, 150 to 500 words, "where": "worktree" or "here", "after": optional, "this" when the plan says the part waits for this branch to land}.`,
23580
- "Reply with an empty array when the plan is one piece of work with no part set apart.",
23707
+ "Below is a plan. List the parts of the work the plan itself means to be built apart from the rest: a step or section it marks as its own branch, a sibling worktree, a follow-up, or independent work, and work it puts off until after this one, to a later branch or a plan of its own, even where that is one sentence or one item of a list of what the plan leaves out. Something the plan only rules out, with no word that it is built later, is not a part. When you list any part, also list what the plan builds in this worktree, as one part.",
23708
+ `Reply with ONLY a JSON array of 0 to 6 objects, each {"title": a name for the part, ${NAME_SHAPE}, "section": the exact heading text of that part, or of the section that names it, "plan": the plan for that part's agent in markdown, from the plan's own words: what to build, why, the files, and what done looks like, 150 to 500 words, "where": "worktree" or "here", "after": optional, "this" when the plan says the part waits for this branch to land or comes after it}.`,
23709
+ "Reply with an empty array when the plan is one piece of work with no part set apart or put off.",
23581
23710
  `Plan:
23582
- ${markdown.slice(0, 12000)}`
23711
+ ${markdown.slice(0, PLAN_READ_CHARS)}`
23583
23712
  ].join(`
23584
23713
  `);
23585
23714
  function parsePieces(text) {
@@ -23839,6 +23968,7 @@ function sleepMsFrom(raw, inCloud) {
23839
23968
  }
23840
23969
  var DEFAULT_SLEEP_MS = sleepMsFrom(process.env.TOYON_PROC_SLEEP_MS, cloud.enabled);
23841
23970
  var PRESSURE_EVERY_MS = 15000;
23971
+ var HARD_PICKS = 3;
23842
23972
  var COSTS_EVERY_MS = 60000;
23843
23973
  var STAGGER_MS = 3000;
23844
23974
 
@@ -23849,6 +23979,10 @@ class IdlePolicy {
23849
23979
  queue = [];
23850
23980
  draining = false;
23851
23981
  drained = [];
23982
+ checking = false;
23983
+ freeing = null;
23984
+ eases = [];
23985
+ putDown = new Set;
23852
23986
  sleepMs;
23853
23987
  now;
23854
23988
  constructor(d) {
@@ -23978,14 +24112,44 @@ class IdlePolicy {
23978
24112
  return out;
23979
24113
  }
23980
24114
  async checkPressure() {
23981
- const signal = await (this.d.memory ?? memoryTight)();
23982
- if (!signal?.tight)
23983
- return;
23984
- const pick = this.d.state.worktrees.filter((w) => this.candidate(w.id) && this.awakeProcs(w.id)).sort((a, b) => this.entry(a.id).activeAt - this.entry(b.id).activeAt)[0];
23985
- if (!pick)
24115
+ if (this.checking)
23986
24116
  return;
23987
- log.info("idle", `${signal.why}; ${pick.id} is the least recently used`);
23988
- this.sleep(pick.id, "short of memory");
24117
+ this.checking = true;
24118
+ try {
24119
+ const signal = await (this.d.memory ?? memoryTight)();
24120
+ const picks = signal?.tight ? this.d.state.worktrees.filter((w) => this.candidate(w.id) && this.awakeProcs(w.id)).sort((a, b) => this.entry(a.id).activeAt - this.entry(b.id).activeAt).slice(0, signal.hard || this.eases.length > 0 ? HARD_PICKS : 1) : [];
24121
+ if (!signal?.tight)
24122
+ this.putDown.clear();
24123
+ if (signal && picks.some((p) => !this.putDown.has(p.id)))
24124
+ this.freeing = signal;
24125
+ else {
24126
+ this.freeing = null;
24127
+ this.letGo();
24128
+ }
24129
+ for (const pick of picks) {
24130
+ log.info("idle", `${signal?.why}; ${pick.id} is the least recently used`);
24131
+ this.putDown.add(pick.id);
24132
+ this.sleep(pick.id, "short of memory");
24133
+ }
24134
+ } catch (e) {
24135
+ this.freeing = null;
24136
+ this.letGo();
24137
+ throw e;
24138
+ } finally {
24139
+ this.checking = false;
24140
+ }
24141
+ }
24142
+ short() {
24143
+ return this.freeing?.why ?? null;
24144
+ }
24145
+ eased() {
24146
+ if (!this.freeing)
24147
+ return Promise.resolve();
24148
+ return new Promise((r) => this.eases.push(r));
24149
+ }
24150
+ letGo() {
24151
+ for (const go of this.eases.splice(0))
24152
+ go();
23989
24153
  }
23990
24154
  async sampleCosts() {
23991
24155
  this.recordViewed();
@@ -27132,7 +27296,7 @@ function looksBinary(text) {
27132
27296
  // ../daemon/src/files/service.ts
27133
27297
  import { spawn as spawn5 } from "child_process";
27134
27298
  import { lstat as lstat2, stat as stat3, unlink } from "fs/promises";
27135
- import { join as join24 } from "path";
27299
+ import { dirname as dirname10, join as join24 } from "path";
27136
27300
 
27137
27301
  // ../daemon/src/git/forge.ts
27138
27302
  function githubRepoUrl(remoteUrl) {
@@ -28021,7 +28185,21 @@ class FileService {
28021
28185
  child.on("error", () => {});
28022
28186
  child.unref();
28023
28187
  }
28188
+ async clip(worktreeId, path) {
28189
+ const target = resolveInside(this.require(worktreeId).path, path);
28190
+ if (!await stat3(target).catch(() => null))
28191
+ throw new UserError(`${path} is no longer there`);
28192
+ await this.clipPath(target);
28193
+ }
28194
+ async clipPath(target) {
28195
+ if (process.platform !== "darwin")
28196
+ throw new UserError("copying a file is only available on macOS");
28197
+ const r = await run("osascript", [...CLIP_FILE.flatMap((line) => ["-e", line]), target], dirname10(target));
28198
+ if (!r.ok)
28199
+ throw new UserError(`the file was not copied: ${r.err.trim() || `osascript exited ${r.exit}`}`);
28200
+ }
28024
28201
  }
28202
+ var CLIP_FILE = ["on run argv", "set the clipboard to (POSIX file (item 1 of argv))", "end run"];
28025
28203
 
28026
28204
  // ../daemon/src/files/open.ts
28027
28205
  var PENDING_MS = 30000;
@@ -28659,7 +28837,7 @@ class AfterLand {
28659
28837
  // ../daemon/src/repos/registry.ts
28660
28838
  import { existsSync as existsSync28, mkdirSync as mkdirSync8, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "fs";
28661
28839
  import { homedir as homedir10 } from "os";
28662
- import { basename as basename9, dirname as dirname10, join as join25 } from "path";
28840
+ import { basename as basename9, dirname as dirname11, join as join25 } from "path";
28663
28841
 
28664
28842
  // ../daemon/src/git/base.ts
28665
28843
  async function resolveBase(repoPath, defaultBranch, route) {
@@ -28930,7 +29108,7 @@ class RepoRegistry {
28930
29108
  await excludeFromGit(repo.path, rel);
28931
29109
  const file = join25(repo.path, rel);
28932
29110
  try {
28933
- mkdirSync8(dirname10(file), { recursive: true });
29111
+ mkdirSync8(dirname11(file), { recursive: true });
28934
29112
  writeFileSync11(file, configText(repo.path, rel, config));
28935
29113
  } catch (e) {
28936
29114
  log.warn(repoId, `could not write ${rel}`, e);
@@ -30481,6 +30659,7 @@ async function withSlot(key, max, fn) {
30481
30659
 
30482
30660
  // ../daemon/src/runs/service.ts
30483
30661
  var DETACHED_POLL_MS = 5000;
30662
+ var SHORT_OF_MEMORY = "short of memory";
30484
30663
 
30485
30664
  class RunService {
30486
30665
  d;
@@ -30491,9 +30670,10 @@ class RunService {
30491
30670
  }
30492
30671
  begin(worktreeId, kind, opts) {
30493
30672
  const key = keyOf2(worktreeId, kind);
30673
+ const memory = opts.memory ? this.d.memory : undefined;
30494
30674
  const run = {
30495
30675
  kind,
30496
- status: opts.queued || opts.slot ? "queued" : "running",
30676
+ status: opts.queued || opts.slot || memory?.short() ? "queued" : "running",
30497
30677
  since: Date.now(),
30498
30678
  timeoutMs: opts.timeoutMs,
30499
30679
  ...opts.stage ? { stage: opts.stage } : {}
@@ -30510,6 +30690,30 @@ class RunService {
30510
30690
  run.since = Date.now();
30511
30691
  this.write(worktreeId, run);
30512
30692
  };
30693
+ const stage = (text) => {
30694
+ if (!mine() || run.stage === text)
30695
+ return;
30696
+ if (text)
30697
+ run.stage = text;
30698
+ else
30699
+ delete run.stage;
30700
+ this.write(worktreeId, run);
30701
+ };
30702
+ const roomy = async () => {
30703
+ const why = memory?.short();
30704
+ if (!memory || !why)
30705
+ return mine();
30706
+ const was = run.stage;
30707
+ const since = Date.now();
30708
+ log.info(worktreeId, `${kind} waits for memory: ${why}`);
30709
+ stage(SHORT_OF_MEMORY);
30710
+ await memory.eased();
30711
+ if (!mine())
30712
+ return false;
30713
+ log.info(worktreeId, `${kind} starts after ${describeDuration(Date.now() - since)} waiting for memory`);
30714
+ stage(was);
30715
+ return true;
30716
+ };
30513
30717
  let admitted = Promise.resolve(true);
30514
30718
  if (opts.slot) {
30515
30719
  let admit = () => {};
@@ -30519,8 +30723,8 @@ class RunService {
30519
30723
  const held = new Promise((r) => {
30520
30724
  live.release = r;
30521
30725
  });
30522
- fireAndForget(worktreeId, withSlot(opts.slot.key, opts.slot.max, () => {
30523
- if (!mine()) {
30726
+ fireAndForget(worktreeId, withSlot(opts.slot.key, opts.slot.max, async () => {
30727
+ if (!mine() || !await roomy()) {
30524
30728
  admit(false);
30525
30729
  return;
30526
30730
  }
@@ -30528,6 +30732,12 @@ class RunService {
30528
30732
  admit(true);
30529
30733
  return held;
30530
30734
  }), `${kind} slot`);
30735
+ } else if (run.status === "queued" && !opts.queued) {
30736
+ admitted = roomy().then((ok) => {
30737
+ if (ok)
30738
+ start();
30739
+ return ok;
30740
+ });
30531
30741
  }
30532
30742
  return {
30533
30743
  admitted,
@@ -30539,15 +30749,7 @@ class RunService {
30539
30749
  run.pid = pgid;
30540
30750
  this.write(worktreeId, run);
30541
30751
  },
30542
- stage: (text) => {
30543
- if (!mine() || run.stage === text)
30544
- return;
30545
- if (text)
30546
- run.stage = text;
30547
- else
30548
- delete run.stage;
30549
- this.write(worktreeId, run);
30550
- },
30752
+ stage,
30551
30753
  finish: (exit) => {
30552
30754
  if (!mine())
30553
30755
  return;
@@ -31567,6 +31769,15 @@ function backfillOf(entries, since) {
31567
31769
  }
31568
31770
  var handlers = {
31569
31771
  async subscribe(msg, ctx, s) {
31772
+ const going = s.worktrees.archivingTranscript(msg.worktreeId);
31773
+ if (going) {
31774
+ if (!ctx.subscribe(msg.worktreeId))
31775
+ return;
31776
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, ...backfillOf(going, msg.since), log: [] });
31777
+ ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
31778
+ ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
31779
+ return;
31780
+ }
31570
31781
  const r = s.worktrees.readable(msg.worktreeId);
31571
31782
  if (!r) {
31572
31783
  const kept = s.worktrees.archivedTranscript(msg.worktreeId);
@@ -31837,7 +32048,7 @@ var handlers = {
31837
32048
  },
31838
32049
  async redraft(msg, _ctx, s) {
31839
32050
  const r = s.worktrees.readable(msg.worktreeId);
31840
- const events = r?.wt ? s.runtime.ensureAgent(r.wt).agent.transcript() : s.worktrees.archivedTranscript(msg.worktreeId) ?? [];
32051
+ const events = s.worktrees.archivingTranscript(msg.worktreeId) ?? (r?.wt ? s.runtime.ensureAgent(r.wt).agent.transcript() : s.worktrees.archivedTranscript(msg.worktreeId) ?? []);
31841
32052
  const event = events.find((e) => e.seq === msg.seq)?.event;
31842
32053
  if (event?.type !== "user-message")
31843
32054
  throw new UserError("that message is no longer in this chat");
@@ -31862,6 +32073,12 @@ var handlers = {
31862
32073
  async "reveal-path"(msg, _ctx, s) {
31863
32074
  s.files.revealPath(await s.opens.where(msg.path));
31864
32075
  },
32076
+ async "copy-file"(msg, _ctx, s) {
32077
+ await s.files.clip(msg.worktreeId, msg.path);
32078
+ },
32079
+ async "copy-file-path"(msg, _ctx, s) {
32080
+ await s.files.clipPath(await s.opens.where(msg.path));
32081
+ },
31865
32082
  async "reveal-attachment"(msg, _ctx, s) {
31866
32083
  const path = await s.attachments.locate(msg.worktreeId, msg.file, (file) => s.worktrees.archivedAttachment(msg.worktreeId, file));
31867
32084
  if (!path)
@@ -33271,7 +33488,7 @@ function startServer(opts) {
33271
33488
  // ../daemon/src/themes/store.ts
33272
33489
  import { existsSync as existsSync32, readdirSync as readdirSync8, readFileSync as readFileSync18, statSync as statSync11, writeFileSync as writeFileSync12 } from "fs";
33273
33490
  import { homedir as homedir13 } from "os";
33274
- import { basename as basename10, dirname as dirname11, join as join29, resolve as resolve11 } from "path";
33491
+ import { basename as basename10, dirname as dirname12, join as join29, resolve as resolve11 } from "path";
33275
33492
  var home = homedir13();
33276
33493
  var defaultExtensionDirs = [
33277
33494
  join29(home, ".vscode", "extensions"),
@@ -33377,7 +33594,7 @@ function readVscodeTheme(file, depth = 0) {
33377
33594
  throw new ThemeImportError(`${file}: not a JSON object`);
33378
33595
  const t = json;
33379
33596
  if (typeof t.include === "string" && depth < 5) {
33380
- const parent = readVscodeTheme(resolve11(dirname11(file), t.include), depth + 1);
33597
+ const parent = readVscodeTheme(resolve11(dirname12(file), t.include), depth + 1);
33381
33598
  return {
33382
33599
  ...parent,
33383
33600
  ...t,
@@ -33928,10 +34145,10 @@ import { join as join30 } from "path";
33928
34145
 
33929
34146
  // ../daemon/src/core/clone.ts
33930
34147
  import { mkdir as mkdir4, rm as rm4 } from "fs/promises";
33931
- import { dirname as dirname12 } from "path";
34148
+ import { dirname as dirname13 } from "path";
33932
34149
  async function cloneTree(src, dst) {
33933
34150
  const started = Date.now();
33934
- await mkdir4(dirname12(dst), { recursive: true });
34151
+ await mkdir4(dirname13(dst), { recursive: true });
33935
34152
  const attempts = [
33936
34153
  ["clonefile", ["-Rc", src, dst]],
33937
34154
  ["reflink", ["-R", "--reflink=auto", src, dst]],
@@ -33939,7 +34156,7 @@ async function cloneTree(src, dst) {
33939
34156
  ];
33940
34157
  let error = "";
33941
34158
  for (const [how, args] of attempts) {
33942
- const r = await run("cp", args, dirname12(dst));
34159
+ const r = await run("cp", args, dirname13(dst));
33943
34160
  if (r.ok)
33944
34161
  return { ok: true, how, ms: Date.now() - started };
33945
34162
  error = r.err.split(`
@@ -34521,6 +34738,19 @@ function handedOrigin(origin, project) {
34521
34738
  ...project ? { project } : {}
34522
34739
  };
34523
34740
  }
34741
+ function pieceLanded(landed, repo, to, mark, why = {}) {
34742
+ return {
34743
+ type: "piece-landed",
34744
+ worktreeId: landed.id,
34745
+ repoName: repo.name,
34746
+ title: landed.title,
34747
+ ...landed.pr?.url ? { url: landed.pr.url } : {},
34748
+ ...mark?.subjects.length ? { subjects: mark.subjects } : {},
34749
+ ...to.repoId === landed.repoId ? { same: true } : {},
34750
+ ...why,
34751
+ ts: Date.now()
34752
+ };
34753
+ }
34524
34754
  var PIECES_MAX = 6;
34525
34755
  var PLAN_MAX = 20000;
34526
34756
  var ATTACH_MAX = 10;
@@ -34606,6 +34836,7 @@ function parsePieces2(args) {
34606
34836
  }
34607
34837
  var HELD = "Handed to the person; the list shows beside your plan and they decide where each piece goes. Ask for plan approval now.";
34608
34838
  var SHOWN = "Handed to the person as a card; they decide. Finish your turn.";
34839
+ var SHOWN_AWAY = "Handed to the person as a card; they decide, and none of it is yours unless Toyon says they kept it here. Do not wait for their answer: go on with the work you already have that does not depend on it, and finish your turn when that is done.";
34609
34840
  var SHOWN_HERE = "Handed to the person as a card. This worktree holds no work yet, so the card leads with doing it here; they decide. Finish your turn.";
34610
34841
  function openPlanCard(entries) {
34611
34842
  const open = new Map;
@@ -34739,6 +34970,14 @@ var KIND_OF = {
34739
34970
  file: "file",
34740
34971
  element: "pick"
34741
34972
  };
34973
+ function wokeBy(landed, repo, to) {
34974
+ return {
34975
+ title: landed.title,
34976
+ branch: landed.branch,
34977
+ ...landed.pr?.url ? { url: landed.pr.url } : {},
34978
+ ...to.repoId === landed.repoId ? {} : { project: repo.name }
34979
+ };
34980
+ }
34742
34981
 
34743
34982
  class HandoffService {
34744
34983
  d;
@@ -34748,13 +34987,18 @@ class HandoffService {
34748
34987
  d.hub.on("handoffDecided", (worktreeId, id, approved) => fireAndForget(worktreeId, approved ? this.act(worktreeId, id, undefined, true) : this.decline(worktreeId, id), "handoff"));
34749
34988
  d.hub.on("planUnsplit", (worktreeId, markdown, planPath) => fireAndForget(worktreeId, this.read(worktreeId, markdown, planPath), "pieces read off the plan"));
34750
34989
  d.hub.on("treeChanged", (worktreeId) => fireAndForget(worktreeId, this.stale(worktreeId), "pieces stale"));
34751
- d.hub.on("landed", (worktreeId) => fireAndForget(worktreeId, this.landed(worktreeId), "pieces after landing"));
34990
+ d.hub.on("landed", (worktreeId, mark) => fireAndForget(worktreeId, this.landed(worktreeId, mark), "pieces after landing"));
34752
34991
  d.hub.on("worktreesChanged", () => this.sweep());
34753
34992
  this.sweep();
34754
34993
  }
34755
34994
  siblings(wt, repoId = wt.repoId) {
34756
34995
  return this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree" && w.id !== wt.id);
34757
34996
  }
34997
+ takesSend(w) {
34998
+ if (!w.landed)
34999
+ return true;
35000
+ return (this.d.counts(w.id)?.dirty ?? 0) > 0 || isBusy(this.d.runtime.agentFor(w.id)?.status);
35001
+ }
34758
35002
  otherProjects(wt) {
34759
35003
  return this.d.state.repos.filter((r) => r.id !== wt.repoId);
34760
35004
  }
@@ -34782,7 +35026,8 @@ class HandoffService {
34782
35026
  }
34783
35027
  tool(worktreeId) {
34784
35028
  const wt = this.d.state.worktree(worktreeId);
34785
- const names = wt ? this.siblings(wt).map((w) => `${w.branch}, "${w.title}"`) : [];
35029
+ const open = wt ? this.siblings(wt).filter((w) => this.takesSend(w)) : [];
35030
+ const names = open.map((w) => `${w.branch}, "${w.title}"`);
34786
35031
  const projects = wt ? this.otherProjects(wt).map((r) => r.name) : [];
34787
35032
  return handoffTool(names, projects);
34788
35033
  }
@@ -34881,7 +35126,8 @@ class HandoffService {
34881
35126
  const q = p.to?.trim() ?? "";
34882
35127
  if (!q)
34883
35128
  return refused(`Piece ${i + 1} ("${title}") is \`send\` and names no worktree in \`to\`.`);
34884
- const found = this.named(this.siblings(wt, piece.project?.id).filter((w) => !w.landed), q, piece.project?.name);
35129
+ const all = this.siblings(wt, piece.project?.id);
35130
+ const found = this.named(all.filter((w) => this.takesSend(w)), q, piece.project?.name, all.filter((w) => !this.takesSend(w)));
34885
35131
  if ("refused" in found)
34886
35132
  return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
34887
35133
  piece.to = found.worktree;
@@ -34924,7 +35170,8 @@ class HandoffService {
34924
35170
  }
34925
35171
  }
34926
35172
  const shown = cut.length ? `${sentenceCase(cut.join("; "))}. ` : "";
34927
- return { text: shown + (held ? HELD : here ? SHOWN_HERE : SHOWN) };
35173
+ const own = pieces.some((p) => p.fate === "here");
35174
+ return { text: shown + (held ? HELD : here ? SHOWN_HERE : own ? SHOWN : SHOWN_AWAY) };
34928
35175
  }
34929
35176
  offerHere(wt, pieces) {
34930
35177
  if (pieces.length !== 1)
@@ -34956,12 +35203,20 @@ class HandoffService {
34956
35203
  return found;
34957
35204
  return { after: found.worktree };
34958
35205
  }
34959
- named(among, q, where = "this project") {
35206
+ named(among, q, where = "this project", done = []) {
34960
35207
  const bare = q.startsWith("@") ? q.slice(1) : q;
34961
35208
  const list = among.map((w) => `${w.branch}, "${w.title}"`).join("; ");
35209
+ const keys = [(w) => w.branch, (w) => w.title];
35210
+ const found = matchBy(among, bare, keys);
35211
+ const landed = found.length === 0 ? matchBy(done, bare, keys) : [];
35212
+ if (landed.length > 0) {
35213
+ const names = landed.map((w) => `"${w.title}" (${w.branch})`).join(", ");
35214
+ return {
35215
+ refused: `${names} landed and ${landed.length === 1 ? "holds" : "hold"} no new work, so no piece is sent there; a \`worktree\` piece starts a new one.`
35216
+ };
35217
+ }
34962
35218
  if (among.length === 0)
34963
35219
  return { refused: `no other worktree of ${where} is open.` };
34964
- const found = matchBy(among, bare, [(w) => w.branch, (w) => w.title]);
34965
35220
  if (found.length === 1) {
34966
35221
  const w = found[0];
34967
35222
  return { worktree: { id: w.id, branch: w.branch, title: w.title } };
@@ -35223,6 +35478,7 @@ ${p.section}`;
35223
35478
  }
35224
35479
  });
35225
35480
  this.d.prompted(target.id);
35481
+ this.recordSender(target, wt.id);
35226
35482
  became.set(piece.id, { fate, worktree: { id: target.id, branch: target.branch, title: target.title } });
35227
35483
  return { pieceId: piece.id, fate, worktreeId: target.id, title: target.title, ...crossed };
35228
35484
  }
@@ -35287,6 +35543,12 @@ ${p.section}`;
35287
35543
  });
35288
35544
  });
35289
35545
  }
35546
+ recordSender(target, senderId) {
35547
+ if (target.senders?.includes(senderId))
35548
+ return;
35549
+ target.senders = [...target.senders ?? [], senderId];
35550
+ this.d.state.save();
35551
+ }
35290
35552
  recordAwaits(originId, awaits) {
35291
35553
  if (awaits.length === 0)
35292
35554
  return;
@@ -35371,11 +35633,12 @@ ${handedPrompt(null, note, at, via)}`
35371
35633
  }
35372
35634
  return out;
35373
35635
  }
35374
- async landed(landedId) {
35636
+ async landed(landedId, mark) {
35375
35637
  const told = new Set;
35376
- const origin = this.tellOrigin(landedId);
35638
+ const origin = this.tellOrigin(landedId, mark);
35377
35639
  if (origin)
35378
35640
  told.add(origin);
35641
+ const senders = this.tellSenders(landedId, told, mark);
35379
35642
  const madeNow = new Set;
35380
35643
  for (const origin of this.d.state.worktrees) {
35381
35644
  const due = origin.queued?.filter((q) => !("piece" in q.after) && q.after.id === landedId) ?? [];
@@ -35414,17 +35677,40 @@ ${handedPrompt(null, note, at, via)}`
35414
35677
  }
35415
35678
  }
35416
35679
  }
35417
- for (const id of this.tellWaiters(landedId, madeNow))
35680
+ for (const id of this.tellWaiters(landedId, madeNow, mark))
35418
35681
  told.add(id);
35419
- this.tellAwaiting(landedId, told);
35682
+ const awaited = this.tellAwaiting(landedId, told, mark);
35683
+ const woken = origin && !awaited.has(origin) && this.wakeHanded(origin, landedId) ? origin : null;
35684
+ for (const id of senders)
35685
+ if (id !== woken && !awaited.has(id))
35686
+ this.wakeHanded(id, landedId, true);
35687
+ }
35688
+ wakeHanded(toId, landedId, sent = false) {
35689
+ const to = this.d.state.worktree(toId);
35690
+ const landed = this.d.state.worktree(landedId);
35691
+ const repo = landed ? this.d.state.repo(landed.repoId) : undefined;
35692
+ if (to?.kind !== "worktree" || !landed || !repo)
35693
+ return false;
35694
+ if (to.awaits?.length || !this.takesSend(to))
35695
+ return false;
35696
+ if (!sent && (landed.lands?.length ?? 0) > 1)
35697
+ return false;
35698
+ this.wake(to, this.d.runtime.ensureAgent(to).agent, {
35699
+ ...wokeBy(landed, repo, to),
35700
+ pieces: [],
35701
+ ...sent ? { sent: true } : { handed: true }
35702
+ });
35703
+ return true;
35420
35704
  }
35421
- tellAwaiting(landedId, told) {
35705
+ tellAwaiting(landedId, told, mark) {
35422
35706
  const landed = this.d.state.worktree(landedId);
35423
35707
  const repo = landed ? this.d.state.repo(landed.repoId) : undefined;
35708
+ const awaited = new Set;
35424
35709
  let changed = false;
35425
35710
  for (const w of this.d.state.worktrees) {
35426
35711
  if (!w.awaits?.some((a) => a.id === landedId))
35427
35712
  continue;
35713
+ awaited.add(w.id);
35428
35714
  const pieces = w.awaits.filter((a) => a.id === landedId).flatMap((a) => a.pieces ?? []);
35429
35715
  w.awaits = w.awaits.filter((a) => a.id !== landedId);
35430
35716
  if (w.awaits.length === 0)
@@ -35433,32 +35719,16 @@ ${handedPrompt(null, note, at, via)}`
35433
35719
  if (!landed || !repo || w.kind !== "worktree")
35434
35720
  continue;
35435
35721
  const agent = this.d.runtime.ensureAgent(w).agent;
35436
- if (!told.has(w.id)) {
35437
- agent.note({
35438
- type: "piece-landed",
35439
- worktreeId: landed.id,
35440
- repoName: repo.name,
35441
- title: landed.title,
35442
- ...landed.pr?.url ? { url: landed.pr.url } : {},
35443
- ...w.repoId === landed.repoId ? { same: true } : {},
35444
- awaited: true,
35445
- ts: Date.now()
35446
- });
35447
- }
35448
- if (!w.landed) {
35449
- this.wake(w, agent, {
35450
- title: landed.title,
35451
- branch: landed.branch,
35452
- ...landed.pr?.url ? { url: landed.pr.url } : {},
35453
- ...w.repoId === landed.repoId ? {} : { project: repo.name },
35454
- pieces
35455
- });
35456
- }
35722
+ if (!told.has(w.id))
35723
+ agent.note(pieceLanded(landed, repo, w, mark, { awaited: true }));
35724
+ if (this.takesSend(w))
35725
+ this.wake(w, agent, { ...wokeBy(landed, repo, w), pieces });
35457
35726
  }
35458
35727
  if (changed) {
35459
35728
  this.d.state.save();
35460
35729
  this.d.hub.emit("worktreesChanged");
35461
35730
  }
35731
+ return awaited;
35462
35732
  }
35463
35733
  wake(w, agent, woke) {
35464
35734
  const pending = this.wakes.get(w.id);
@@ -35470,19 +35740,14 @@ ${handedPrompt(null, note, at, via)}`
35470
35740
  const landed = this.wakes.get(w.id) ?? [];
35471
35741
  this.wakes.delete(w.id);
35472
35742
  const live = this.d.state.worktree(w.id);
35473
- if (live?.kind !== "worktree" || live.landed)
35743
+ if (live?.kind !== "worktree" || !this.takesSend(live))
35474
35744
  return;
35475
35745
  if (agent.transcript().slice(mark).some((e) => isMessage(e.event)))
35476
35746
  return;
35477
35747
  const titles = landed.map((l) => `"${l.title}"`).join(", ");
35478
35748
  const pieces = new Set(landed.flatMap((l) => l.pieces));
35479
- agent.send(pieceWakePrompt(landed), {
35480
- asked: {
35481
- kind: "handoff",
35482
- why: `${titles} landed, so the agent starts the ${pieces.size === 1 ? "piece" : "pieces"} that waited`
35483
- },
35484
- keepCards: true
35485
- });
35749
+ const why = landed.every((l) => l.handed || l.sent) ? `${titles} landed, so the agent takes up what it handed off` : `${titles} landed, so the agent starts the ${pieces.size === 1 ? "piece" : "pieces"} that waited`;
35750
+ agent.send(pieceWakePrompt(landed), { asked: { kind: "handoff", why }, keepCards: true });
35486
35751
  }), "piece woken");
35487
35752
  }
35488
35753
  repoint(origin, made, worktree) {
@@ -35525,7 +35790,7 @@ ${handedPrompt(null, note, at, via)}`
35525
35790
  this.dropDependents(live, q);
35526
35791
  }
35527
35792
  }
35528
- tellOrigin(landedId) {
35793
+ tellOrigin(landedId, mark) {
35529
35794
  const wt = this.d.state.worktree(landedId);
35530
35795
  if (wt?.from?.kind !== "worktree")
35531
35796
  return null;
@@ -35535,18 +35800,10 @@ ${handedPrompt(null, note, at, via)}`
35535
35800
  const repo = this.d.state.repo(wt.repoId);
35536
35801
  if (!repo)
35537
35802
  return null;
35538
- this.d.runtime.ensureAgent(origin).agent.note({
35539
- type: "piece-landed",
35540
- worktreeId: wt.id,
35541
- repoName: repo.name,
35542
- title: wt.title,
35543
- ...wt.pr?.url ? { url: wt.pr.url } : {},
35544
- ...wt.repoId === origin.repoId ? { same: true } : {},
35545
- ts: Date.now()
35546
- });
35803
+ this.d.runtime.ensureAgent(origin).agent.note(pieceLanded(wt, repo, origin, mark));
35547
35804
  return origin.id;
35548
35805
  }
35549
- tellWaiters(landedId, madeNow) {
35806
+ tellWaiters(landedId, madeNow, mark) {
35550
35807
  const landed = this.d.state.worktree(landedId);
35551
35808
  if (!landed)
35552
35809
  return [];
@@ -35560,19 +35817,33 @@ ${handedPrompt(null, note, at, via)}`
35560
35817
  if (w.from?.kind !== "worktree" || w.from.after?.id !== landedId)
35561
35818
  continue;
35562
35819
  told.push(w.id);
35563
- this.d.runtime.ensureAgent(w).agent.note({
35564
- type: "piece-landed",
35565
- worktreeId: landed.id,
35566
- repoName: repo.name,
35567
- title: landed.title,
35568
- ...landed.pr?.url ? { url: landed.pr.url } : {},
35569
- ...w.repoId === landed.repoId ? { same: true } : {},
35570
- again: true,
35571
- ts: Date.now()
35572
- });
35820
+ this.d.runtime.ensureAgent(w).agent.note(pieceLanded(landed, repo, w, mark, { again: true }));
35573
35821
  }
35574
35822
  return told;
35575
35823
  }
35824
+ tellSenders(landedId, told, mark) {
35825
+ const wt = this.d.state.worktree(landedId);
35826
+ if (!wt?.senders)
35827
+ return [];
35828
+ const senders = wt.senders;
35829
+ delete wt.senders;
35830
+ this.d.state.save();
35831
+ const repo = this.d.state.repo(wt.repoId);
35832
+ if (!repo)
35833
+ return [];
35834
+ const open = [];
35835
+ for (const id of senders) {
35836
+ const sender = this.d.state.worktree(id);
35837
+ if (sender?.kind !== "worktree")
35838
+ continue;
35839
+ open.push(id);
35840
+ if (told.has(id) || sender.awaits?.some((a) => a.id === landedId))
35841
+ continue;
35842
+ told.add(id);
35843
+ this.d.runtime.ensureAgent(sender).agent.note(pieceLanded(wt, repo, sender, mark, { sent: true }));
35844
+ }
35845
+ return open;
35846
+ }
35576
35847
  sweep() {
35577
35848
  let changed = false;
35578
35849
  for (const origin of this.d.state.worktrees) {
@@ -35859,7 +36130,8 @@ ${verdict.body}` : verdict.subject;
35859
36130
  this.d.runs.drop(worktreeId, "check");
35860
36131
  const run = this.d.runs.begin(worktreeId, "check", {
35861
36132
  timeoutMs: ceiling,
35862
- slot: { key: repo.path, max: CHECK_SLOTS }
36133
+ slot: { key: repo.path, max: CHECK_SLOTS },
36134
+ memory: true
35863
36135
  });
35864
36136
  if (!await run.admitted)
35865
36137
  return;
@@ -35939,7 +36211,7 @@ ${verdict.body}` : verdict.subject;
35939
36211
  this.judging.delete(worktreeId);
35940
36212
  if (verdict)
35941
36213
  this.recapTurn(worktreeId, verdict);
35942
- log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms${waited > 100 ? ` (waited ${waited}ms for a check slot)` : ""}`);
36214
+ log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms${waited > 100 ? ` (waited ${waited}ms to start)` : ""}`);
35943
36215
  this.d.worktrees.setLanding(worktreeId, landing);
35944
36216
  if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
35945
36217
  this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
@@ -36145,7 +36417,7 @@ function clip2(text) {
36145
36417
 
36146
36418
  // ../daemon/src/worktrees/outside.ts
36147
36419
  import { stat as stat6 } from "fs/promises";
36148
- import { basename as basename12, dirname as dirname13, resolve as resolve12 } from "path";
36420
+ import { basename as basename12, dirname as dirname14, resolve as resolve12 } from "path";
36149
36421
 
36150
36422
  // ../daemon/src/worktrees/discover.ts
36151
36423
  import { createHash as createHash6 } from "crypto";
@@ -36318,7 +36590,7 @@ class OutsideSend {
36318
36590
  const gitDir = common.ok && common.out ? canonical(common.out) : null;
36319
36591
  if (!gitDir || basename12(gitDir) !== ".git")
36320
36592
  throw new UserError(`${raw} keeps its repository apart from its checkout`);
36321
- return { path, top, mainPath: dirname13(gitDir) };
36593
+ return { path, top, mainPath: dirname14(gitDir) };
36322
36594
  }
36323
36595
  async start(repoId, text, context, carry) {
36324
36596
  const repo = this.d.state.requireRepo(repoId);
@@ -36843,7 +37115,7 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
36843
37115
  // ../daemon/src/worktrees/service.ts
36844
37116
  import { cpSync, existsSync as existsSync38, readFileSync as readFileSync22, rmSync as rmSync9 } from "fs";
36845
37117
  import { rm as rm6 } from "fs/promises";
36846
- import { dirname as dirname15, join as join36 } from "path";
37118
+ import { dirname as dirname16, join as join36 } from "path";
36847
37119
 
36848
37120
  // ../daemon/src/git/archive.ts
36849
37121
  import { rmSync as rmSync6 } from "fs";
@@ -37091,7 +37363,7 @@ async function dropTurnRefs(repoPath, worktreeId) {
37091
37363
 
37092
37364
  // ../daemon/src/worktrees/archive.ts
37093
37365
  import { existsSync as existsSync35, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync21, renameSync as renameSync7, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
37094
- import { basename as basename13, dirname as dirname14, join as join33 } from "path";
37366
+ import { basename as basename13, dirname as dirname15, join as join33 } from "path";
37095
37367
  class WorktreeArchive {
37096
37368
  dir;
37097
37369
  records = new Map;
@@ -37235,7 +37507,7 @@ function firstPrompt(transcript) {
37235
37507
  function move(from, to) {
37236
37508
  if (!existsSync35(from))
37237
37509
  return;
37238
- mkdirSync9(dirname14(to), { recursive: true });
37510
+ mkdirSync9(dirname15(to), { recursive: true });
37239
37511
  rmSync8(to, { recursive: true, force: true });
37240
37512
  renameSync7(from, to);
37241
37513
  }
@@ -37886,9 +38158,11 @@ class Ship {
37886
38158
  if (f)
37887
38159
  this.setLanded(wt, landedNow(wt, f));
37888
38160
  }
37889
- async settleLanded(wt) {
38161
+ async settleLanded(wt, noted) {
37890
38162
  const repo = this.d.state.repo(wt.repoId);
37891
38163
  const f = wt.kind === "main" || !repo ? null : await this.landedFacts(wt);
38164
+ if (noted)
38165
+ wt.lands = [...wt.lands ?? [], noted];
37892
38166
  delete wt.landing;
37893
38167
  let held;
37894
38168
  if (f && repo) {
@@ -37987,7 +38261,11 @@ ${wt.landing.body}` : s;
37987
38261
  const where = cwd === wt.path ? "git" : `git -C ${cwd}`;
37988
38262
  const commit = args[0] === "commit";
37989
38263
  const ceiling = commit ? timeoutFor(repo.config, "commit") : STEP_TIMEOUT_MS;
37990
- const handle = commit ? this.d.runs.begin(wt.id, "commit", { timeoutMs: ceiling }) : null;
38264
+ const handle = commit ? this.d.runs.begin(wt.id, "commit", { timeoutMs: ceiling, memory: true }) : null;
38265
+ if (handle && !await handle.admitted) {
38266
+ const err = "a newer commit took its place";
38267
+ return { ok: false, out: "", err, exit: err, text: "" };
38268
+ }
37991
38269
  const run = (onText, signal) => stepRun(cwd, args, onText, signal, {
37992
38270
  timeoutMs: ceiling,
37993
38271
  ...handle ? { onSpawn: handle.spawned } : {},
@@ -38087,7 +38365,7 @@ ${wt.landing.body}` : s;
38087
38365
  ts: Date.now()
38088
38366
  });
38089
38367
  }
38090
- this.d.hub.emit("landed", wt.id);
38368
+ this.d.hub.emit("landed", wt.id, o.mark);
38091
38369
  }
38092
38370
  async followBranch(wt, current) {
38093
38371
  if (wt.kind === "main")
@@ -38322,13 +38600,13 @@ ${wt.landing.body}` : s;
38322
38600
  return { result: { ok: true, message: `${did}; ${here}` }, landed: true, ...settled };
38323
38601
  }
38324
38602
  async settle(wt, repo, mark, opts = {}) {
38325
- const noted = await this.noteLand(repo, wt, mark, opts);
38603
+ const noted = await this.landMark(repo, wt, mark, opts);
38326
38604
  if (!opts.stay)
38327
38605
  await this.restartFromMain(wt, baseOf(repo));
38328
38606
  this.d.tree.invalidateAll();
38329
- return { mark: noted, held: await this.settleLanded(wt) };
38607
+ return { mark: noted, held: await this.settleLanded(wt, noted) };
38330
38608
  }
38331
- async noteLand(repo, wt, mark, opts = {}) {
38609
+ async landMark(repo, wt, mark, opts = {}) {
38332
38610
  if (!mark)
38333
38611
  return;
38334
38612
  const n = wt.lands?.length ?? 0;
@@ -38337,15 +38615,12 @@ ${wt.landing.body}` : s;
38337
38615
  log.warn(wt.id, `could not keep the commits it landed: ${pinned.err}`);
38338
38616
  return;
38339
38617
  }
38340
- const noted = {
38618
+ return {
38341
38619
  ...mark,
38342
38620
  at: Date.now(),
38343
38621
  ...opts.pr ? { pr: opts.pr.number } : {},
38344
38622
  subjects: await this.landedWhat(repo, wt, mark, opts)
38345
38623
  };
38346
- wt.lands = [...wt.lands ?? [], noted];
38347
- this.d.state.save();
38348
- return noted;
38349
38624
  }
38350
38625
  async landedWhat(repo, wt, mark, opts) {
38351
38626
  if (opts.pr?.title)
@@ -40269,6 +40544,11 @@ class WorktreeService {
40269
40544
  archivingNow(worktreeId) {
40270
40545
  return this.archiving.get(worktreeId);
40271
40546
  }
40547
+ archivingTranscript(worktreeId) {
40548
+ if (!this.archiving.has(worktreeId))
40549
+ return null;
40550
+ return this.d.runtime.transcriptOf(worktreeId) ?? new Transcript(this.chatFiles(worktreeId).transcript, worktreeId).entries;
40551
+ }
40272
40552
  async discardWorktree(worktreeId) {
40273
40553
  const wt = this.d.state.worktree(worktreeId);
40274
40554
  if (!wt || !(canArchive(wt) || wt.kind === "spare"))
@@ -40305,6 +40585,8 @@ class WorktreeService {
40305
40585
  rmSync9(this.plansScratch(wt.id), { recursive: true, force: true });
40306
40586
  throw e;
40307
40587
  }
40588
+ if (this.d.watches)
40589
+ fireAndForget(worktreeId, this.d.watches.drop(wt.path), "watchman watch drop");
40308
40590
  if (hasOwnBranch(wt)) {
40309
40591
  const r = await git(repo.path, "branch", "-D", wt.branch);
40310
40592
  if (!r.ok)
@@ -40386,7 +40668,7 @@ class WorktreeService {
40386
40668
  throw new Error(`git worktree remove --force ${wt.path} failed (${r.exit}): ${r.err}`);
40387
40669
  if (await deleteTree(wt.path))
40388
40670
  return;
40389
- await run("chmod", ["-R", "u+w", wt.path], dirname15(wt.path));
40671
+ await run("chmod", ["-R", "u+w", wt.path], dirname16(wt.path));
40390
40672
  if (!await deleteTree(wt.path))
40391
40673
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
40392
40674
  }
@@ -40574,6 +40856,9 @@ class WorktreeService {
40574
40856
  return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
40575
40857
  }
40576
40858
  async restore(archiveId, createdBy, message) {
40859
+ const going = this.archivingNow(archiveId);
40860
+ if (going)
40861
+ await going.catch(() => {});
40577
40862
  const running = this.restoring.get(archiveId);
40578
40863
  const live = running ? await running : this.d.state.worktree(archiveId);
40579
40864
  if (live) {
@@ -40603,7 +40888,7 @@ class WorktreeService {
40603
40888
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
40604
40889
  if (rec.auto)
40605
40890
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
40606
- const path = existsSync38(old.path) ? join36(dirname15(old.path), freeSlot(dirname15(old.path)).dir) : old.path;
40891
+ const path = existsSync38(old.path) ? join36(dirname16(old.path), freeSlot(dirname16(old.path)).dir) : old.path;
40607
40892
  const wt = {
40608
40893
  ...old,
40609
40894
  repoId: repo.id,
@@ -41184,9 +41469,13 @@ class WorktreeService {
41184
41469
  const path = transcriptPathFor(this.d.paths.transcriptsDir, worktreeId);
41185
41470
  return existsSync38(path) ? path : undefined;
41186
41471
  }
41187
- transcriptPath(id) {
41472
+ chatFile(id) {
41188
41473
  const path = this.d.state.worktree(id) ? this.writtenTranscript(id) : this.archive.chatFiles(id)?.transcript;
41189
- if (!path || !existsSync38(path))
41474
+ return path && existsSync38(path) ? path : undefined;
41475
+ }
41476
+ transcriptPath(id) {
41477
+ const path = this.chatFile(id);
41478
+ if (!path)
41190
41479
  throw new UserError("nothing has been written to this chat yet");
41191
41480
  return path;
41192
41481
  }
@@ -41330,6 +41619,100 @@ class WorktreeService {
41330
41619
  }
41331
41620
  }
41332
41621
 
41622
+ // ../daemon/src/worktrees/stop.ts
41623
+ var REASON_MAX = 300;
41624
+ var SHAPE = "stop takes { worktree: string, reason: string }.";
41625
+ var refused4 = (text) => ({ text, isError: true });
41626
+
41627
+ class StopTool {
41628
+ d;
41629
+ constructor(d) {
41630
+ this.d = d;
41631
+ }
41632
+ tool(worktreeId) {
41633
+ const wt = this.d.state.worktree(worktreeId);
41634
+ const made = wt ? this.made(wt) : [];
41635
+ const running = made.filter((w) => this.running(w));
41636
+ const now = made.length === 0 ? "Right now no open worktree was made from a piece of this chat's work." : running.length === 0 ? "Right now none of the worktrees made from this chat's pieces is running a turn." : `Right now you can stop: ${running.map((w) => this.named(wt, w)).join("; ")}.`;
41637
+ return {
41638
+ name: STOP_TOOL,
41639
+ description: `Stop the turn a worktree made from a piece you handed off is running, when its work should not go on: the person said to drop or pause it, or what it builds on changed. It is the stop the person presses and nothing more: the worktree, its files and its chat stay, that chat says this one stopped it and why, and the person can send it on from there. It does not archive or remove the worktree, which is the person's to do in Toyon, and it reaches only worktrees made from this chat's pieces, never one a piece was sent into. ${now}`,
41640
+ inputSchema: {
41641
+ type: "object",
41642
+ properties: {
41643
+ worktree: { type: "string", description: "The worktree to stop, by its branch or its title." },
41644
+ reason: {
41645
+ type: "string",
41646
+ description: `Why, in a sentence of at most ${REASON_MAX} characters: the person reads it on that chat's stopped row.`
41647
+ }
41648
+ },
41649
+ required: ["worktree", "reason"],
41650
+ additionalProperties: false
41651
+ }
41652
+ };
41653
+ }
41654
+ call(worktreeId, args) {
41655
+ const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
41656
+ if (!a || typeof a.worktree !== "string" || typeof a.reason !== "string")
41657
+ return refused4(SHAPE);
41658
+ const wt = this.d.state.worktree(worktreeId);
41659
+ if (!wt)
41660
+ return refused4("unknown worktree");
41661
+ const reason = a.reason.trim();
41662
+ if (!reason)
41663
+ return refused4("Say why in `reason`: the person reads it on that chat's stopped row.");
41664
+ if (reason.length > REASON_MAX) {
41665
+ return refused4(`The reason is over ${REASON_MAX} characters; say it in a sentence.`);
41666
+ }
41667
+ const q = a.worktree.trim();
41668
+ const bare = q.startsWith("@") ? q.slice(1) : q;
41669
+ if (!bare)
41670
+ return refused4("Name the worktree to stop, by its branch or its title.");
41671
+ const made = this.made(wt);
41672
+ const keys = [(w) => w.branch, (w) => w.title];
41673
+ const found = matchBy(made, bare, keys);
41674
+ if (found.length > 1) {
41675
+ return refused4(`"${q}" names more than one worktree: ${found.map((w) => w.branch).join(", ")}. Say which by its branch.`);
41676
+ }
41677
+ const target = found[0];
41678
+ if (!target) {
41679
+ if (bare === "this" || matchBy([wt], bare, keys).length > 0) {
41680
+ return refused4("That is this worktree; end your own turn by finishing it.");
41681
+ }
41682
+ const other = matchBy(this.d.state.worktrees.filter((w) => w.id !== wt.id && w.kind === "worktree" && !made.includes(w)), bare, keys)[0];
41683
+ if (other) {
41684
+ return refused4(`"${other.title}" (${other.branch}) was not made from a piece of this chat's work, so it is the person's to stop. Tell them, and name it.`);
41685
+ }
41686
+ return refused4(made.length === 0 ? "No open worktree was made from a piece of this chat's work, so there is nothing here to stop." : `No worktree made from this chat's pieces is called "${q}". Those open: ${made.map((w) => this.named(wt, w)).join("; ")}.`);
41687
+ }
41688
+ const name = `"${target.title}" (${target.branch})`;
41689
+ const agent = this.d.agentFor(target.id);
41690
+ if (!agent || !this.running(target))
41691
+ return refused4(`${name} is not running a turn; there is nothing to stop.`);
41692
+ if (agent.stopping)
41693
+ return refused4(`${name} is stopping already.`);
41694
+ const waiting = agent.queueLength;
41695
+ const project = target.repoId === wt.repoId ? undefined : this.d.state.repo(wt.repoId)?.name;
41696
+ if (!agent.stop({ origin: handedOrigin(wt, project), reason })) {
41697
+ return refused4(`${name} is not running a turn; there is nothing to stop.`);
41698
+ }
41699
+ const next = waiting > 0 ? ` ${waiting === 1 ? "A message" : `${waiting} messages`} already waiting in that chat ${waiting === 1 ? "goes" : "go"} next, as after any stop.` : "";
41700
+ return {
41701
+ text: `${name} is stopping its turn. Its chat says this one stopped it and why; its worktree and files stay as they are, and the person can send it on or archive it in Toyon.${next}`
41702
+ };
41703
+ }
41704
+ made(wt) {
41705
+ return this.d.state.worktrees.filter((w) => w.kind === "worktree" && w.id !== wt.id && w.from?.kind === "worktree" && w.from.origin.id === wt.id);
41706
+ }
41707
+ running(w) {
41708
+ return isBusy(this.d.agentFor(w.id)?.status);
41709
+ }
41710
+ named(wt, w) {
41711
+ const repo = wt && w.repoId !== wt.repoId ? this.d.state.repo(w.repoId) : undefined;
41712
+ return `${w.branch}, "${w.title}"${repo ? ` in ${repo.name}` : ""}`;
41713
+ }
41714
+ }
41715
+
41333
41716
  // ../daemon/src/worktrees/sweep.ts
41334
41717
  var ARCHIVE_AFTER_MS = 2 * 60 * 60000;
41335
41718
  var SWEEP_EVERY_MS = 10 * 60000;
@@ -41415,11 +41798,65 @@ class ArchiveSweep {
41415
41798
  }
41416
41799
  }
41417
41800
 
41801
+ // ../daemon/src/worktrees/watchman.ts
41802
+ import { existsSync as existsSync39 } from "fs";
41803
+ var NO_SERVER = ["--no-spawn", "--no-local"];
41804
+ var TIMEOUT_MS2 = 5000;
41805
+
41806
+ class WatchmanWatches {
41807
+ d;
41808
+ bin;
41809
+ constructor(d) {
41810
+ this.d = d;
41811
+ }
41812
+ async drop(path) {
41813
+ if (existsSync39(path))
41814
+ return;
41815
+ const gone = canonical(path);
41816
+ if (!within2(gone, canonical(this.d.root)))
41817
+ return;
41818
+ await this.del((root) => within2(root, gone));
41819
+ }
41820
+ async sweep() {
41821
+ await this.del((root) => !existsSync39(root));
41822
+ }
41823
+ async del(dead) {
41824
+ const home = canonical(this.d.root);
41825
+ let dropped = 0;
41826
+ for (const root of await this.roots()) {
41827
+ if (root === home || !within2(root, home) || !dead(root))
41828
+ continue;
41829
+ if ((await this.ask("watch-del", root))?.ok)
41830
+ dropped++;
41831
+ }
41832
+ if (dropped > 0)
41833
+ log.info("watchman", `dropped ${dropped} ${dropped === 1 ? "watch" : "watches"} whose worktree is gone`);
41834
+ }
41835
+ async roots() {
41836
+ const r = await this.ask("watch-list");
41837
+ if (!r?.ok)
41838
+ return [];
41839
+ try {
41840
+ const roots = JSON.parse(r.out).roots;
41841
+ return Array.isArray(roots) ? roots.filter((p) => typeof p === "string") : [];
41842
+ } catch {
41843
+ return [];
41844
+ }
41845
+ }
41846
+ async ask(...command) {
41847
+ if (this.bin === undefined)
41848
+ this.bin = (this.d.which ?? Bun.which)("watchman");
41849
+ if (!this.bin)
41850
+ return null;
41851
+ return (this.d.run ?? run)(this.bin, [...NO_SERVER, ...command], this.d.root, undefined, { timeoutMs: TIMEOUT_MS2 });
41852
+ }
41853
+ }
41854
+
41418
41855
  // ../daemon/src/index.ts
41419
41856
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
41420
41857
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
41421
41858
  log.info("daemon", `toyon ${package_default.version} on bun ${Bun.version}, pid ${process.pid}, ${process.argv[1]}`);
41422
- var here = dirname16(fileURLToPath(import.meta.url));
41859
+ var here = dirname17(fileURLToPath(import.meta.url));
41423
41860
  var {
41424
41861
  shellDist: SHELL_DIST,
41425
41862
  bridgeJs: BRIDGE_JS,
@@ -41513,6 +41950,7 @@ var runtime = new RuntimeRegistry({
41513
41950
  pageErrors: (id) => pageErrors.ambient(id),
41514
41951
  turnStarting: (id) => worktrees.turnStarted(id),
41515
41952
  archived: (repoId) => worktrees.archived(repoId),
41953
+ chatFile: (id) => worktrees.chatFile(id),
41516
41954
  standing: (id) => worktrees.standing(id),
41517
41955
  tier: (id) => worktrees.tierStanding(id),
41518
41956
  remote: () => setting.get(),
@@ -41528,9 +41966,14 @@ var drafts = new DraftStore({
41528
41966
  hub,
41529
41967
  uploads
41530
41968
  });
41531
- var runs = new RunService({ state, hub });
41969
+ var runs = new RunService({
41970
+ state,
41971
+ hub,
41972
+ memory: { short: () => idle.short(), eased: () => idle.eased() }
41973
+ });
41532
41974
  var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
41533
41975
  var cache = new ArtifactCache({ paths, state, hub });
41976
+ var watches = new WatchmanWatches({ root: paths.worktreesDir });
41534
41977
  var fix = new FixService({ state, hub, runtime });
41535
41978
  var interest = new Interest;
41536
41979
  var worktrees = new WorktreeService({
@@ -41547,7 +41990,8 @@ var worktrees = new WorktreeService({
41547
41990
  pageErrors: (id) => pageErrors.ambient(id),
41548
41991
  interested: (id) => interest.isSubscribed(id),
41549
41992
  shown: (id) => idle.isShown(id),
41550
- message: (wt) => landing.message(wt.id)
41993
+ message: (wt) => landing.message(wt.id),
41994
+ watches
41551
41995
  });
41552
41996
  interest.onFirst((id) => worktrees.tree.watch(id));
41553
41997
  interest.onLast((id) => worktrees.tree.unwatch(id));
@@ -41571,6 +42015,7 @@ var handoff2 = new HandoffService({
41571
42015
  tools.add(handoff2);
41572
42016
  tools.add(new WorktreeLookup({ state, transcriptsDir: paths.transcriptsDir, archived: (repoId) => worktrees.archived(repoId) }));
41573
42017
  tools.add(new OwnTool({ state, own: (id, names) => worktrees.ownProcs(id, names, { by: "agent" }) }));
42018
+ tools.add(new StopTool({ state, agentFor: (id) => runtime.agentFor(id) }));
41574
42019
  var turns = new TurnService({
41575
42020
  state,
41576
42021
  hub,
@@ -41829,6 +42274,7 @@ idle.boot();
41829
42274
  runs.boot();
41830
42275
  landing.boot();
41831
42276
  fireAndForget("cache", cache.sweep(), "cache sweep");
42277
+ fireAndForget("watchman", watches.sweep(), "watchman sweep");
41832
42278
  fireAndForget("agents", agents.installMissing().then(() => readiness2.checkMissing()), "agent adapter install");
41833
42279
  var repoArg = process.argv[2];
41834
42280
  if (repoArg) {