@xrkseek/harness 0.5.9 → 0.5.11

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 (228) hide show
  1. package/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts +4 -3
  2. package/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts.map +1 -1
  3. package/node_modules/@xrkseek/core-agent-loop/dist/index.js +25 -60
  4. package/node_modules/@xrkseek/core-agent-loop/dist/index.js.map +1 -1
  5. package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.d.ts +0 -2
  6. package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.d.ts.map +1 -1
  7. package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.js +7 -3
  8. package/node_modules/@xrkseek/core-agent-loop/dist/llm-retry.js.map +1 -1
  9. package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.d.ts +16 -3
  10. package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.d.ts.map +1 -1
  11. package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.js +24 -8
  12. package/node_modules/@xrkseek/core-agent-loop/dist/settle-batch.js.map +1 -1
  13. package/node_modules/@xrkseek/core-session/dist/sqlite-store.d.ts.map +1 -1
  14. package/node_modules/@xrkseek/core-session/dist/sqlite-store.js +27 -1
  15. package/node_modules/@xrkseek/core-session/dist/sqlite-store.js.map +1 -1
  16. package/node_modules/@xrkseek/core-tools/dist/index.d.ts +1 -1
  17. package/node_modules/@xrkseek/core-tools/dist/index.d.ts.map +1 -1
  18. package/node_modules/@xrkseek/core-tools/dist/index.js +1 -1
  19. package/node_modules/@xrkseek/core-tools/dist/index.js.map +1 -1
  20. package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.d.ts +9 -0
  21. package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.d.ts.map +1 -1
  22. package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.js +45 -12
  23. package/node_modules/@xrkseek/core-tools/dist/subagent-prompt.js.map +1 -1
  24. package/node_modules/@xrkseek/exec-fs/dist/index.d.ts +9 -1
  25. package/node_modules/@xrkseek/exec-fs/dist/index.d.ts.map +1 -1
  26. package/node_modules/@xrkseek/exec-fs/dist/index.js +21 -14
  27. package/node_modules/@xrkseek/exec-fs/dist/index.js.map +1 -1
  28. package/node_modules/@xrkseek/exec-fs/dist/paths.d.ts +11 -0
  29. package/node_modules/@xrkseek/exec-fs/dist/paths.d.ts.map +1 -1
  30. package/node_modules/@xrkseek/exec-fs/dist/paths.js +40 -0
  31. package/node_modules/@xrkseek/exec-fs/dist/paths.js.map +1 -1
  32. package/node_modules/@xrkseek/exec-memory/dist/index.d.ts +2 -2
  33. package/node_modules/@xrkseek/exec-memory/dist/index.d.ts.map +1 -1
  34. package/node_modules/@xrkseek/exec-memory/dist/index.js +2 -2
  35. package/node_modules/@xrkseek/exec-memory/dist/index.js.map +1 -1
  36. package/node_modules/@xrkseek/exec-memory/dist/store.d.ts +18 -0
  37. package/node_modules/@xrkseek/exec-memory/dist/store.d.ts.map +1 -1
  38. package/node_modules/@xrkseek/exec-memory/dist/store.js +108 -7
  39. package/node_modules/@xrkseek/exec-memory/dist/store.js.map +1 -1
  40. package/node_modules/@xrkseek/exec-memory/dist/tools.d.ts.map +1 -1
  41. package/node_modules/@xrkseek/exec-memory/dist/tools.js +17 -1
  42. package/node_modules/@xrkseek/exec-memory/dist/tools.js.map +1 -1
  43. package/node_modules/@xrkseek/exec-memory/dist/write-path.d.ts +2 -0
  44. package/node_modules/@xrkseek/exec-memory/dist/write-path.d.ts.map +1 -1
  45. package/node_modules/@xrkseek/exec-memory/dist/write-path.js +28 -3
  46. package/node_modules/@xrkseek/exec-memory/dist/write-path.js.map +1 -1
  47. package/node_modules/@xrkseek/exec-sandbox/dist/resolve.d.ts +7 -4
  48. package/node_modules/@xrkseek/exec-sandbox/dist/resolve.d.ts.map +1 -1
  49. package/node_modules/@xrkseek/exec-sandbox/dist/resolve.js +4 -0
  50. package/node_modules/@xrkseek/exec-sandbox/dist/resolve.js.map +1 -1
  51. package/node_modules/@xrkseek/exec-sandbox/dist/service.d.ts +7 -0
  52. package/node_modules/@xrkseek/exec-sandbox/dist/service.d.ts.map +1 -1
  53. package/node_modules/@xrkseek/exec-sandbox/dist/service.js +18 -8
  54. package/node_modules/@xrkseek/exec-sandbox/dist/service.js.map +1 -1
  55. package/node_modules/@xrkseek/exec-web/dist/tools.d.ts +1 -1
  56. package/node_modules/@xrkseek/exec-web/dist/tools.d.ts.map +1 -1
  57. package/node_modules/@xrkseek/exec-web/dist/tools.js +10 -0
  58. package/node_modules/@xrkseek/exec-web/dist/tools.js.map +1 -1
  59. package/node_modules/@xrkseek/llm/dist/index.d.ts.map +1 -1
  60. package/node_modules/@xrkseek/llm/dist/index.js +8 -2
  61. package/node_modules/@xrkseek/llm/dist/index.js.map +1 -1
  62. package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts +16 -0
  63. package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts.map +1 -1
  64. package/node_modules/@xrkseek/preset-harness/dist/preset.js +13 -2
  65. package/node_modules/@xrkseek/preset-harness/dist/preset.js.map +1 -1
  66. package/node_modules/@xrkseek/preset-harness/dist/tsconfig.tsbuildinfo +1 -1
  67. package/node_modules/@xrkseek/preset-minimal/dist/tsconfig.tsbuildinfo +1 -1
  68. package/node_modules/@xrkseek/preset-server/dist/preset.d.ts.map +1 -1
  69. package/node_modules/@xrkseek/preset-server/dist/preset.js +4 -1
  70. package/node_modules/@xrkseek/preset-server/dist/preset.js.map +1 -1
  71. package/node_modules/@xrkseek/preset-server/dist/tsconfig.tsbuildinfo +1 -1
  72. package/node_modules/@xrkseek/protocol/dist/session-events.d.ts +5 -0
  73. package/node_modules/@xrkseek/protocol/dist/session-events.d.ts.map +1 -1
  74. package/node_modules/@xrkseek/protocol/dist/session-events.js.map +1 -1
  75. package/node_modules/@xrkseek/protocol/dist/validate.d.ts.map +1 -1
  76. package/node_modules/@xrkseek/protocol/dist/validate.js +5 -0
  77. package/node_modules/@xrkseek/protocol/dist/validate.js.map +1 -1
  78. package/node_modules/@xrkseek/server-face/dist/adapt/index.d.ts +1 -1
  79. package/node_modules/@xrkseek/server-face/dist/adapt/index.d.ts.map +1 -1
  80. package/node_modules/@xrkseek/server-face/dist/adapt/index.js +1 -1
  81. package/node_modules/@xrkseek/server-face/dist/adapt/index.js.map +1 -1
  82. package/node_modules/@xrkseek/server-face/dist/adapt/model-route.d.ts +5 -0
  83. package/node_modules/@xrkseek/server-face/dist/adapt/model-route.d.ts.map +1 -1
  84. package/node_modules/@xrkseek/server-face/dist/adapt/model-route.js +16 -0
  85. package/node_modules/@xrkseek/server-face/dist/adapt/model-route.js.map +1 -1
  86. package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.d.ts +32 -0
  87. package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.d.ts.map +1 -1
  88. package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.js +87 -0
  89. package/node_modules/@xrkseek/server-face/dist/adapt/subagent-notice.js.map +1 -1
  90. package/node_modules/@xrkseek/server-face/dist/adapt/wire-event.d.ts.map +1 -1
  91. package/node_modules/@xrkseek/server-face/dist/adapt/wire-event.js +29 -2
  92. package/node_modules/@xrkseek/server-face/dist/adapt/wire-event.js.map +1 -1
  93. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts +102 -0
  94. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts.map +1 -0
  95. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js +495 -0
  96. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js.map +1 -0
  97. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.d.ts +12 -0
  98. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.d.ts.map +1 -0
  99. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.js +141 -0
  100. package/node_modules/@xrkseek/server-face/dist/agent-roster-tools.js.map +1 -0
  101. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.d.ts +46 -0
  102. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.d.ts.map +1 -1
  103. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.js +150 -1
  104. package/node_modules/@xrkseek/server-face/dist/agent-team-roles.js.map +1 -1
  105. package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.d.ts +5 -0
  106. package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.d.ts.map +1 -1
  107. package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.js +24 -0
  108. package/node_modules/@xrkseek/server-face/dist/agent-team-tasks.js.map +1 -1
  109. package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.d.ts +6 -0
  110. package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.d.ts.map +1 -0
  111. package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.js +43 -0
  112. package/node_modules/@xrkseek/server-face/dist/capture-member-playbook.js.map +1 -0
  113. package/node_modules/@xrkseek/server-face/dist/context.d.ts +21 -4
  114. package/node_modules/@xrkseek/server-face/dist/context.d.ts.map +1 -1
  115. package/node_modules/@xrkseek/server-face/dist/dispatch.d.ts.map +1 -1
  116. package/node_modules/@xrkseek/server-face/dist/dispatch.js +19 -0
  117. package/node_modules/@xrkseek/server-face/dist/dispatch.js.map +1 -1
  118. package/node_modules/@xrkseek/server-face/dist/fork-cut.d.ts +3 -2
  119. package/node_modules/@xrkseek/server-face/dist/fork-cut.d.ts.map +1 -1
  120. package/node_modules/@xrkseek/server-face/dist/fork-cut.js +58 -20
  121. package/node_modules/@xrkseek/server-face/dist/fork-cut.js.map +1 -1
  122. package/node_modules/@xrkseek/server-face/dist/handlers/job.d.ts.map +1 -1
  123. package/node_modules/@xrkseek/server-face/dist/handlers/job.js +1 -0
  124. package/node_modules/@xrkseek/server-face/dist/handlers/job.js.map +1 -1
  125. package/node_modules/@xrkseek/server-face/dist/handlers/session.d.ts.map +1 -1
  126. package/node_modules/@xrkseek/server-face/dist/handlers/session.js +88 -18
  127. package/node_modules/@xrkseek/server-face/dist/handlers/session.js.map +1 -1
  128. package/node_modules/@xrkseek/server-face/dist/handlers/subagent.d.ts.map +1 -1
  129. package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js +20 -0
  130. package/node_modules/@xrkseek/server-face/dist/handlers/subagent.js.map +1 -1
  131. package/node_modules/@xrkseek/server-face/dist/handlers/team.d.ts +11 -0
  132. package/node_modules/@xrkseek/server-face/dist/handlers/team.d.ts.map +1 -0
  133. package/node_modules/@xrkseek/server-face/dist/handlers/team.js +268 -0
  134. package/node_modules/@xrkseek/server-face/dist/handlers/team.js.map +1 -0
  135. package/node_modules/@xrkseek/server-face/dist/index.d.ts +6 -2
  136. package/node_modules/@xrkseek/server-face/dist/index.d.ts.map +1 -1
  137. package/node_modules/@xrkseek/server-face/dist/index.js +5 -1
  138. package/node_modules/@xrkseek/server-face/dist/index.js.map +1 -1
  139. package/node_modules/@xrkseek/server-face/dist/model-catalog.d.ts +17 -1
  140. package/node_modules/@xrkseek/server-face/dist/model-catalog.d.ts.map +1 -1
  141. package/node_modules/@xrkseek/server-face/dist/model-catalog.js +46 -1
  142. package/node_modules/@xrkseek/server-face/dist/model-catalog.js.map +1 -1
  143. package/node_modules/@xrkseek/server-face/dist/presence-tools.d.ts.map +1 -1
  144. package/node_modules/@xrkseek/server-face/dist/presence-tools.js +15 -6
  145. package/node_modules/@xrkseek/server-face/dist/presence-tools.js.map +1 -1
  146. package/node_modules/@xrkseek/server-face/dist/presets-catalog.d.ts +16 -0
  147. package/node_modules/@xrkseek/server-face/dist/presets-catalog.d.ts.map +1 -1
  148. package/node_modules/@xrkseek/server-face/dist/presets-catalog.js +5 -0
  149. package/node_modules/@xrkseek/server-face/dist/presets-catalog.js.map +1 -1
  150. package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.d.ts +8 -2
  151. package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.d.ts.map +1 -1
  152. package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.js +14 -12
  153. package/node_modules/@xrkseek/server-face/dist/projections/units/turn-outline.js.map +1 -1
  154. package/node_modules/@xrkseek/server-face/dist/ralph-tool.d.ts.map +1 -1
  155. package/node_modules/@xrkseek/server-face/dist/ralph-tool.js +15 -12
  156. package/node_modules/@xrkseek/server-face/dist/ralph-tool.js.map +1 -1
  157. package/node_modules/@xrkseek/server-face/dist/runtime.d.ts.map +1 -1
  158. package/node_modules/@xrkseek/server-face/dist/runtime.js +96 -14
  159. package/node_modules/@xrkseek/server-face/dist/runtime.js.map +1 -1
  160. package/node_modules/@xrkseek/server-face/dist/session-cwd.d.ts +12 -0
  161. package/node_modules/@xrkseek/server-face/dist/session-cwd.d.ts.map +1 -1
  162. package/node_modules/@xrkseek/server-face/dist/session-cwd.js +14 -0
  163. package/node_modules/@xrkseek/server-face/dist/session-cwd.js.map +1 -1
  164. package/node_modules/@xrkseek/server-face/dist/session-model-store.d.ts +2 -0
  165. package/node_modules/@xrkseek/server-face/dist/session-model-store.d.ts.map +1 -1
  166. package/node_modules/@xrkseek/server-face/dist/session-model-store.js +14 -0
  167. package/node_modules/@xrkseek/server-face/dist/session-model-store.js.map +1 -1
  168. package/node_modules/@xrkseek/server-face/dist/session-status.d.ts +16 -1
  169. package/node_modules/@xrkseek/server-face/dist/session-status.d.ts.map +1 -1
  170. package/node_modules/@xrkseek/server-face/dist/session-status.js +47 -6
  171. package/node_modules/@xrkseek/server-face/dist/session-status.js.map +1 -1
  172. package/node_modules/@xrkseek/server-face/dist/session-thread-publish.d.ts +17 -0
  173. package/node_modules/@xrkseek/server-face/dist/session-thread-publish.d.ts.map +1 -0
  174. package/node_modules/@xrkseek/server-face/dist/session-thread-publish.js +34 -0
  175. package/node_modules/@xrkseek/server-face/dist/session-thread-publish.js.map +1 -0
  176. package/node_modules/@xrkseek/server-face/dist/session-thread-store.d.ts +38 -0
  177. package/node_modules/@xrkseek/server-face/dist/session-thread-store.d.ts.map +1 -0
  178. package/node_modules/@xrkseek/server-face/dist/session-thread-store.js +207 -0
  179. package/node_modules/@xrkseek/server-face/dist/session-thread-store.js.map +1 -0
  180. package/node_modules/@xrkseek/server-face/dist/session-thread-tools.d.ts +12 -0
  181. package/node_modules/@xrkseek/server-face/dist/session-thread-tools.d.ts.map +1 -0
  182. package/node_modules/@xrkseek/server-face/dist/session-thread-tools.js +223 -0
  183. package/node_modules/@xrkseek/server-face/dist/session-thread-tools.js.map +1 -0
  184. package/node_modules/@xrkseek/server-face/dist/settings-agent-tools.d.ts.map +1 -1
  185. package/node_modules/@xrkseek/server-face/dist/settings-agent-tools.js +13 -1
  186. package/node_modules/@xrkseek/server-face/dist/settings-agent-tools.js.map +1 -1
  187. package/node_modules/@xrkseek/server-face/dist/settings-document.d.ts.map +1 -1
  188. package/node_modules/@xrkseek/server-face/dist/settings-document.js +10 -11
  189. package/node_modules/@xrkseek/server-face/dist/settings-document.js.map +1 -1
  190. package/node_modules/@xrkseek/server-face/dist/settings-schemas.d.ts.map +1 -1
  191. package/node_modules/@xrkseek/server-face/dist/settings-schemas.js +17 -10
  192. package/node_modules/@xrkseek/server-face/dist/settings-schemas.js.map +1 -1
  193. package/node_modules/@xrkseek/server-face/dist/subagent-registry.d.ts +46 -0
  194. package/node_modules/@xrkseek/server-face/dist/subagent-registry.d.ts.map +1 -1
  195. package/node_modules/@xrkseek/server-face/dist/subagent-registry.js +111 -8
  196. package/node_modules/@xrkseek/server-face/dist/subagent-registry.js.map +1 -1
  197. package/node_modules/@xrkseek/server-face/dist/subagent-tools.d.ts +21 -0
  198. package/node_modules/@xrkseek/server-face/dist/subagent-tools.d.ts.map +1 -1
  199. package/node_modules/@xrkseek/server-face/dist/subagent-tools.js +422 -85
  200. package/node_modules/@xrkseek/server-face/dist/subagent-tools.js.map +1 -1
  201. package/node_modules/@xrkseek/server-face/dist/types.d.ts +9 -0
  202. package/node_modules/@xrkseek/server-face/dist/types.d.ts.map +1 -1
  203. package/node_modules/@xrkseek/server-face/dist/wire/paths.d.ts.map +1 -1
  204. package/node_modules/@xrkseek/server-face/dist/wire/paths.js +2 -0
  205. package/node_modules/@xrkseek/server-face/dist/wire/paths.js.map +1 -1
  206. package/node_modules/@xrkseek/server-face/dist/workspace-face.d.ts.map +1 -1
  207. package/node_modules/@xrkseek/server-face/dist/workspace-face.js +7 -0
  208. package/node_modules/@xrkseek/server-face/dist/workspace-face.js.map +1 -1
  209. package/node_modules/@xrkseek/server-host/dist/agent-cache.d.ts +5 -1
  210. package/node_modules/@xrkseek/server-host/dist/agent-cache.d.ts.map +1 -1
  211. package/node_modules/@xrkseek/server-host/dist/agent-cache.js +29 -3
  212. package/node_modules/@xrkseek/server-host/dist/agent-cache.js.map +1 -1
  213. package/node_modules/@xrkseek/server-host/dist/index.d.ts +21 -0
  214. package/node_modules/@xrkseek/server-host/dist/index.d.ts.map +1 -1
  215. package/node_modules/@xrkseek/server-host/dist/index.js +108 -21
  216. package/node_modules/@xrkseek/server-host/dist/index.js.map +1 -1
  217. package/node_modules/@xrkseek/server-http/dist/static.d.ts.map +1 -1
  218. package/node_modules/@xrkseek/server-http/dist/static.js +45 -5
  219. package/node_modules/@xrkseek/server-http/dist/static.js.map +1 -1
  220. package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.d.ts +8 -0
  221. package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.d.ts.map +1 -1
  222. package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.js +6 -2
  223. package/node_modules/@xrkseek/workspace/dist/ecosystem-instructions.js.map +1 -1
  224. package/node_modules/@xrkseek/workspace/dist/index.d.ts +16 -0
  225. package/node_modules/@xrkseek/workspace/dist/index.d.ts.map +1 -1
  226. package/node_modules/@xrkseek/workspace/dist/index.js +17 -8
  227. package/node_modules/@xrkseek/workspace/dist/index.js.map +1 -1
  228. package/package.json +1 -1
@@ -1,18 +1,21 @@
1
1
  import { SUBAGENT_ROUTING_PROMPT_TEXT } from "@xrkseek/core-tools";
2
2
  import { listPendingAdmits, readSessionEvents, } from "@xrkseek/core-session";
3
3
  import { dispatchFaceMethod } from "./dispatch.js";
4
- import { lastAssistantBodyText } from "./adapt/subagent-notice.js";
4
+ import { formatChildOutcome, lastAssistantBodyText, lastModelRetryNotice, } from "./adapt/subagent-notice.js";
5
5
  import { boundChildAnswer, SUBAGENT_ANSWER_INLINE_BYTES, } from "./adapt/subagent-answer-bound.js";
6
6
  export { boundChildAnswer, SUBAGENT_ANSWER_INLINE_BYTES };
7
+ import { subagentOwnedEvents } from "./subagent-registry.js";
7
8
  import { DEFAULT_MAX_ACTIVE_CHILDREN, DEFAULT_MAX_DEPTH, resolveAgentPresetProfile, } from "./presets-catalog.js";
8
9
  import { effectiveSessionAgentPreset } from "./session-agent-preset.js";
9
10
  import { resolveSessionCwd } from "./session-cwd.js";
11
+ import { canvasWorkspaceIdForSession } from "./canvas-tools.js";
12
+ import { formatRosterCatalog } from "./agent-roster-store.js";
10
13
  import { ExternalAgentError, parseExternalAgentKind, parseExternalAgentProduct, runExternalAgentTurn, supportsExternalContinuable, startExternalContinuable, isChildSessionActive, } from "./external-agent-runtime.js";
11
14
  import { createSubagentWorktree, finalizeSubagentWorktree, formatWorktreeResult, worktreeContextNote, worktreeSkippedForRemote, } from "./subagent-worktree.js";
12
- import { resolveSessionModelSelection } from "./model-catalog.js";
15
+ import { resolveSessionModelSelection, resolveSubagentModelSetting, } from "./model-catalog.js";
13
16
  import { selectSessionModel } from "./select-session-model.js";
14
17
  import { appendOutputContract, buildOutputSchemaRetryMessage, coerceOutputSchema, validateOutputAgainstSchema, } from "./agent-team-output.js";
15
- import { applySpawnRoleReminder, parseAgentTeamSpawnRole, } from "./agent-team-roles.js";
18
+ import { applySubagentSpawnPreamble, parseAgentTeamSpawnRole, rootUserAuthorizationBlock, } from "./agent-team-roles.js";
16
19
  import { isAgentTeamRole, } from "./agent-team-graph.js";
17
20
  import { bindRalphTool } from "./ralph-tool.js";
18
21
  const FOREGROUND_WAIT_MS = 10 * 60 * 1000;
@@ -106,19 +109,45 @@ async function waitDrainIdle(runtime, sessionId, signal, timeoutMs = FOREGROUND_
106
109
  // for its child would otherwise hang the tool batch forever. The join
107
110
  // stays attached in the background — the caller cancels the child right
108
111
  // after this rejects, which is what actually drains it.
109
- await abortable(run(sessionId), signal);
112
+ //
113
+ // The race must also carry the timeout: latch.run() joins until the
114
+ // child's chain *actually* settles, so a child stuck on a tool that
115
+ // ignores abort pins the parent's turn for as long as it hangs. The
116
+ // timeout used to apply only to the polling fallback below — i.e. never
117
+ // on the Host path, which always has run().
118
+ await raceDeadline(abortable(run(sessionId), signal), timeoutMs, sessionId);
110
119
  return;
111
120
  }
112
- const deadline = Date.now() + timeoutMs;
113
- while (runtime.drain.isActive(sessionId)) {
114
- if (signal?.aborted) {
115
- throw new DOMException("aborted", "AbortError");
116
- }
117
- if (Date.now() > deadline) {
118
- throw new Error(`subagent timed out waiting for session ${sessionId}`);
121
+ // Same deadline semantics as the hub.run branch above — one throw site, one
122
+ // message, so a caller (or a log grep) cannot tell the two paths apart.
123
+ await raceDeadline((async () => {
124
+ while (runtime.drain.isActive(sessionId)) {
125
+ if (signal?.aborted) {
126
+ throw new DOMException("aborted", "AbortError");
127
+ }
128
+ await new Promise((r) => setTimeout(r, POLL_MS));
119
129
  }
120
- await new Promise((r) => setTimeout(r, POLL_MS));
121
- }
130
+ })(), timeoutMs, sessionId);
131
+ }
132
+ /**
133
+ * Reject once `timeoutMs` elapses, naming the session so the failure reads
134
+ * like the polling branch's. The wrapped promise keeps running — the caller
135
+ * cancels the child right after, which is what actually drains it.
136
+ */
137
+ function raceDeadline(promise, timeoutMs, sessionId) {
138
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
139
+ return promise;
140
+ let timer;
141
+ const timedOut = new Promise((_, reject) => {
142
+ timer = setTimeout(() => reject(new Error(`subagent timed out waiting for session ${sessionId}`)), timeoutMs);
143
+ });
144
+ return Promise.race([
145
+ promise.finally(() => {
146
+ if (timer !== undefined)
147
+ clearTimeout(timer);
148
+ }),
149
+ timedOut,
150
+ ]);
122
151
  }
123
152
  /** Reject as soon as `signal` aborts; the wrapped promise keeps running. */
124
153
  function abortable(promise, signal) {
@@ -146,6 +175,27 @@ function abortError(reason) {
146
175
  err.name = "AbortError";
147
176
  return err;
148
177
  }
178
+ /**
179
+ * How a parent gets work back out of a child that outlived the foreground
180
+ * wait. One place on purpose: background spawn, timeout salvage and failure
181
+ * replies must not drift apart, or a parent learns the recovery verbs only
182
+ * on the happy path.
183
+ */
184
+ export function childResumeHint(childId) {
185
+ return (`Child session \`${childId}\` is not gone: \`wait_agent\` keeps waiting for it, ` +
186
+ `\`session_read\` replays its history, \`session_trace\` shows its lineage, ` +
187
+ `\`list_agents\` its state, and \`followup_task\` wakes a new turn on it. Read it ` +
188
+ `before spawning a replacement so the new task does not repeat work it already did.`);
189
+ }
190
+ /**
191
+ * The slice of a child's log the parent owns: seeded parent text never counts.
192
+ * One read, two projections (`lastAssistantBodyText` for the salvaged answer,
193
+ * `lastModelRetryNotice` for what its model request was doing).
194
+ */
195
+ export function childOwnedEvents(runtime, childId) {
196
+ const link = runtime.subagents.getByChild(childId);
197
+ return subagentOwnedEvents(link, readSessionEvents(runtime.store, childId));
198
+ }
149
199
  function formatSubagentAbortReason(reason) {
150
200
  if (reason === undefined || reason === null)
151
201
  return "aborted";
@@ -193,29 +243,88 @@ function countActiveChildren(runtime, parentSessionId) {
193
243
  }
194
244
  return n;
195
245
  }
246
+ /**
247
+ * Undo a half-made spawn. `session.create` / `session.fork` already made the
248
+ * child durable and registered its link, so any failure between there and a
249
+ * successful `session.prompt` would otherwise leave an empty session in the
250
+ * sidebar and a permanent entry in the catalog / quota counts.
251
+ */
252
+ async function discardUnstartedChild(runtime, childSessionId) {
253
+ runtime.subagents.detach(childSessionId);
254
+ runtime.agentTeams.removeNode(childSessionId);
255
+ runtime.agentTeamTasks.forgetByChild(childSessionId);
256
+ runtime.sessionCwds.delete(childSessionId);
257
+ try {
258
+ await dispatchFaceMethod(runtime, "session.cancel", `tool-sa-discard-${childSessionId}`, { sessionId: childSessionId, cascade: true });
259
+ }
260
+ catch {
261
+ /* best effort: the log is dropped either way */
262
+ }
263
+ try {
264
+ runtime.store.delete?.(childSessionId);
265
+ }
266
+ catch {
267
+ /* store without delete / already gone */
268
+ }
269
+ await Promise.resolve(runtime.invalidateAgent?.(childSessionId)).catch(() => undefined);
270
+ }
271
+ /**
272
+ * Reject a caller-supplied `task_id` another child already owns. Checked
273
+ * *before* the child exists: rebinding the card would misdirect that child's
274
+ * later idle notice (`completeByChild` keys on childSessionId).
275
+ */
276
+ function taskIdConflict(runtime, taskIdHint) {
277
+ if (!taskIdHint)
278
+ return undefined;
279
+ const boundChild = runtime.agentTeamTasks.get(taskIdHint)?.childSessionId;
280
+ return boundChild
281
+ ? `subagent: task_id ${taskIdHint} is already bound to child ${boundChild}; use a different task_id`
282
+ : undefined;
283
+ }
196
284
  /**
197
285
  * Optional LLM target for a freshly created child.
198
- * `model` alone inherits the parent's current provider; `provider` alone is rejected.
286
+ *
287
+ * Precedence: the call's own args → the fleet-wide `agent-loop.subagentModel`
288
+ * setting → the parent's own route. `model` alone inherits the parent's current
289
+ * provider; `provider` alone is rejected.
199
290
  */
200
291
  async function applySubagentModelOverride(runtime, args) {
201
292
  const providerRaw = args.provider?.trim() ?? "";
202
293
  const modelRaw = args.model?.trim() ?? "";
203
294
  const effortRaw = args.reasoningEffort?.trim() ?? "";
204
- if (!providerRaw && !modelRaw && !effortRaw)
295
+ // Refuse loudly rather than silently: an explicit model on an external
296
+ // child would otherwise read as honored when the subprocess ignores it.
297
+ if (args.skipExternal && (providerRaw || modelRaw || effortRaw)) {
298
+ return {
299
+ ok: false,
300
+ message: "subagent: model / provider / reasoning_effort do not apply to an external runtime " +
301
+ "(acp / app-server / claude-code) — the child CLI picks its own model. " +
302
+ "Drop the argument, or use runtime=in-process.",
303
+ };
304
+ }
305
+ // No per-call pin: the setting owns the route, and an empty setting leaves
306
+ // the child on the parent's selection that `session.create` copied.
307
+ const fleet = providerRaw || modelRaw || effortRaw
308
+ ? undefined
309
+ : resolveSubagentModelSetting(runtime);
310
+ if (!providerRaw && !modelRaw && !effortRaw && !fleet)
205
311
  return { ok: true };
206
- if (!modelRaw) {
312
+ const model = modelRaw || fleet?.model || "";
313
+ const effort = effortRaw || fleet?.reasoningEffort || "";
314
+ if (!model) {
207
315
  return {
208
316
  ok: false,
209
317
  message: "subagent: model is required when overriding provider or reasoning_effort",
210
318
  };
211
319
  }
212
320
  const provider = providerRaw
321
+ || fleet?.provider
213
322
  || resolveSessionModelSelection(runtime, args.parentSessionId).provider;
214
323
  const selected = await selectSessionModel(runtime, {
215
324
  sessionId: args.childSessionId,
216
325
  provider,
217
- model: modelRaw,
218
- ...(effortRaw ? { reasoningEffort: effortRaw } : {}),
326
+ model,
327
+ ...(effort ? { reasoningEffort: effort } : {}),
219
328
  });
220
329
  if (!selected.ok) {
221
330
  return {
@@ -226,27 +335,54 @@ async function applySubagentModelOverride(runtime, args) {
226
335
  return { ok: true };
227
336
  }
228
337
  function createSubagentTool(options) {
229
- const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
230
- const maxActiveChildren = options.maxActiveChildren ?? DEFAULT_MAX_ACTIVE_CHILDREN;
338
+ /**
339
+ * Ceilings for one call. Resolved live (not frozen at bind time) so the
340
+ * numbers `analytics` / `list_agents` report are the numbers spawn
341
+ * enforces — a Settings change takes effect on the next call instead of
342
+ * at the next agent rebuild. The bind options are a test-only override.
343
+ */
344
+ const caps = () => {
345
+ const quota = resolveSubagentQuota(options.runtime, options.parentSessionId);
346
+ return {
347
+ maxDepth: options.maxDepth ?? quota.maxDepth,
348
+ maxActive: options.maxActiveChildren ?? quota.maxActive,
349
+ active: quota.active,
350
+ };
351
+ };
352
+ const foregroundWaitMs = options.foregroundWaitMs ?? FOREGROUND_WAIT_MS;
353
+ const description = "Delegate a self-contained task to a teammate subagent (separate session, same workspace as this session unless worktree:true). " +
354
+ "Use for focused independent work — research, a scoped implementation, analysis, or read-only review — " +
355
+ "so it does not consume this conversation's context. " +
356
+ "By default the child cannot see this chat — give a complete standalone prompt (paths, goals, constraints, persona). " +
357
+ "Do not tell the child to read AGENTS.md to discover who it is; Face prepends parent/child session ids, mode, role, and cwd. " +
358
+ "Set inherit_context true to seed the child with this session's completed turns only " +
359
+ "(the current in-flight turn is excluded). " +
360
+ "By default waits for the result (one-shot, no later human messages on that child). " +
361
+ "That foreground wait is bounded (10 min by default): on expiry the child is NOT discarded — " +
362
+ "whatever answer it already wrote is salvaged into this result and the child id stays readable " +
363
+ "(session_read / followup_task), so prefer run_in_background for genuinely long work instead of retrying blind. " +
364
+ "Set run_in_background true for a continuable child (chat companion / long task) and continue via followup_task / send_message. " +
365
+ "Prefer Agent Team member_id (catalog on this tool and team_list) over restating playbook or tools; " +
366
+ "optional role (worker|researcher|reviewer|lead) is only the fallback when no member fits; " +
367
+ "optional output_schema appends an OUTPUT CONTRACT and validates the final JSON; " +
368
+ "optional task_id / task_name register the work on the Agent Teams task board. " +
369
+ "Optional provider / model / reasoning_effort pin the child's LLM (model alone keeps the parent provider). " +
370
+ "Optional runtime: omit or in-process (default Face child); acp / app-server / claude-code spawn an external subprocess. " +
371
+ "acp / app-server support run_in_background + followup_task / send_message / wait_agent / interrupt_agent on the same list surface; claude-code remains one-shot print. " +
372
+ "The child's answer is returned inline in this tool result when short. " +
373
+ "Only when the UTF-8 body exceeds ~12KB does Face spill it under ~/.xrk/spill/tool-outputs/ " +
374
+ "and replace the body with a path + head/tail preview — then read_file that path. " +
375
+ "An empty or missing tool-outputs directory means nothing spilled; the full answer is already in this result (or the session event log), not on disk.";
231
376
  return {
232
377
  name: "subagent",
233
- description: "Delegate a self-contained task to a teammate subagent (separate session/context). " +
234
- "Use for focused independent work — research, a scoped implementation, analysis, or read-only review — " +
235
- "so it does not consume this conversation's context. " +
236
- "By default the child cannot see this chat — give a complete standalone prompt. " +
237
- "Set inherit_context true to seed the child with this session's completed turns only " +
238
- "(the current in-flight turn is excluded). " +
239
- "By default waits for the result; set run_in_background true to get a durable child id and continue later via send_message. " +
240
- "Optional role (worker|researcher|reviewer|lead) prefixes a role reminder; " +
241
- "optional output_schema appends an OUTPUT CONTRACT and validates the final JSON; " +
242
- "optional task_id / task_name register the work on the Agent Teams task board. " +
243
- "Optional provider / model / reasoning_effort pin the child's LLM (model alone keeps the parent provider). " +
244
- "Optional runtime: omit or in-process (default Face child); acp / app-server / claude-code spawn an external subprocess. " +
245
- "acp / app-server support run_in_background + followup_task / send_message / wait_agent / interrupt_agent on the same list surface; claude-code remains one-shot print. " +
246
- "The child's answer is returned inline in this tool result when short. " +
247
- "Only when the UTF-8 body exceeds ~12KB does Face spill it under ~/.xrk/spill/tool-outputs/ " +
248
- "and replace the body with a path + head/tail preview — then read_file that path. " +
249
- "An empty or missing tool-outputs directory means nothing spilled; the full answer is already in this result (or the session event log), not on disk.",
378
+ description,
379
+ dynamicSchema: () => {
380
+ const ws = canvasWorkspaceIdForSession(options.runtime, options.parentSessionId);
381
+ const catalog = formatRosterCatalog(options.runtime.agentRoster.listVisible(ws));
382
+ return {
383
+ description: `${description}\n\nPrefer member_id from this Agent Team catalog:\n${catalog}`,
384
+ };
385
+ },
250
386
  parameters: {
251
387
  type: "object",
252
388
  properties: {
@@ -256,12 +392,19 @@ function createSubagentTool(options) {
256
392
  },
257
393
  prompt: {
258
394
  type: "string",
259
- description: "Task for the subagent. When inherit_context is false (default), include paths, goals, and constraints — it does not see this conversation. When inherit_context is true, build on the seeded completed turns.",
395
+ description: "Task for the subagent. Put the actual job here (paths, goals, constraints, any persona). " +
396
+ "When inherit_context is false (default), the child does not see this conversation. " +
397
+ "When inherit_context is true, build on the seeded completed turns. " +
398
+ "Face already injects parent/child session ids — do not send the child to AGENTS.md to learn its role.",
260
399
  },
261
400
  inherit_context: {
262
401
  type: "boolean",
263
402
  description: "If true, seed the child with this session's completed-turn prefix (open turn excluded). Default false. Ignored for external runtimes.",
264
403
  },
404
+ member_id: {
405
+ type: "string",
406
+ description: "Agent Team member id. Face applies that member's tools, inject, and playbook — the same three surfaces the parent already edits. Prefer this over restating those in prompt.",
407
+ },
265
408
  run_in_background: {
266
409
  type: "boolean",
267
410
  description: "If true, return the child session id immediately (continuable). Default false (wait for the child's final answer). Supported for in-process and external acp / app-server (not claude-code).",
@@ -329,10 +472,11 @@ function createSubagentTool(options) {
329
472
  isError: true,
330
473
  };
331
474
  }
332
- const prompt = String(a.prompt ?? "").trim();
333
- if (!prompt) {
475
+ const promptBare = String(a.prompt ?? "").trim();
476
+ if (!promptBare) {
334
477
  return { content: "subagent: empty prompt", isError: true };
335
478
  }
479
+ let prompt = promptBare;
336
480
  const roleRaw = a.role ?? a.agent_type;
337
481
  let spawnRole;
338
482
  if (roleRaw !== undefined && String(roleRaw).trim()) {
@@ -344,6 +488,23 @@ function createSubagentTool(options) {
344
488
  };
345
489
  }
346
490
  }
491
+ const memberId = String(a.member_id ?? "").trim();
492
+ let rosterMember;
493
+ if (memberId) {
494
+ const workspaceId = canvasWorkspaceIdForSession(options.runtime, options.parentSessionId);
495
+ rosterMember = options.runtime.agentRoster.get(workspaceId, memberId);
496
+ if (!rosterMember) {
497
+ return {
498
+ content: `subagent: unknown member_id "${memberId}" (team_list first)`,
499
+ isError: true,
500
+ };
501
+ }
502
+ prompt = `${rosterMember.playbook.trim()}\n\nTASK:\n${promptBare}`;
503
+ if (spawnRole === undefined && rosterMember.role !== "default") {
504
+ spawnRole = rosterMember.role;
505
+ }
506
+ }
507
+ const memberSpawn = memberId ? { memberId } : {};
347
508
  const schemaCoerce = coerceOutputSchema(a.output_schema);
348
509
  if (schemaCoerce.error) {
349
510
  return {
@@ -361,20 +522,35 @@ function createSubagentTool(options) {
361
522
  }
362
523
  if (runtimeKind !== "in-process") {
363
524
  const background = a.run_in_background === true;
525
+ // One cwd for every external branch. `resolveSessionCwd` already
526
+ // falls back to the workspace root, so the child runs where the
527
+ // parent runs instead of always at the root (which silently broke
528
+ // any session that had moved into another workspace).
529
+ const externalCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
364
530
  if (background && !supportsExternalContinuable(runtimeKind)) {
365
531
  return {
366
532
  content: "subagent: run_in_background is not supported for claude-code (print); use acp or app-server",
367
533
  isError: true,
368
534
  };
369
535
  }
536
+ if (a.worktree === true) {
537
+ // Worktree handling lives in the in-process branch below; silently
538
+ // dropping the flag would have the child edit the parent's own
539
+ // checkout while the caller believed it was isolated.
540
+ return {
541
+ content: "subagent: worktree is only supported for runtime=in-process " +
542
+ "(an external runtime owns its own working directory).",
543
+ isError: true,
544
+ };
545
+ }
370
546
  const depth = subagentDepth(options.runtime, options.parentSessionId);
547
+ const { maxDepth, maxActive: maxActiveChildren, active } = caps();
371
548
  if (depth >= maxDepth) {
372
549
  return {
373
550
  content: `subagent: max depth ${maxDepth} reached (current depth ${depth})`,
374
551
  isError: true,
375
552
  };
376
553
  }
377
- const active = countActiveChildren(options.runtime, options.parentSessionId);
378
554
  if (active >= maxActiveChildren) {
379
555
  return {
380
556
  content: `subagent: max active children ${maxActiveChildren} reached (active ${active})`,
@@ -391,6 +567,10 @@ function createSubagentTool(options) {
391
567
  parentSessionId: options.parentSessionId,
392
568
  label,
393
569
  mode: "continuable",
570
+ ...memberSpawn,
571
+ ...(spawnRole && spawnRole !== "default"
572
+ ? { role: spawnRole }
573
+ : {}),
394
574
  });
395
575
  if (!created.result.ok) {
396
576
  return {
@@ -407,11 +587,22 @@ function createSubagentTool(options) {
407
587
  ...(typeof a.reasoning_effort === "string"
408
588
  ? { reasoningEffort: a.reasoning_effort }
409
589
  : {}),
590
+ // An ACP / app-server / claude-code child runs on ITS OWN CLI
591
+ // config; a Face-side pin cannot reach it. Recording one anyway
592
+ // made `/status` and `list_agents` report a route the subprocess
593
+ // never used, so the record is skipped rather than faked.
594
+ skipExternal: true,
410
595
  });
411
596
  if (!modelPin.ok) {
597
+ await discardUnstartedChild(options.runtime, childId);
412
598
  return { content: modelPin.message, isError: true };
413
599
  }
414
600
  const taskTitle = String(a.task_name ?? "").trim() || label;
601
+ const taskIdConflictMsg = taskIdConflict(options.runtime, String(a.task_id ?? "").trim() || undefined);
602
+ if (taskIdConflictMsg) {
603
+ await discardUnstartedChild(options.runtime, childId);
604
+ return { content: taskIdConflictMsg, isError: true };
605
+ }
415
606
  const task = options.runtime.agentTeamTasks.open({
416
607
  parentSessionId: options.parentSessionId,
417
608
  title: taskTitle,
@@ -430,7 +621,7 @@ function createSubagentTool(options) {
430
621
  runtime: options.runtime,
431
622
  faceSessionId: childId,
432
623
  kind: runtimeKind,
433
- cwd: resolveSessionCwd(options.runtime, options.parentSessionId) ?? options.runtime.workspaceRoot,
624
+ cwd: externalCwd,
434
625
  prompt,
435
626
  background: true,
436
627
  ...(signal ? { signal } : {}),
@@ -454,7 +645,7 @@ function createSubagentTool(options) {
454
645
  }
455
646
  const result = await runExternalAgentTurn({
456
647
  kind: runtimeKind,
457
- cwd: options.runtime.workspaceRoot,
648
+ cwd: externalCwd,
458
649
  prompt,
459
650
  ...(signal ? { signal } : {}),
460
651
  ...(options.externalEnv ? { env: options.externalEnv } : {}),
@@ -477,13 +668,13 @@ function createSubagentTool(options) {
477
668
  }
478
669
  }
479
670
  const depth = subagentDepth(options.runtime, options.parentSessionId);
671
+ const { maxDepth, maxActive: maxActiveChildren, active } = caps();
480
672
  if (depth >= maxDepth) {
481
673
  return {
482
674
  content: `subagent: max depth ${maxDepth} reached (current depth ${depth})`,
483
675
  isError: true,
484
676
  };
485
677
  }
486
- const active = countActiveChildren(options.runtime, options.parentSessionId);
487
678
  if (active >= maxActiveChildren) {
488
679
  return {
489
680
  content: `subagent: max active children ${maxActiveChildren} reached (active ${active})`,
@@ -497,6 +688,14 @@ function createSubagentTool(options) {
497
688
  (background ? "subagent" : "subagent-task");
498
689
  const linkMode = background ? "continuable" : "one-shot";
499
690
  const parentCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
691
+ const taskTitle = String(a.task_name ?? "").trim() || label;
692
+ const taskIdHint = String(a.task_id ?? "").trim() || undefined;
693
+ // Before anything is allocated: reusing a task_id another child owns
694
+ // must not leave a worktree or a session behind.
695
+ const taskIdError = taskIdConflict(options.runtime, taskIdHint);
696
+ if (taskIdError) {
697
+ return { content: taskIdError, isError: true };
698
+ }
500
699
  let isolated = null;
501
700
  let managedLease = null;
502
701
  let worktreeSkip = "";
@@ -528,15 +727,13 @@ function createSubagentTool(options) {
528
727
  }
529
728
  }
530
729
  }
531
- let childPrompt = applySpawnRoleReminder(prompt, spawnRole);
730
+ let taskBody = prompt;
532
731
  if (outputSchema) {
533
- childPrompt = appendOutputContract(childPrompt, outputSchema);
732
+ taskBody = appendOutputContract(taskBody, outputSchema);
534
733
  }
535
- childPrompt = isolated
536
- ? `${childPrompt}\n\n${worktreeContextNote(isolated)}`
537
- : childPrompt;
538
- const taskTitle = String(a.task_name ?? "").trim() || label;
539
- const taskIdHint = String(a.task_id ?? "").trim() || undefined;
734
+ taskBody = isolated
735
+ ? `${taskBody}\n\n${worktreeContextNote(isolated)}`
736
+ : taskBody;
540
737
  const openTask = (childSessionId) => {
541
738
  const task = options.runtime.agentTeamTasks.open({
542
739
  parentSessionId: options.parentSessionId,
@@ -591,14 +788,44 @@ function createSubagentTool(options) {
591
788
  finalizeSubagentWorktree(isolated);
592
789
  }
593
790
  };
791
+ /**
792
+ * Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
793
+ * EVERY exit path, including an unexpected throw. Explicit
794
+ * `discardUnstartedChild` calls cover the known failure branches; this
795
+ * flag + finally is the backstop for anything they miss.
796
+ */
797
+ let started = false;
798
+ const discardIfUnstarted = async () => {
799
+ if (started || !childId)
800
+ return;
801
+ started = true;
802
+ dropIsolated();
803
+ await discardUnstartedChild(options.runtime, childId);
804
+ };
594
805
  let childId;
595
- let seedEventCount = 0;
806
+ /** Fork seed size — bounds the root-evidence slice below. */
807
+ let seedCut = 0;
596
808
  if (inherit) {
597
- const forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
598
- sessionId: options.parentSessionId,
599
- linkMode,
600
- label,
601
- });
809
+ // A transport reject (not an RpcResult error) here would strand the
810
+ // worktree lease allocated above; catch keeps the cleanup total.
811
+ let forked;
812
+ try {
813
+ forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
814
+ sessionId: options.parentSessionId,
815
+ linkMode,
816
+ label,
817
+ ...(spawnRole && spawnRole !== "default"
818
+ ? { role: spawnRole }
819
+ : {}),
820
+ });
821
+ }
822
+ catch {
823
+ dropIsolated();
824
+ return {
825
+ content: "subagent seed failed: fork request errored",
826
+ isError: true,
827
+ };
828
+ }
602
829
  if (!forked.result.ok) {
603
830
  // No completed turn yet — fall back to a fresh child (DSH fork omits seed).
604
831
  if (forked.result.error.code === "fork-unavailable") {
@@ -606,6 +833,10 @@ function createSubagentTool(options) {
606
833
  parentSessionId: options.parentSessionId,
607
834
  label,
608
835
  mode: linkMode,
836
+ ...memberSpawn,
837
+ ...(spawnRole && spawnRole !== "default"
838
+ ? { role: spawnRole }
839
+ : {}),
609
840
  });
610
841
  if (!created.result.ok) {
611
842
  dropIsolated();
@@ -626,7 +857,10 @@ function createSubagentTool(options) {
626
857
  }
627
858
  else {
628
859
  childId = String(forked.result.value.sessionId);
629
- seedEventCount = Number(forked.result.value.eventCount ?? 0);
860
+ // Durable seed boundary: every answer read of this child must
861
+ // slice from here, or the parent's last reply reads as its answer.
862
+ seedCut = Number(forked.result.value.eventCount ?? 0);
863
+ options.runtime.subagents.setSeedEventCount(childId, seedCut);
630
864
  }
631
865
  }
632
866
  else {
@@ -634,6 +868,10 @@ function createSubagentTool(options) {
634
868
  parentSessionId: options.parentSessionId,
635
869
  label,
636
870
  mode: linkMode,
871
+ ...memberSpawn,
872
+ ...(spawnRole && spawnRole !== "default"
873
+ ? { role: spawnRole }
874
+ : {}),
637
875
  });
638
876
  if (!created.result.ok) {
639
877
  dropIsolated();
@@ -654,21 +892,65 @@ function createSubagentTool(options) {
654
892
  : {}),
655
893
  });
656
894
  if (!modelPin.ok) {
657
- dropIsolated();
895
+ await discardIfUnstarted();
658
896
  return { content: modelPin.message, isError: true };
659
897
  }
660
898
  if (isolated) {
661
899
  options.runtime.sessionCwds.set(childId, isolated.path);
662
- await options.runtime.invalidateAgent?.(childId);
900
+ // Worktree bind failure must not leak the child: swallow and let the
901
+ // prompt below decide the spawn's fate (or the catch backstop).
902
+ await Promise.resolve(options.runtime.invalidateAgent?.(childId)).catch(() => undefined);
663
903
  }
904
+ const childPrompt = applySubagentSpawnPreamble({
905
+ prompt: taskBody,
906
+ parentSessionId: options.parentSessionId,
907
+ childSessionId: childId,
908
+ mode: linkMode,
909
+ label,
910
+ ...(spawnRole ? { role: spawnRole } : {}),
911
+ inheritContext: inherit,
912
+ cwd: isolated?.path ?? parentCwd,
913
+ isolatedWorktree: isolated !== null,
914
+ ...(rosterMember
915
+ ? { memberId: rosterMember.id, inject: rosterMember.inject }
916
+ : {}),
917
+ // The human's own asks: a child that only sees the parent's paraphrase
918
+ // drifts from what was actually authorized (Codex
919
+ // `control/user_authorization.rs`). A forked child already carries
920
+ // these in its seeded transcript, so only add them beyond the cut.
921
+ ...(() => {
922
+ const block = rootUserAuthorizationBlock({
923
+ parentEvents: readSessionEvents(options.runtime.store, options.parentSessionId),
924
+ ...(seedCut > 0 ? { sinceEventCount: seedCut } : {}),
925
+ });
926
+ return block ? { userAuthorization: block } : {};
927
+ })(),
928
+ });
664
929
  const task = openTask(childId);
665
930
  const worktreeLine = reclaimIsolated;
666
- const prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
667
- sessionId: childId,
668
- mode: "queue",
669
- content: [{ type: "text", text: childPrompt }],
670
- });
931
+ // A transport-level reject (not an RpcResult error) must not leak the
932
+ // child: the catch backstop tears the half-made spawn down.
933
+ let prompted;
934
+ try {
935
+ prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
936
+ sessionId: childId,
937
+ mode: "queue",
938
+ content: [{ type: "text", text: childPrompt }],
939
+ });
940
+ }
941
+ catch (err) {
942
+ await discardIfUnstarted();
943
+ const extra = worktreeLine(false);
944
+ const message = err instanceof Error ? err.message : String(err);
945
+ return {
946
+ content: [`subagent prompt failed: ${message}`, extra]
947
+ .filter(Boolean)
948
+ .join("\n"),
949
+ isError: true,
950
+ };
951
+ }
671
952
  if (!prompted.result.ok) {
953
+ await discardIfUnstarted();
672
954
  const extra = worktreeLine(false);
673
955
  return {
674
956
  content: [`subagent prompt failed: ${prompted.result.error.message}`, extra]
@@ -677,6 +959,8 @@ function createSubagentTool(options) {
677
959
  isError: true,
678
960
  };
679
961
  }
962
+ // Spawn accepted — the child is no longer a half-made shell.
963
+ started = true;
680
964
  if (background) {
681
965
  return {
682
966
  content: [
@@ -689,7 +973,8 @@ function createSubagentTool(options) {
689
973
  outputSchema
690
974
  ? "OUTPUT CONTRACT attached; result will be schema-checked on idle."
691
975
  : undefined,
692
- "Use followup_task to wake a new task, send_message (delivery queue|steer) to continue, wait_agent for results, interrupt_agent (takeover true to pause for human) to stop, list_agents / analytics for quota.",
976
+ childResumeHint(childId),
977
+ "Use followup_task to wake a new task, send_message (delivery queue|steer) to continue, wait_agent for results, interrupt_agent (takeover true to pause for human) to stop, analytics for quota.",
693
978
  "Keep working; do not busy-poll.",
694
979
  worktreeLine(true) || undefined,
695
980
  ]
@@ -698,29 +983,63 @@ function createSubagentTool(options) {
698
983
  };
699
984
  }
700
985
  try {
701
- await waitDrainIdle(options.runtime, childId, signal);
986
+ await waitDrainIdle(options.runtime, childId, signal, foregroundWaitMs);
702
987
  }
703
988
  catch (err) {
704
989
  const message = err instanceof Error ? err.message : String(err);
705
- await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`, { sessionId: childId }).catch(() => undefined);
990
+ const timedOut = /timed out waiting/i.test(message);
991
+ // Salvage before cancelling: `session.cancel` drops the chain, so a
992
+ // child that had already written its answer would bill the parent for
993
+ // nothing. A child that produced nothing is stuck, so it still gets
994
+ // cancelled instead of left dangling.
995
+ const owned = childOwnedEvents(options.runtime, childId);
996
+ const salvaged = lastAssistantBodyText(owned);
997
+ const retrying = timedOut
998
+ ? lastModelRetryNotice(owned)
999
+ : undefined;
1000
+ if (!salvaged) {
1001
+ await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`,
1002
+ // `parent`, not the default `user`: a parent that gave up waiting
1003
+ // is not a human hitting Stop. The old default stamped
1004
+ // turn/end `reason: user`, so every timed-out child read as "stopped
1005
+ // by the user" in the child log and in the completion notice.
1006
+ { sessionId: childId, cause: { kind: "parent" } }).catch(() => undefined);
1007
+ }
706
1008
  options.runtime.agentTeamTasks.complete(task.id, {
707
1009
  ok: false,
708
1010
  preview: message,
709
1011
  });
710
1012
  const extra = worktreeLine(false);
711
1013
  return {
712
- content: [`subagent failed: ${message}`, extra].filter(Boolean).join("\n"),
1014
+ content: [
1015
+ salvaged
1016
+ ? `${message}, but the child finished work before the wait expired — its last answer is salvaged below.`
1017
+ : `subagent failed: ${message}`,
1018
+ salvaged || undefined,
1019
+ timedOut ? childResumeHint(childId) : undefined,
1020
+ retrying,
1021
+ extra,
1022
+ ]
1023
+ .filter(Boolean)
1024
+ .join("\n"),
713
1025
  isError: true,
714
1026
  };
715
1027
  }
716
1028
  const all = readSessionEvents(options.runtime.store, childId);
717
1029
  // Never return seeded parent assistant text as the child's result.
718
- let owned = seedEventCount > 0 ? all.slice(seedEventCount) : all;
1030
+ const link = options.runtime.subagents.getByChild(childId);
1031
+ const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
1032
+ let owned = subagentOwnedEvents(link, all);
719
1033
  let text = lastAssistantBodyText(owned);
720
1034
  let schemaValid;
721
1035
  let schemaErrors;
722
- if (outputSchema && text) {
723
- let checked = validateOutputAgainstSchema(text, outputSchema);
1036
+ if (outputSchema) {
1037
+ // An empty body cannot satisfy the contract either: a silent skip
1038
+ // would hand the parent `ok: true` with an unvalidated (missing)
1039
+ // answer, so treat "no text" as one more schema failure.
1040
+ let checked = text
1041
+ ? validateOutputAgainstSchema(text, outputSchema)
1042
+ : { valid: false, errors: ["child produced no assistant text"] };
724
1043
  if (!checked.valid) {
725
1044
  const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
726
1045
  sessionId: childId,
@@ -739,18 +1058,20 @@ function createSubagentTool(options) {
739
1058
  catch {
740
1059
  /* keep first answer */
741
1060
  }
742
- owned = seedEventCount > 0
743
- ? readSessionEvents(options.runtime.store, childId).slice(seedEventCount)
744
- : readSessionEvents(options.runtime.store, childId);
1061
+ owned = readOwned();
745
1062
  text = lastAssistantBodyText(owned) || text;
746
- checked = validateOutputAgainstSchema(text, outputSchema);
1063
+ checked = text
1064
+ ? validateOutputAgainstSchema(text, outputSchema)
1065
+ : { valid: false, errors: ["child produced no assistant text"] };
747
1066
  }
748
1067
  }
749
1068
  schemaValid = checked.valid;
750
1069
  schemaErrors = checked.errors;
751
1070
  }
752
1071
  options.runtime.agentTeamTasks.complete(task.id, {
753
- ok: true,
1072
+ // An unvalidated answer is not a successful delegation: the task
1073
+ // board must not read `completed` when the contract was skipped.
1074
+ ok: schemaValid !== false,
754
1075
  ...(text ? { preview: text } : {}),
755
1076
  ...(schemaValid !== undefined ? { schemaValid } : {}),
756
1077
  ...(schemaErrors && schemaErrors.length > 0
@@ -786,7 +1107,8 @@ function createSubagentTool(options) {
786
1107
  function createListAgentsTool(options) {
787
1108
  return {
788
1109
  name: "list_agents",
789
- description: "List direct child subagents of this session (id, label, mode, running, inbox queue/steer). " +
1110
+ description: "List direct child subagents of this session (id, label, mode, running, model, inbox queue/steer, " +
1111
+ "and how each child's last turn actually ended — finished vs aborted by parent/user vs error, with idle age). " +
790
1112
  "Prefaces with depth/active quota. Prefer `analytics` for a fuller quota snapshot.",
791
1113
  parameters: { type: "object", properties: {} },
792
1114
  isConcurrencySafe: () => true,
@@ -819,7 +1141,14 @@ function createListAgentsTool(options) {
819
1141
  const inbox = id !== "?"
820
1142
  ? childInboxCounts(options.runtime, id)
821
1143
  : { queued: 0, steering: 0 };
822
- return `${id}\t${label}\t${mode}\t${activity}\tq=${inbox.queued}\tsteer=${inbox.steering}`;
1144
+ const route = e.model ? `\t${e.model}` : "";
1145
+ // `idle` alone reads the same for a child that finished and one the
1146
+ // parent's wait budget cut off; the outcome line is what separates
1147
+ // "delivered" from "killed".
1148
+ const outcome = id === "?" || e.kind === "diagnostic"
1149
+ ? ""
1150
+ : `\t${formatChildOutcome(readSessionEvents(options.runtime.store, id))}`;
1151
+ return `${id}\t${label}\t${mode}\t${activity}${route}${outcome}\tq=${inbox.queued}\tsteer=${inbox.steering}`;
823
1152
  });
824
1153
  return { content: [header, ...lines].join("\n") };
825
1154
  },
@@ -873,7 +1202,8 @@ function createSendMessageTool(options) {
873
1202
  isError: true,
874
1203
  };
875
1204
  }
876
- options.runtime.agentTeamTasks.markResumed(childId);
1205
+ // Task-board resume is owned by `subagent.prompt` (the SoT for every
1206
+ // delivery route); calling it again here double-bumped the revision.
877
1207
  return { content: `sent to ${childId} (${delivery})` };
878
1208
  },
879
1209
  };
@@ -1032,7 +1362,6 @@ function createTeamGraphTool(options) {
1032
1362
  results.push(`${peer.id}\tfail\t${prompted.result.error.message}`);
1033
1363
  continue;
1034
1364
  }
1035
- options.runtime.agentTeamTasks.markResumed(peer.id);
1036
1365
  results.push(`${peer.id}\tok\t${delivery}`);
1037
1366
  }
1038
1367
  return {
@@ -1110,7 +1439,6 @@ function createFollowupTaskTool(options) {
1110
1439
  isError: true,
1111
1440
  };
1112
1441
  }
1113
- options.runtime.agentTeamTasks.markResumed(childId);
1114
1442
  return { content: `followup_task sent to ${childId} (steer)` };
1115
1443
  },
1116
1444
  };
@@ -1217,10 +1545,11 @@ function createWaitAgentTool(options) {
1217
1545
  : `wait_agent completed (${timeoutMs}ms budget)`,
1218
1546
  ];
1219
1547
  for (const childId of targets) {
1548
+ const link = options.runtime.subagents.getByChild(childId);
1220
1549
  const running = isChildSessionActive(options.runtime, childId);
1221
1550
  const text = running
1222
1551
  ? undefined
1223
- : lastAssistantBodyText(readSessionEvents(options.runtime.store, childId));
1552
+ : lastAssistantBodyText(subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId)));
1224
1553
  const inbox = childInboxCounts(options.runtime, childId);
1225
1554
  lines.push([
1226
1555
  childId,
@@ -1245,7 +1574,8 @@ function createAnalyticsTool(options) {
1245
1574
  return {
1246
1575
  name: "analytics",
1247
1576
  description: "Local subagent quota and queue snapshot for this session (not cloud telemetry). " +
1248
- "Shows depth/active caps, free slots, and each child's running state plus inbox queue/steer counts.",
1577
+ "Shows depth/active caps, free slots, and each child's running state, model route, " +
1578
+ "plus inbox queue/steer counts.",
1249
1579
  parameters: { type: "object", properties: {} },
1250
1580
  isConcurrencySafe: () => true,
1251
1581
  async execute() {
@@ -1278,7 +1608,14 @@ function createAnalyticsTool(options) {
1278
1608
  ? "running"
1279
1609
  : "idle";
1280
1610
  const inbox = childInboxCounts(options.runtime, link.childSessionId);
1281
- lines.push(` ${link.childSessionId}\t${link.label ?? "subagent"}\t${link.mode}\t${activity}\tq=${inbox.queued}\tsteer=${inbox.steering}`);
1611
+ const externalKind = options.runtime.externalAgents.kind(link.childSessionId);
1612
+ const route = externalKind
1613
+ ? `external:${externalKind}`
1614
+ : (() => {
1615
+ const m = resolveSessionModelSelection(options.runtime, link.childSessionId);
1616
+ return `${m.provider}/${m.model}`;
1617
+ })();
1618
+ lines.push(` ${link.childSessionId}\t${link.label ?? "subagent"}\t${link.mode}\t${activity}\t${route}\tq=${inbox.queued}\tsteer=${inbox.steering}`);
1282
1619
  }
1283
1620
  }
1284
1621
  return { content: lines.join("\n") };