visionclaw 0.1.219-dev.codex-issue-2805-worker-runtime.3 → 0.1.220-dev.codex-fix-2941-activity-foreground.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/dist/agent/api-proxy.d.ts +16 -0
  2. package/dist/agent/api-proxy.d.ts.map +1 -1
  3. package/dist/agent/api-proxy.js +8 -9
  4. package/dist/agent/api-proxy.js.map +1 -1
  5. package/dist/agent/browser-launcher.d.ts +0 -3
  6. package/dist/agent/browser-launcher.d.ts.map +1 -1
  7. package/dist/agent/browser-launcher.js +43 -101
  8. package/dist/agent/browser-launcher.js.map +1 -1
  9. package/dist/agent/coding-conventions.d.ts +3 -2
  10. package/dist/agent/coding-conventions.d.ts.map +1 -1
  11. package/dist/agent/coding-conventions.js +8 -7
  12. package/dist/agent/coding-conventions.js.map +1 -1
  13. package/dist/agent/command-handlers.d.ts.map +1 -1
  14. package/dist/agent/command-handlers.js +16 -4
  15. package/dist/agent/command-handlers.js.map +1 -1
  16. package/dist/agent/context-action-restart-fallback.d.ts.map +1 -1
  17. package/dist/agent/context-action-restart-fallback.js +3 -9
  18. package/dist/agent/context-action-restart-fallback.js.map +1 -1
  19. package/dist/agent/context-refresh.d.ts.map +1 -1
  20. package/dist/agent/context-refresh.js +7 -0
  21. package/dist/agent/context-refresh.js.map +1 -1
  22. package/dist/agent/context.d.ts.map +1 -1
  23. package/dist/agent/context.js +8 -6
  24. package/dist/agent/context.js.map +1 -1
  25. package/dist/agent/engine-fallback.d.ts +35 -0
  26. package/dist/agent/engine-fallback.d.ts.map +1 -1
  27. package/dist/agent/engine-fallback.js +3 -4
  28. package/dist/agent/engine-fallback.js.map +1 -1
  29. package/dist/agent/engines/browser-tool-policy.d.ts +1 -12
  30. package/dist/agent/engines/browser-tool-policy.d.ts.map +1 -1
  31. package/dist/agent/engines/browser-tool-policy.js +2 -17
  32. package/dist/agent/engines/browser-tool-policy.js.map +1 -1
  33. package/dist/agent/engines/claude/session.d.ts +0 -1
  34. package/dist/agent/engines/claude/session.d.ts.map +1 -1
  35. package/dist/agent/engines/claude/session.js +36 -21
  36. package/dist/agent/engines/claude/session.js.map +1 -1
  37. package/dist/agent/engines/pi/tools.d.ts.map +1 -1
  38. package/dist/agent/engines/pi/tools.js +10 -2
  39. package/dist/agent/engines/pi/tools.js.map +1 -1
  40. package/dist/agent/engines/pi-coding/session.d.ts +0 -3
  41. package/dist/agent/engines/pi-coding/session.d.ts.map +1 -1
  42. package/dist/agent/engines/pi-coding/session.js +22 -19
  43. package/dist/agent/engines/pi-coding/session.js.map +1 -1
  44. package/dist/agent/engines/session-types.d.ts +0 -6
  45. package/dist/agent/engines/session-types.d.ts.map +1 -1
  46. package/dist/agent/external-context/runtime.d.ts.map +1 -1
  47. package/dist/agent/external-context/runtime.js +14 -15
  48. package/dist/agent/external-context/runtime.js.map +1 -1
  49. package/dist/agent/fast-responder.d.ts +20 -4
  50. package/dist/agent/fast-responder.d.ts.map +1 -1
  51. package/dist/agent/fast-responder.js +39 -9
  52. package/dist/agent/fast-responder.js.map +1 -1
  53. package/dist/agent/interrupt-handler.d.ts +18 -4
  54. package/dist/agent/interrupt-handler.d.ts.map +1 -1
  55. package/dist/agent/interrupt-handler.js +284 -49
  56. package/dist/agent/interrupt-handler.js.map +1 -1
  57. package/dist/agent/lab-run-system-prompt.d.ts.map +1 -1
  58. package/dist/agent/lab-run-system-prompt.js +0 -3
  59. package/dist/agent/lab-run-system-prompt.js.map +1 -1
  60. package/dist/agent/lifecycle-trace.d.ts.map +1 -1
  61. package/dist/agent/lifecycle-trace.js +5 -6
  62. package/dist/agent/lifecycle-trace.js.map +1 -1
  63. package/dist/agent/loop.d.ts +1 -4
  64. package/dist/agent/loop.d.ts.map +1 -1
  65. package/dist/agent/loop.js +129 -105
  66. package/dist/agent/loop.js.map +1 -1
  67. package/dist/agent/memory-protocol.d.ts +2 -2
  68. package/dist/agent/memory-protocol.d.ts.map +1 -1
  69. package/dist/agent/memory-protocol.js +15 -3
  70. package/dist/agent/memory-protocol.js.map +1 -1
  71. package/dist/agent/memory-store.d.ts.map +1 -1
  72. package/dist/agent/memory-store.js +1 -11
  73. package/dist/agent/memory-store.js.map +1 -1
  74. package/dist/agent/model-provider.d.ts +0 -2
  75. package/dist/agent/model-provider.d.ts.map +1 -1
  76. package/dist/agent/model-provider.js +25 -31
  77. package/dist/agent/model-provider.js.map +1 -1
  78. package/dist/agent/product-identity.d.ts +4 -5
  79. package/dist/agent/product-identity.d.ts.map +1 -1
  80. package/dist/agent/product-identity.js +6 -7
  81. package/dist/agent/product-identity.js.map +1 -1
  82. package/dist/agent/public-activity.d.ts +1 -1
  83. package/dist/agent/public-activity.d.ts.map +1 -1
  84. package/dist/agent/public-activity.js.map +1 -1
  85. package/dist/agent/reply-watchdog.d.ts +3 -5
  86. package/dist/agent/reply-watchdog.d.ts.map +1 -1
  87. package/dist/agent/reply-watchdog.js +4 -6
  88. package/dist/agent/reply-watchdog.js.map +1 -1
  89. package/dist/agent/session-manager.d.ts +0 -5
  90. package/dist/agent/session-manager.d.ts.map +1 -1
  91. package/dist/agent/session-manager.js +0 -7
  92. package/dist/agent/session-manager.js.map +1 -1
  93. package/dist/agent/state.d.ts +0 -7
  94. package/dist/agent/state.d.ts.map +1 -1
  95. package/dist/agent/state.js +11 -21
  96. package/dist/agent/state.js.map +1 -1
  97. package/dist/agent/stream-handler.d.ts.map +1 -1
  98. package/dist/agent/stream-handler.js +17 -0
  99. package/dist/agent/stream-handler.js.map +1 -1
  100. package/dist/agent/system-prompt.d.ts.map +1 -1
  101. package/dist/agent/system-prompt.js +6 -10
  102. package/dist/agent/system-prompt.js.map +1 -1
  103. package/dist/agent/transcript/transcript-indexer.d.ts +28 -0
  104. package/dist/agent/transcript/transcript-indexer.d.ts.map +1 -1
  105. package/dist/agent/transcript/transcript-indexer.js +57 -58
  106. package/dist/agent/transcript/transcript-indexer.js.map +1 -1
  107. package/dist/agent/usage/usage-report-queue.d.ts.map +1 -1
  108. package/dist/agent/usage/usage-report-queue.js +52 -53
  109. package/dist/agent/usage/usage-report-queue.js.map +1 -1
  110. package/dist/agent/wake-cycle-runner.d.ts +8 -7
  111. package/dist/agent/wake-cycle-runner.d.ts.map +1 -1
  112. package/dist/agent/wake-cycle-runner.js +66 -105
  113. package/dist/agent/wake-cycle-runner.js.map +1 -1
  114. package/dist/builtin-skills/owner-manual/SKILL.md +2 -2
  115. package/dist/calendar/occurrence-ownership.d.ts.map +1 -1
  116. package/dist/calendar/occurrence-ownership.js +5 -6
  117. package/dist/calendar/occurrence-ownership.js.map +1 -1
  118. package/dist/calendar/scheduler.d.ts.map +1 -1
  119. package/dist/calendar/scheduler.js +3 -4
  120. package/dist/calendar/scheduler.js.map +1 -1
  121. package/dist/channels/interface.d.ts +2 -8
  122. package/dist/channels/interface.d.ts.map +1 -1
  123. package/dist/channels/interface.js +1 -8
  124. package/dist/channels/interface.js.map +1 -1
  125. package/dist/channels/manager.d.ts.map +1 -1
  126. package/dist/channels/manager.js +0 -2
  127. package/dist/channels/manager.js.map +1 -1
  128. package/dist/channels/qq-status.d.ts.map +1 -1
  129. package/dist/channels/qq-status.js +1 -0
  130. package/dist/channels/qq-status.js.map +1 -1
  131. package/dist/channels/queue.d.ts.map +1 -1
  132. package/dist/channels/queue.js +0 -8
  133. package/dist/channels/queue.js.map +1 -1
  134. package/dist/channels/telegram-status.d.ts.map +1 -1
  135. package/dist/channels/telegram-status.js +2 -0
  136. package/dist/channels/telegram-status.js.map +1 -1
  137. package/dist/channels/tui.d.ts +2 -2
  138. package/dist/channels/tui.d.ts.map +1 -1
  139. package/dist/channels/tui.js +2 -7
  140. package/dist/channels/tui.js.map +1 -1
  141. package/dist/config/index.d.ts.map +1 -1
  142. package/dist/config/index.js +15 -16
  143. package/dist/config/index.js.map +1 -1
  144. package/dist/config/types.d.ts +8 -16
  145. package/dist/config/types.d.ts.map +1 -1
  146. package/dist/config/types.js +24 -13
  147. package/dist/config/types.js.map +1 -1
  148. package/dist/email/email-route-store.d.ts.map +1 -1
  149. package/dist/email/email-route-store.js +5 -6
  150. package/dist/email/email-route-store.js.map +1 -1
  151. package/dist/i18n/messages.d.ts +0 -12
  152. package/dist/i18n/messages.d.ts.map +1 -1
  153. package/dist/i18n/messages.js +9 -21
  154. package/dist/i18n/messages.js.map +1 -1
  155. package/dist/logger.d.ts +0 -2
  156. package/dist/logger.d.ts.map +1 -1
  157. package/dist/logger.js +1 -19
  158. package/dist/logger.js.map +1 -1
  159. package/dist/threads/active-threads-store.d.ts.map +1 -1
  160. package/dist/threads/active-threads-store.js +27 -8
  161. package/dist/threads/active-threads-store.js.map +1 -1
  162. package/dist/threads/execution-host.d.ts.map +1 -1
  163. package/dist/threads/execution-host.js +0 -1
  164. package/dist/threads/execution-host.js.map +1 -1
  165. package/dist/threads/execution-scheduler.d.ts +0 -4
  166. package/dist/threads/execution-scheduler.d.ts.map +1 -1
  167. package/dist/threads/execution-scheduler.js +2 -38
  168. package/dist/threads/execution-scheduler.js.map +1 -1
  169. package/dist/threads/execution-store.d.ts +5 -20
  170. package/dist/threads/execution-store.d.ts.map +1 -1
  171. package/dist/threads/execution-store.js +8 -45
  172. package/dist/threads/execution-store.js.map +1 -1
  173. package/dist/threads/foreground-task.d.ts +6 -10
  174. package/dist/threads/foreground-task.d.ts.map +1 -1
  175. package/dist/threads/foreground-task.js +26 -34
  176. package/dist/threads/foreground-task.js.map +1 -1
  177. package/dist/threads/foreman-client.d.ts.map +1 -1
  178. package/dist/threads/foreman-client.js +1 -3
  179. package/dist/threads/foreman-client.js.map +1 -1
  180. package/dist/threads/foreman-execution.d.ts +12 -12
  181. package/dist/threads/foreman-execution.d.ts.map +1 -1
  182. package/dist/threads/foreman-execution.js +19 -30
  183. package/dist/threads/foreman-execution.js.map +1 -1
  184. package/dist/threads/foreman-host.d.ts +2 -17
  185. package/dist/threads/foreman-host.d.ts.map +1 -1
  186. package/dist/threads/foreman-host.js +63 -452
  187. package/dist/threads/foreman-host.js.map +1 -1
  188. package/dist/threads/foreman-protocol.d.ts +12 -113
  189. package/dist/threads/foreman-protocol.d.ts.map +1 -1
  190. package/dist/threads/foreman-protocol.js +15 -41
  191. package/dist/threads/foreman-protocol.js.map +1 -1
  192. package/dist/threads/foreman-store.d.ts +10 -44
  193. package/dist/threads/foreman-store.d.ts.map +1 -1
  194. package/dist/threads/foreman-store.js +19 -78
  195. package/dist/threads/foreman-store.js.map +1 -1
  196. package/dist/threads/generic-execution.d.ts +2 -2
  197. package/dist/threads/generic-execution.d.ts.map +1 -1
  198. package/dist/threads/generic-execution.js +6 -9
  199. package/dist/threads/generic-execution.js.map +1 -1
  200. package/dist/threads/ipc.d.ts +0 -3
  201. package/dist/threads/ipc.d.ts.map +1 -1
  202. package/dist/threads/ipc.js.map +1 -1
  203. package/dist/threads/relay-adapter.d.ts +2 -0
  204. package/dist/threads/relay-adapter.d.ts.map +1 -1
  205. package/dist/threads/relay-adapter.js +7 -1
  206. package/dist/threads/relay-adapter.js.map +1 -1
  207. package/dist/threads/supervisor.d.ts +51 -21
  208. package/dist/threads/supervisor.d.ts.map +1 -1
  209. package/dist/threads/supervisor.js +449 -160
  210. package/dist/threads/supervisor.js.map +1 -1
  211. package/dist/threads/task-contracts.d.ts +194 -0
  212. package/dist/threads/task-contracts.d.ts.map +1 -0
  213. package/dist/threads/task-contracts.js +82 -0
  214. package/dist/threads/task-contracts.js.map +1 -0
  215. package/dist/threads/task-sidecar.d.ts +145 -0
  216. package/dist/threads/task-sidecar.d.ts.map +1 -0
  217. package/dist/threads/task-sidecar.js +311 -0
  218. package/dist/threads/task-sidecar.js.map +1 -0
  219. package/dist/threads/task-store.d.ts +172 -0
  220. package/dist/threads/task-store.d.ts.map +1 -0
  221. package/dist/threads/task-store.js +599 -0
  222. package/dist/threads/task-store.js.map +1 -0
  223. package/dist/threads/thread-activity-controller.d.ts +7 -0
  224. package/dist/threads/thread-activity-controller.d.ts.map +1 -1
  225. package/dist/threads/thread-activity-controller.js +52 -2
  226. package/dist/threads/thread-activity-controller.js.map +1 -1
  227. package/dist/threads/thread-task-context.d.ts +40 -0
  228. package/dist/threads/thread-task-context.d.ts.map +1 -0
  229. package/dist/threads/thread-task-context.js +160 -0
  230. package/dist/threads/thread-task-context.js.map +1 -0
  231. package/dist/threads/thread-task-controller.d.ts +122 -0
  232. package/dist/threads/thread-task-controller.d.ts.map +1 -0
  233. package/dist/threads/thread-task-controller.js +276 -0
  234. package/dist/threads/thread-task-controller.js.map +1 -0
  235. package/dist/threads/thread-task-runtime.d.ts +350 -0
  236. package/dist/threads/thread-task-runtime.d.ts.map +1 -0
  237. package/dist/threads/thread-task-runtime.js +896 -0
  238. package/dist/threads/thread-task-runtime.js.map +1 -0
  239. package/dist/threads/worker-entry.d.ts.map +1 -1
  240. package/dist/threads/worker-entry.js +28 -29
  241. package/dist/threads/worker-entry.js.map +1 -1
  242. package/dist/threads/worker-execution-runtime.d.ts +2 -3
  243. package/dist/threads/worker-execution-runtime.d.ts.map +1 -1
  244. package/dist/threads/worker-execution-runtime.js +2 -5
  245. package/dist/threads/worker-execution-runtime.js.map +1 -1
  246. package/dist/tools/delegation.d.ts +2 -75
  247. package/dist/tools/delegation.d.ts.map +1 -1
  248. package/dist/tools/delegation.js +29 -63
  249. package/dist/tools/delegation.js.map +1 -1
  250. package/dist/tools/email.d.ts +1 -1
  251. package/dist/tools/execution-control.d.ts +0 -5
  252. package/dist/tools/execution-control.d.ts.map +1 -1
  253. package/dist/tools/execution-control.js +2 -9
  254. package/dist/tools/execution-control.js.map +1 -1
  255. package/dist/tools/index.d.ts.map +1 -1
  256. package/dist/tools/index.js +6 -2
  257. package/dist/tools/index.js.map +1 -1
  258. package/dist/tools/mcp-manager.d.ts +1 -1
  259. package/dist/tools/memory.d.ts +1 -1
  260. package/dist/tools/notify.d.ts +0 -2
  261. package/dist/tools/notify.d.ts.map +1 -1
  262. package/dist/tools/notify.js +5 -21
  263. package/dist/tools/notify.js.map +1 -1
  264. package/dist/tools/skill.d.ts +1 -1
  265. package/dist/tools/thread-tasks.d.ts +23 -0
  266. package/dist/tools/thread-tasks.d.ts.map +1 -0
  267. package/dist/tools/thread-tasks.js +122 -0
  268. package/dist/tools/thread-tasks.js.map +1 -0
  269. package/dist/tools/totp.d.ts +1 -1
  270. package/dist/tui/request-store.d.ts +1 -1
  271. package/dist/tui/request-store.d.ts.map +1 -1
  272. package/dist/tui/request-store.js +2 -2
  273. package/dist/tui/request-store.js.map +1 -1
  274. package/dist/tui/server.d.ts.map +1 -1
  275. package/dist/tui/server.js +2 -17
  276. package/dist/tui/server.js.map +1 -1
  277. package/dist/utils/restart.d.ts.map +1 -1
  278. package/dist/utils/restart.js +0 -6
  279. package/dist/utils/restart.js.map +1 -1
  280. package/dist-agent/bundle.cjs +2 -2
  281. package/package.json +1 -1
  282. package/dist/agent/runtime-context.d.ts +0 -27
  283. package/dist/agent/runtime-context.d.ts.map +0 -1
  284. package/dist/agent/runtime-context.js +0 -60
  285. package/dist/agent/runtime-context.js.map +0 -1
  286. package/dist/agent/runtime-disclosure-policy.d.ts +0 -6
  287. package/dist/agent/runtime-disclosure-policy.d.ts.map +0 -1
  288. package/dist/agent/runtime-disclosure-policy.js +0 -16
  289. package/dist/agent/runtime-disclosure-policy.js.map +0 -1
  290. package/dist/conversations/lazy-runtime.d.ts +0 -5
  291. package/dist/conversations/lazy-runtime.d.ts.map +0 -1
  292. package/dist/conversations/lazy-runtime.js +0 -12
  293. package/dist/conversations/lazy-runtime.js.map +0 -1
  294. package/dist/conversations/runtime.d.ts +0 -17
  295. package/dist/conversations/runtime.d.ts.map +0 -1
  296. package/dist/conversations/runtime.js +0 -92
  297. package/dist/conversations/runtime.js.map +0 -1
  298. package/dist/conversations/scheduler.d.ts +0 -57
  299. package/dist/conversations/scheduler.d.ts.map +0 -1
  300. package/dist/conversations/scheduler.js +0 -175
  301. package/dist/conversations/scheduler.js.map +0 -1
  302. package/dist/threads/foreman-business.d.ts +0 -9
  303. package/dist/threads/foreman-business.d.ts.map +0 -1
  304. package/dist/threads/foreman-business.js +0 -117
  305. package/dist/threads/foreman-business.js.map +0 -1
  306. package/dist/threads/foreman-delivery.d.ts +0 -5
  307. package/dist/threads/foreman-delivery.d.ts.map +0 -1
  308. package/dist/threads/foreman-delivery.js +0 -21
  309. package/dist/threads/foreman-delivery.js.map +0 -1
  310. package/dist/threads/task-workspace.d.ts +0 -8
  311. package/dist/threads/task-workspace.d.ts.map +0 -1
  312. package/dist/threads/task-workspace.js +0 -52
  313. package/dist/threads/task-workspace.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { DEFAULT_THREAD_ID } from "../channels/interface.js";
3
3
  import { isDeliveryTarget } from "../channels/delivery-target.js";
4
4
  import { taskContinuationRouting } from "../agent/reply-target.js";
@@ -6,10 +6,7 @@ import { resolveWorkspaceCwd } from "../agent/workspace-cwd.js";
6
6
  import { t } from "../i18n/index.js";
7
7
  import { logger } from "../logger.js";
8
8
  import { ExecutionCommandConflictError, isTerminalExecutionState, } from "./execution-store.js";
9
- import { DEFAULT_LAB_RUN_BUDGET } from "./execution-snapshot.js";
10
- import { prepareTaskWorkspace } from "./task-workspace.js";
11
- import { publicTaskText, taskDeliveryMode } from "./foreman-delivery.js";
12
- import { CancelArgsSchema, DelegateArgsSchema, DelegateBatchArgsSchema, FOREMAN_EVENT_META_KEY, FOREMAN_SCOPE, InputArgsSchema, StatusArgsSchema, } from "./foreman-protocol.js";
9
+ import { CancelArgsSchema, DelegateArgsSchema, FOREMAN_EVENT_META_KEY, FOREMAN_SCOPE, InputArgsSchema, StatusArgsSchema, } from "./foreman-protocol.js";
13
10
  /**
14
11
  * The Foreman host (#2805 stage 3), in the main process.
15
12
  *
@@ -42,23 +39,6 @@ function textOf(value, fallback = "") {
42
39
  function clip(text, max) {
43
40
  return text.length <= max ? text : `${text.slice(0, max)}…`;
44
41
  }
45
- function identity(...parts) {
46
- return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
47
- }
48
- function taskIdFor(conversation, dispatch, key) {
49
- return `task-${identity(conversation, dispatch, key)}`;
50
- }
51
- function remainingBudget(limits, attempts) {
52
- const used = (field) => attempts.reduce((sum, attempt) => {
53
- const value = (attempt.usage ?? attempt.result?.usage)?.[field];
54
- return sum + (typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0);
55
- }, 0);
56
- return {
57
- maxModelAttempts: limits.maxModelAttempts - used("modelAttempts"),
58
- maxToolCalls: limits.maxToolCalls - used("toolCalls"),
59
- maxDurationMs: limits.maxDurationMs - used("elapsedMs"),
60
- };
61
- }
62
42
  function describeState(record) {
63
43
  switch (record.state) {
64
44
  case "queued": return "queued, waiting for a worker slot";
@@ -77,13 +57,10 @@ function fallbackKey(obligation) {
77
57
  return "foreman.fallback.succeeded";
78
58
  if (obligation.status === "cancelled")
79
59
  return "foreman.fallback.cancelled";
80
- if (obligation.status === "partial")
81
- return "foreman.fallback.partial";
82
60
  return "foreman.fallback.failed";
83
61
  }
84
62
  export class ForemanHost {
85
63
  deps;
86
- pendingBatches = new Map();
87
64
  timer = null;
88
65
  sweeping = false;
89
66
  now;
@@ -104,33 +81,13 @@ export class ForemanHost {
104
81
  clearInterval(this.timer);
105
82
  this.timer = null;
106
83
  }
107
- /** The supervisor supplies executor identity; model arguments never select an account. */
108
- async readBusiness(executionId, tool, args, workspace) {
109
- const record = this.deps.executions.get(executionId);
110
- const dispatch = record ? this.ownedDispatch(record.caller.ref, record.taskId) : null;
111
- if (record?.caller.kind !== "general" || record.state !== "running" || !record.payload.businessRead || !dispatch?.metadata.businessRead
112
- || !this.deps.canReadBusiness?.(dispatch.threadId, dispatch.replyTarget) || !this.deps.readBusiness) {
113
- return fail("This execution is not authorized to read the owner's workspace");
114
- }
115
- if (record.payload.workspaceMode === "read_only" && tool === "manage_drive"
116
- && typeof args === "object" && args !== null && "action" in args && args.action === "download_file") {
117
- return fail("A read-only Task cannot download files into its workspace");
118
- }
119
- const stillAuthorized = () => {
120
- const current = this.deps.executions.get(executionId);
121
- return current?.state === "running" && current.workerInstanceId === record.workerInstanceId
122
- && this.deps.canReadBusiness?.(dispatch.threadId, dispatch.replyTarget) === true;
123
- };
124
- const result = await this.deps.readBusiness(tool, args, workspace, stillAuthorized);
125
- return stillAuthorized() ? result : fail("The execution stopped or its business-read permission was revoked");
126
- }
127
- async handle(threadId, command) {
84
+ handle(threadId, command) {
128
85
  try {
129
- return await this.run(threadId, command);
86
+ return Promise.resolve(this.run(threadId, command));
130
87
  }
131
88
  catch (error) {
132
89
  logger.warn(`[foreman] ${command.type} from thread=${threadId} failed: ${errorMessage(error)}`);
133
- return fail(errorMessage(error));
90
+ return Promise.resolve(fail(errorMessage(error)));
134
91
  }
135
92
  }
136
93
  // ----------------------------------------------------------- delivery
@@ -147,25 +104,11 @@ export class ForemanHost {
147
104
  return Promise.resolve(true);
148
105
  const threadId = caller.ref;
149
106
  const dispatch = this.ownedDispatch(threadId, event.taskId);
150
- if (dispatch?.metadata.parentTaskId && event.type === "result") {
151
- return this.deliverParentResult(threadId, dispatch.metadata.parentTaskId);
152
- }
153
- // A retried Task may still have an older result/question in the outbox.
154
- // A transport replay must not revive that superseded attempt's delivery.
155
- const latest = dispatch && !dispatch.metadata.children?.length ? this.latest(dispatch.taskId) : null;
156
- if (latest && latest.executionId !== event.executionId)
157
- return Promise.resolve(true);
158
- if (event.type === "result" && this.deps.store.isUserCancelled(event.taskId))
159
- return Promise.resolve(true);
160
107
  const userCancelled = event.type === "result" && event.payload.status === "cancelled"
161
108
  && (event.payload.userInitiated === true || this.deps.store.isUserCancelled(event.taskId));
162
- const publicText = publicTaskText(event);
163
- const direct = Boolean(dispatch?.replyTarget && !userCancelled && publicText !== null && taskDeliveryMode(event) === "direct"
164
- && !dispatch.metadata.children?.length);
165
109
  // Queue first, then open the obligation: both are idempotent per event, so
166
110
  // a crash in between is repaired by the outbox's redelivery.
167
- if (!direct)
168
- this.deps.enqueueToGeneral(threadId, this.eventMessage(event, threadId, dispatch, dispatch !== null && !userCancelled));
111
+ this.deps.enqueueToGeneral(threadId, this.eventMessage(event, threadId, dispatch, dispatch !== null && !userCancelled));
169
112
  if (dispatch) {
170
113
  this.deps.store.open({
171
114
  taskId: event.taskId,
@@ -174,36 +117,23 @@ export class ForemanHost {
174
117
  kind: event.type,
175
118
  status: event.type === "question" ? "question" : String(event.payload.status),
176
119
  userInitiated: userCancelled,
177
- publicText,
178
- deliveryMode: direct ? "direct" : "review",
179
120
  });
180
121
  }
181
- return direct ? this.sweep().then(() => true) : Promise.resolve(true);
122
+ return Promise.resolve(true);
182
123
  }
183
- /** Deliver the public event content for every obligation that is due. */
124
+ /** Send the fixed notice for every obligation that is due. Safe to call at any time. */
184
125
  async sweep() {
185
126
  if (this.sweeping)
186
127
  return;
187
128
  this.sweeping = true;
188
129
  try {
189
- for (const cancel of this.deps.store.groupCancels()) {
190
- const parent = this.deps.store.dispatch(cancel.taskId);
191
- if (parent)
192
- this.cancelChildren(parent, cancel.reason, cancel.byUser);
193
- }
194
130
  for (const obligation of this.deps.store.due()) {
195
131
  const dispatch = this.deps.store.dispatch(obligation.taskId);
196
132
  if (!dispatch?.replyTarget) {
197
133
  this.deps.store.fallbackFailed(obligation.taskId, obligation.eventId, "No reply target was recorded for this task");
198
134
  continue;
199
135
  }
200
- const legacyEvent = obligation.publicText === null
201
- ? this.allEvents(obligation.executionId).find((event) => event.eventId === obligation.eventId) : undefined;
202
- const publicText = obligation.publicText ?? (legacyEvent ? publicTaskText(legacyEvent) : null);
203
- const text = obligation.deliveryMode === "direct" && publicText ? `${dispatch.title}\n\n${publicText}`
204
- : obligation.deliveryMode === "review" && obligation.kind === "result"
205
- ? t("foreman.fallback.review", this.deps.locale(), dispatch.title)
206
- : [t(fallbackKey(obligation), this.deps.locale(), dispatch.title), publicText].filter(Boolean).join("\n\n");
136
+ const text = t(fallbackKey(obligation), this.deps.locale(), dispatch.title);
207
137
  let result;
208
138
  try {
209
139
  result = await this.deps.deliver(dispatch.replyTarget, text, dispatch.threadId);
@@ -212,11 +142,8 @@ export class ForemanHost {
212
142
  result = { ok: false, errorMessage: errorMessage(error) };
213
143
  }
214
144
  if (result.ok) {
215
- if (obligation.deliveryMode === "direct")
216
- this.deps.store.markDelivered(obligation.taskId, obligation.kind === "result" ? "completion" : undefined, obligation.eventId);
217
- else
218
- this.deps.store.fallbackSent(obligation.taskId, obligation.eventId);
219
- logger.system(`[foreman] Sent ${obligation.deliveryMode === "direct" ? "direct delivery" : "fallback notice"} for task=${obligation.taskId} event=${obligation.eventId} (${obligation.kind})`);
145
+ this.deps.store.fallbackSent(obligation.taskId, obligation.eventId);
146
+ logger.system(`[foreman] Sent the delivery fallback for task=${obligation.taskId} event=${obligation.eventId} (${obligation.kind})`);
220
147
  continue;
221
148
  }
222
149
  const after = this.deps.store.fallbackFailed(obligation.taskId, obligation.eventId, result.errorMessage);
@@ -237,25 +164,11 @@ export class ForemanHost {
237
164
  // ----------------------------------------------------------- commands
238
165
  run(threadId, command) {
239
166
  switch (command.type) {
240
- case "business_read": return fail("Business reads require an admitted execution connection");
241
- case "delegate": return this.deps.allowDelegation?.() === false ? fail("New Task delegation is disabled") : this.delegate(threadId, command);
242
- case "delegate_batch": return this.deps.allowDelegation?.() === false ? fail("New Task delegation is disabled") : this.delegateBatch(threadId, command);
167
+ case "delegate": return this.delegate(threadId, command);
243
168
  case "status": return this.status(threadId, command.args);
244
169
  case "input": return this.input(threadId, command.args);
245
170
  case "cancel": return this.cancel(threadId, command.args);
246
- case "cancel_all": {
247
- const roots = this.deps.store.allForThread(threadId).filter((task) => !task.metadata.parentTaskId && this.isOpen(task));
248
- const results = roots.map((task) => this.cancel(threadId, { task_id: task.taskId, requested_by_user: true, reason: "User stopped this conversation's tasks" }));
249
- return {
250
- ok: results.every((result) => result.ok),
251
- text: roots.length > 0 ? [
252
- t("foreman.cancel_all", this.deps.locale(), String(roots.length)),
253
- ...roots.slice(0, 10).map((task) => `- ${task.title}`),
254
- ...results.filter((result) => !result.ok).map((result) => result.text),
255
- ].join("\n") : "",
256
- };
257
- }
258
- case "delivered": return this.delivered(threadId, command.taskId, command.purpose, command.eventId);
171
+ case "delivered": return this.delivered(threadId, command.taskId, command.purpose);
259
172
  case "turn_ended": return this.turnEnded(threadId, command.events);
260
173
  case "summary": return { ok: true, text: this.summary(threadId) };
261
174
  }
@@ -271,177 +184,15 @@ export class ForemanHost {
271
184
  latest(taskId) {
272
185
  return this.deps.executions.latestForTask(taskId, FOREMAN_SCOPE);
273
186
  }
274
- childRecords(parent) {
275
- return (parent.metadata.children ?? []).map((taskId) => ({ taskId, record: this.latest(taskId) }));
276
- }
277
- parentState(parent) {
278
- const children = this.childRecords(parent);
279
- if (children.every((child) => child.record === null))
280
- return "not_started";
281
- if (children.some((child) => child.record === null || !isTerminalExecutionState(child.record.state))) {
282
- if (children.some((child) => child.record?.state === "running"))
283
- return "running";
284
- if (children.some((child) => child.record?.state === "cancel_requested"))
285
- return "cancel_requested";
286
- if (children.some((child) => child.record?.state === "waiting_input"))
287
- return "waiting_input";
288
- return "queued";
289
- }
290
- if (children.every((child) => child.record?.state === "succeeded"))
291
- return "succeeded";
292
- if (children.every((child) => child.record?.state === "cancelled") || this.deps.store.groupCancels().some((item) => item.taskId === parent.taskId))
293
- return "cancelled";
294
- if (children.some((child) => child.record?.state === "succeeded"))
295
- return "partial";
296
- return "failed";
297
- }
298
- deliverParentResult(threadId, taskId) {
299
- const parent = this.ownedDispatch(threadId, taskId);
300
- if (!parent?.metadata.children?.length)
301
- return Promise.resolve(false);
302
- const children = this.childRecords(parent);
303
- if (children.some((child) => !child.record || !isTerminalExecutionState(child.record.state)))
304
- return Promise.resolve(true);
305
- const records = children.map((child) => child.record).filter((record) => record !== null);
306
- // The set of attempts is the result version. A late event from an old
307
- // attempt cannot create a second delivery for the current result version.
308
- const eventId = `evt-task-${identity(taskId, ...records.map((record) => record.executionId))}`;
309
- const status = this.parentState(parent);
310
- const summary = records.map((record) => {
311
- const childId = record.taskId;
312
- const title = this.deps.store.dispatch(childId)?.title ?? childId;
313
- return `${childId} "${title}": ${record.state}. ${clip(record.result?.summary ?? record.result?.error?.message ?? "", 400)}`;
314
- }).join("\n");
315
- const event = {
316
- protocolVersion: 1, eventId, seq: 0, taskId, executionId: taskId, type: "result",
317
- createdAt: records.map((record) => record.updatedAt).sort().at(-1) ?? this.now().toISOString(),
318
- payload: {
319
- status, summary,
320
- data: { userMessage: children.map(({ taskId: childId, record }) => {
321
- const message = publicTaskText({ type: "result", payload: { data: record?.result?.data } });
322
- const title = this.deps.store.dispatch(childId)?.title ?? childId;
323
- return message ? `${title}\n${message}` : null;
324
- }).filter(Boolean).join("\n\n") },
325
- artifactRefs: records.flatMap((record) => record.result?.artifactRefs ?? []).slice(0, 30),
326
- userInitiated: status === "cancelled" && this.deps.store.isUserCancelled(taskId),
327
- },
328
- };
329
- return this.deliverEvent(event, { kind: "general", ref: threadId });
330
- }
331
- cancelChildren(parent, reason, byUser) {
332
- for (const { taskId, record } of this.childRecords(parent)) {
333
- if (!record || isTerminalExecutionState(record.state))
334
- continue;
335
- if (byUser)
336
- this.deps.store.markUserCancelled(taskId);
337
- this.deps.store.withdraw(taskId);
338
- this.deps.executions.cancel(this.envelope(parent.threadId, `cancel-${identity(parent.taskId, record.executionId, reason, String(byUser))}`), {
339
- executionId: record.executionId, reason, userInitiated: byUser,
340
- });
341
- }
342
- }
343
- delegateBatch(threadId, command) {
344
- const parsed = DelegateBatchArgsSchema.safeParse(command.args);
345
- if (!parsed.success)
346
- return Promise.resolve(invalid(parsed.error));
347
- const key = identity(threadId, command.dispatchKey, parsed.data.title);
348
- const specification = JSON.stringify(parsed.data);
349
- const pending = this.pendingBatches.get(key);
350
- if (pending)
351
- return pending.specification === specification ? pending.result : Promise.resolve(fail("Batch identity is already being prepared with different content"));
352
- const result = this.prepareBatch(threadId, command);
353
- this.pendingBatches.set(key, { specification, result });
354
- void result.finally(() => { this.pendingBatches.delete(key); }).catch(() => { });
355
- return result;
356
- }
357
- async prepareBatch(threadId, command) {
358
- const parsed = DelegateBatchArgsSchema.safeParse(command.args);
359
- if (!parsed.success)
360
- return invalid(parsed.error);
361
- const args = parsed.data;
362
- if (new Set(args.tasks.map((task) => task.key)).size !== args.tasks.length)
363
- return fail("Task keys must be unique within a batch");
364
- const atomic = this.deps.executions.atomic;
365
- if (!atomic)
366
- return fail("This execution host does not support atomic task batches");
367
- const rootId = taskIdFor(threadId, command.dispatchKey, `batch:${args.title}`);
368
- const specification = identity(JSON.stringify(args));
369
- const previous = this.ownedDispatch(threadId, rootId);
370
- if (previous && previous.metadata.specification !== specification)
371
- return fail("This batch key was already used with different tasks; use a distinct title for new work");
372
- const ids = args.tasks.map((task) => taskIdFor(threadId, command.dispatchKey, `${rootId}:${task.key}`));
373
- const commits = new Map();
374
- // Filesystem preparation happens before the synchronous acceptance transaction.
375
- // No worker starts until every Task and command has been committed.
376
- const workspaces = [];
377
- const contexts = [];
378
- for (const [index, task] of args.tasks.entries()) {
379
- const existing = this.ownedDispatch(threadId, ids[index]);
380
- if (existing?.metadata.specification && existing.metadata.specification !== identity(specification, task.key))
381
- throw new Error(`Task key ${task.key} was already used with different content`);
382
- const source = task.context_task_id ? this.ownedDispatch(threadId, task.context_task_id) : null;
383
- const sourceRecord = source ? (existing?.metadata.contextExecutionId
384
- ? this.deps.executions.get(existing.metadata.contextExecutionId) : this.latest(source.taskId)) : null;
385
- if (task.context_task_id && (!sourceRecord || sourceRecord.taskId !== source?.taskId || !isTerminalExecutionState(sourceRecord.state))) {
386
- return fail("context_task_id must name a completed execution Task from this conversation");
387
- }
388
- contexts.push(sourceRecord?.executionId);
389
- const directory = task.project_dir ?? sourceRecord?.payload.workspaceRef ?? source?.workspace;
390
- workspaces.push(existing?.workspace ?? (directory
391
- ? await prepareTaskWorkspace(directory, ids[index], task.workspace_mode, commits)
392
- : undefined));
393
- }
394
- const parent = args.result_mode === "combined" && args.tasks.length > 1;
395
- const foreground = this.ownedDispatch(threadId, ids[0])?.metadata.foregroundCalls
396
- ?? Math.max(0, Math.trunc(command.foreground.toolCallsUsed));
397
- const budget = DEFAULT_LAB_RUN_BUDGET;
398
- return atomic(() => this.deps.store.atomic(() => {
399
- if (parent)
400
- this.deps.store.recordDispatch({
401
- taskId: rootId, threadId, title: args.title, replyTarget: isDeliveryTarget(command.replyTarget)
402
- ? command.replyTarget : threadId === DEFAULT_THREAD_ID ? this.deps.ownerTarget() : null,
403
- sender: command.sender ?? "", metadata: { children: ids, specification, sourceKey: command.dispatchKey },
404
- });
405
- for (const [index, task] of args.tasks.entries()) {
406
- const result = this.delegate(threadId, {
407
- type: "delegate", args: { ...task, project_dir: workspaces[index], workspace_mode: task.workspace_mode === "isolated" ? "shared" : task.workspace_mode },
408
- foreground: { taskId: ids[index], toolCallsUsed: index === 0 ? foreground : 0 },
409
- ...(parent ? { parentTaskId: rootId } : {}),
410
- preparedWorkspace: Boolean(workspaces[index] && task.workspace_mode === "isolated"),
411
- contextExecutionId: contexts[index],
412
- specification: identity(specification, task.key),
413
- sourceKey: command.dispatchKey,
414
- budget, replyTarget: command.replyTarget, sender: command.sender,
415
- });
416
- if (!result.ok)
417
- throw new Error(result.text);
418
- }
419
- return {
420
- ok: true, acceptedTaskId: parent ? rootId : ids[0], acceptedTaskIds: ids,
421
- text: `Accepted ${ids.length} Task(s)${parent ? ` under parent ${rootId} for one combined delivery` : " with independent delivery"}: ${args.tasks.map((task, i) => `${task.key}=${ids[i]}`).join(", ")}. Each Task has its own budget and runs when slots are available. You may delegate more independent work, coordinate existing Tasks, or reply. Do not repeat work assigned to these Tasks.`,
422
- };
423
- }));
424
- }
425
187
  delegate(threadId, command) {
426
- if (command.args && typeof command.args === "object" && "worker" in command.args && command.args.worker !== null && command.args.worker !== undefined) {
427
- return fail("Worker aliases are no longer used. Select a completed context_task_id, or send input to the existing Task.");
428
- }
429
188
  const parsed = DelegateArgsSchema.safeParse(command.args);
430
189
  if (!parsed.success)
431
190
  return invalid(parsed.error);
432
191
  const args = parsed.data;
433
192
  let retryOf = null;
434
- const retryDispatchId = args.retry_task_id && command.dispatchKey ? identity(threadId, command.dispatchKey) : undefined;
435
- const retrySpecification = retryDispatchId ? identity(JSON.stringify(args)) : undefined;
436
193
  if (args.retry_task_id) {
437
194
  if (!this.ownedDispatch(threadId, args.retry_task_id))
438
195
  return fail(`task ${args.retry_task_id} was not delegated from this conversation`);
439
- const replay = retryDispatchId ? this.deps.executions.attemptsForTask?.(args.retry_task_id, FOREMAN_SCOPE)
440
- .find((attempt) => attempt.payload.retryDispatchId === retryDispatchId) : undefined;
441
- if (replay)
442
- return replay.payload.retrySpecification === retrySpecification
443
- ? { ok: true, acceptedTaskId: replay.taskId, text: `Retry already accepted for task ${replay.taskId}, execution ${replay.executionId}: ${describeState(replay)}.` }
444
- : fail("This retry was already accepted with different content; use background_task_input or a new user instruction");
445
196
  retryOf = this.latest(args.retry_task_id);
446
197
  if (!retryOf)
447
198
  return fail(`task ${args.retry_task_id} has no earlier run to retry`);
@@ -449,34 +200,13 @@ export class ForemanHost {
449
200
  return fail(`task ${args.retry_task_id} is still ${describeState(retryOf)}; send changes with background_task_input instead`);
450
201
  }
451
202
  }
452
- const taskId = args.retry_task_id ?? (command.dispatchKey
453
- ? taskIdFor(threadId, command.dispatchKey, args.key ?? identity(JSON.stringify(args)))
454
- : command.foreground.taskId);
455
- const oldDispatch = this.ownedDispatch(threadId, taskId);
456
- if (command.parentTaskId && !this.ownedDispatch(threadId, command.parentTaskId)?.metadata.children?.includes(taskId)) {
457
- return fail("The parent Task did not admit this child");
458
- }
459
- const previous = args.context_task_id ? this.ownedDispatch(threadId, args.context_task_id) : null;
460
- const contextExecutionId = command.contextExecutionId ?? oldDispatch?.metadata.contextExecutionId;
461
- const previousRecord = previous ? (contextExecutionId
462
- ? this.deps.executions.get(contextExecutionId) : this.latest(previous.taskId)) : null;
463
- if (args.context_task_id) {
464
- if (args.retry_task_id)
465
- return fail("Use either context_task_id for new work or retry_task_id for another attempt, not both");
466
- if (!previousRecord || previousRecord.taskId !== previous?.taskId)
467
- return fail("context_task_id must name an execution Task from this conversation");
468
- if (!isTerminalExecutionState(previousRecord.state))
469
- return fail("That Task is unfinished; use background_task_input to continue it");
470
- }
203
+ const taskId = args.retry_task_id ?? command.foreground.taskId;
204
+ const previous = args.worker ? this.deps.store.lastForWorker(threadId, args.worker) : null;
471
205
  let workspace;
472
- if (retryOf && args.workspace_mode && (retryOf.payload.workspaceMode === "read_only") !== (args.workspace_mode === "read_only")) {
473
- return fail("A retry keeps its workspace access. Delegate a new Task to change read-only versus writable access");
474
- }
475
- const workspaceMode = retryOf?.payload.workspaceMode ?? (command.preparedWorkspace ? "isolated" : args.workspace_mode ?? "shared");
476
- const requestedDir = args.project_dir ?? retryOf?.payload.workspaceRef ?? oldDispatch?.workspace ?? previousRecord?.payload.workspaceRef ?? previous?.workspace ?? undefined;
206
+ const requestedDir = args.project_dir ?? previous?.workspace ?? undefined;
477
207
  if (requestedDir) {
478
208
  try {
479
- workspace = resolveWorkspaceCwd(requestedDir, { writable: workspaceMode !== "read_only" });
209
+ workspace = resolveWorkspaceCwd(requestedDir, { writable: true });
480
210
  }
481
211
  catch (error) {
482
212
  return fail(`project_dir: ${errorMessage(error)}. Nothing was delegated.`);
@@ -493,65 +223,41 @@ export class ForemanHost {
493
223
  title: args.title,
494
224
  replyTarget,
495
225
  sender: typeof command.sender === "string" ? command.sender : "",
226
+ worker: args.worker ?? null,
496
227
  workspace: workspace ?? null,
497
- metadata: {
498
- sourceKey: command.sourceKey ?? command.dispatchKey ?? (command.parentTaskId ? this.deps.store.dispatch(command.parentTaskId)?.metadata.sourceKey : undefined),
499
- contextTaskId: previous?.taskId,
500
- contextExecutionId: previousRecord?.executionId,
501
- businessRead: this.deps.canReadBusiness?.(threadId, replyTarget) ?? false,
502
- ...(command.parentTaskId ? { parentTaskId: command.parentTaskId } : {}),
503
- budget: command.budget ?? DEFAULT_LAB_RUN_BUDGET,
504
- foregroundCalls: command.foreground.toolCallsUsed,
505
- preparedWorkspace: command.preparedWorkspace,
506
- specification: command.specification,
507
- },
508
228
  });
509
229
  const context = [
510
230
  args.context,
511
- retryOf?.result?.summary ? `Previous attempt result: ${clip(retryOf.result.summary, 4_000)}` : undefined,
512
- retryOf?.payload.contextSummary,
513
231
  dispatch.sender ? `Requested by: ${dispatch.sender}` : undefined,
514
- previous && previous.taskId !== taskId ? this.previousWork(previous, previousRecord) : undefined,
232
+ previous && previous.taskId !== taskId ? this.previousWork(previous) : undefined,
515
233
  ].filter((part) => typeof part === "string" && part.length > 0);
516
- const toolCallsUsed = retryOf ? command.foreground.toolCallsUsed : dispatch.metadata.foregroundCalls ?? command.foreground.toolCallsUsed;
234
+ const toolCallsUsed = command.foreground.toolCallsUsed;
517
235
  const payload = {
518
236
  goal: args.goal,
519
- requirements: [...new Set([...(retryOf?.payload.requirements ?? []), ...args.requirements])],
520
- ...((args.expected_output ?? retryOf?.payload.expectedOutput) ? { expectedOutput: args.expected_output ?? retryOf?.payload.expectedOutput } : {}),
237
+ requirements: args.requirements,
238
+ ...(args.expected_output ? { expectedOutput: args.expected_output } : {}),
521
239
  ...(context.length > 0 ? { contextSummary: clip(context.join("\n\n"), 20_000) } : {}),
522
240
  checkpoint: {
523
241
  done: args.done,
524
242
  remaining: args.remaining,
525
243
  decisions: [],
526
244
  blockers: [],
527
- artifactRefs: [...new Set([...(previousRecord?.result?.artifactRefs ?? []), ...args.artifacts])],
245
+ artifactRefs: args.artifacts,
528
246
  uncertainOperations: args.uncertain_operations,
529
247
  },
530
248
  title: dispatch.title,
531
- businessRead: dispatch.metadata.businessRead === true,
532
249
  foregroundToolCallsUsed: Number.isInteger(toolCallsUsed) && toolCallsUsed >= 0 ? toolCallsUsed : 0,
533
250
  ...(workspace ? { workspaceRef: workspace } : {}),
534
- workspaceMode,
535
- ...(dispatch.metadata.parentTaskId ? { parentTaskId: dispatch.metadata.parentTaskId } : {}),
536
- ...((command.preparedWorkspace || (dispatch.metadata.preparedWorkspace && workspace === dispatch.workspace)
537
- || (previousRecord?.payload.preparedWorkspace && workspace === previousRecord.payload.workspaceRef)
538
- || (retryOf?.payload.preparedWorkspace && workspace === retryOf.payload.workspaceRef)) ? { preparedWorkspace: true } : {}),
539
- ...(retryOf ? { priorExecutionId: retryOf.executionId } : {}),
540
- ...(retryDispatchId ? { retryDispatchId, retrySpecification } : {}),
541
251
  };
542
252
  // One command per handoff (or per retry of one run): a repeated call
543
253
  // returns the original acceptance instead of starting a second worker.
544
254
  const commandId = retryOf ? `retry-${taskId}-${retryOf.executionId}` : `delegate-${taskId}`;
545
- const budget = remainingBudget(dispatch.metadata.budget ?? DEFAULT_LAB_RUN_BUDGET, retryOf ? this.deps.executions.attemptsForTask?.(taskId, FOREMAN_SCOPE) ?? [retryOf] : []);
546
- if (budget.maxModelAttempts < 1 || budget.maxToolCalls <= toolCallsUsed || budget.maxDurationMs < 1)
547
- return fail(`Task ${taskId} has exhausted its cumulative budget`);
548
255
  let outcome;
549
256
  try {
550
257
  outcome = this.deps.executions.submit(this.envelope(threadId, commandId), {
551
258
  taskId,
552
259
  ...(retryOf ? { retryOf: retryOf.executionId } : {}),
553
260
  payload,
554
- budget,
555
261
  });
556
262
  }
557
263
  catch (error) {
@@ -569,11 +275,6 @@ export class ForemanHost {
569
275
  // The previous result is superseded: whatever it still owed the user moves to the new run.
570
276
  this.deps.store.withdraw(taskId);
571
277
  this.deps.store.clearUserCancelled(taskId);
572
- if (dispatch.metadata.parentTaskId) {
573
- this.deps.store.withdraw(dispatch.metadata.parentTaskId);
574
- this.deps.store.clearUserCancelled(dispatch.metadata.parentTaskId);
575
- this.deps.store.clearGroupCancel(dispatch.metadata.parentTaskId);
576
- }
577
278
  }
578
279
  logger.system(`[foreman] Delegated task=${taskId} execution=${outcome.value.executionId} from thread=${threadId}${retryOf ? ` (retry of ${retryOf.executionId})` : ""}`);
579
280
  return {
@@ -581,14 +282,15 @@ export class ForemanHost {
581
282
  acceptedTaskId: taskId,
582
283
  text: [
583
284
  `Delegated as background task ${taskId} ("${dispatch.title}"), execution ${outcome.value.executionId}, ${outcome.value.state === "queued" ? "queued: it starts when a worker slot is free" : outcome.value.state}.`,
584
- "Do not repeat this Task's work here. You may delegate more independent Tasks, coordinate existing work, or reply to the user.",
285
+ "Stop working on this task here. Tell the user it is running in the background (unless you already did), then call finish.",
585
286
  "Its question or result arrives later as a [Background task update] message.",
586
287
  ].join(" "),
587
288
  };
588
289
  }
589
- /** Explicit Task context survives process replacement without a worker identity. */
590
- previousWork(previous, record) {
591
- const lines = [`Referenced Task: ${previous.taskId} "${previous.title}" (${record ? describeState(record) : "not started"})`];
290
+ /** What the same logical worker did last time, so follow-up work starts informed. */
291
+ previousWork(previous) {
292
+ const record = this.latest(previous.taskId);
293
+ const lines = [`Previous task of this worker: ${previous.taskId} "${previous.title}" (${record ? describeState(record) : "not started"})`];
592
294
  if (record?.result?.summary)
593
295
  lines.push(`Its result: ${clip(record.result.summary, 4_000)}`);
594
296
  if (record?.result?.artifactRefs?.length)
@@ -605,9 +307,7 @@ export class ForemanHost {
605
307
  ? { ok: true, text: this.describeTask(dispatch, true) }
606
308
  : fail(`no background task ${parsed.data.task_id} in this conversation`);
607
309
  }
608
- const roots = this.deps.store.allForThread(threadId).filter((dispatch) => !dispatch.metadata.parentTaskId);
609
- const active = roots.filter((dispatch) => this.isOpen(dispatch));
610
- const dispatches = [...active, ...roots.filter((dispatch) => !this.isOpen(dispatch)).slice(0, Math.max(0, 20 - active.length))];
310
+ const dispatches = this.deps.store.dispatchesForThread(threadId, 20);
611
311
  if (dispatches.length === 0)
612
312
  return { ok: true, text: "No background tasks in this conversation." };
613
313
  return { ok: true, text: dispatches.map((dispatch) => this.describeTask(dispatch, false)).join("\n") };
@@ -616,15 +316,9 @@ export class ForemanHost {
616
316
  const parsed = InputArgsSchema.safeParse(args);
617
317
  if (!parsed.success)
618
318
  return invalid(parsed.error);
619
- const { task_id: taskId, text, source_task_id: sourceTaskId, source_event_id: sourceEventId } = parsed.data;
620
- if (Boolean(sourceTaskId) !== Boolean(sourceEventId)) {
621
- return fail("source_task_id and source_event_id must be provided together when forwarding a completed result");
622
- }
623
- const dispatch = this.ownedDispatch(threadId, taskId);
624
- if (!dispatch)
319
+ const { task_id: taskId, text } = parsed.data;
320
+ if (!this.ownedDispatch(threadId, taskId))
625
321
  return fail(`no background task ${taskId} in this conversation`);
626
- if (dispatch.metadata.children?.length)
627
- return fail(`Send the answer or requirement to a child Task: ${dispatch.metadata.children.join(", ")}. Each child owns its input cursor.`);
628
322
  const record = this.latest(taskId);
629
323
  if (!record)
630
324
  return fail(`task ${taskId} has not started`);
@@ -633,15 +327,6 @@ export class ForemanHost {
633
327
  }
634
328
  if (record.state === "cancel_requested")
635
329
  return fail(`task ${taskId} is stopping and takes no more input`);
636
- const sourceObligation = sourceTaskId && sourceEventId ? this.deps.store.get(sourceTaskId, sourceEventId) : null;
637
- if (sourceTaskId && sourceEventId) {
638
- if (sourceTaskId === taskId)
639
- return fail("source_task_id must name a different Task when forwarding a result");
640
- if (!this.ownedDispatch(threadId, sourceTaskId))
641
- return fail(`no source Task ${sourceTaskId} in this conversation`);
642
- if (sourceObligation?.kind !== "result")
643
- return fail(`no result event ${sourceEventId} found for source Task ${sourceTaskId}`);
644
- }
645
330
  const questionId = parsed.data.question_id
646
331
  ?? (record.state === "waiting_input" ? record.openQuestionId ?? undefined : undefined);
647
332
  const outcome = this.deps.executions.sendInput(this.envelope(threadId, `input-${randomUUID()}`), {
@@ -654,23 +339,13 @@ export class ForemanHost {
654
339
  ? `question ${questionId ?? ""} of task ${taskId} is no longer open`
655
340
  : `the input was not accepted (${outcome.reason})`);
656
341
  }
657
- if (sourceTaskId && sourceEventId) {
658
- // The target received the handoff. Transfer the source obligation so the
659
- // final synthesis task owns the user-facing delivery; turn_ended cannot
660
- // make this intermediate result due again.
661
- this.deps.store.transfer(sourceTaskId, sourceEventId, taskId, outcome.value.inputSeq);
662
- }
663
- else {
664
- // General handled what the task asked; the next result carries the obligation.
665
- this.deps.store.withdraw(taskId);
666
- }
342
+ // General handled what the task asked; the next result carries the obligation.
343
+ this.deps.store.withdraw(taskId);
667
344
  return {
668
345
  ok: true,
669
346
  text: questionId
670
347
  ? `Answer stored for task ${taskId} (input ${outcome.value.inputSeq}); the task resumes with it.`
671
- : sourceTaskId && sourceEventId
672
- ? `Input stored for task ${taskId} (input ${outcome.value.inputSeq}); result ${sourceTaskId}/${sourceEventId} was transferred to this Task and will not trigger a separate user fallback.`
673
- : `Input stored for task ${taskId} (input ${outcome.value.inputSeq}); the worker reads it at its next step. It is not applied yet.`,
348
+ : `Input stored for task ${taskId} (input ${outcome.value.inputSeq}); the worker reads it at its next step. It is not applied yet.`,
674
349
  };
675
350
  }
676
351
  cancel(threadId, args) {
@@ -678,25 +353,10 @@ export class ForemanHost {
678
353
  if (!parsed.success)
679
354
  return invalid(parsed.error);
680
355
  const { task_id: taskId, requested_by_user: byUser, reason } = parsed.data;
681
- const dispatch = this.ownedDispatch(threadId, taskId);
682
- if (!dispatch)
356
+ if (!this.ownedDispatch(threadId, taskId))
683
357
  return fail(`no background task ${taskId} in this conversation`);
684
- if (dispatch.metadata.children?.length) {
685
- if (!this.isOpen(dispatch))
686
- return { ok: true, text: `Task ${taskId} has already finished (${this.parentState(dispatch)}).` };
687
- if (byUser)
688
- this.deps.store.markUserCancelled(taskId);
689
- this.deps.store.withdraw(taskId);
690
- this.deps.store.requestGroupCancel(taskId, reason ?? "Parent task cancelled", byUser);
691
- this.cancelChildren(dispatch, reason ?? "Parent task cancelled", byUser);
692
- return { ok: true, text: `Cancel requested for all unfinished children of ${taskId}. Running children must confirm their stop before the parent is cancelled.` };
693
- }
694
358
  const record = this.latest(taskId);
695
359
  if (!record || isTerminalExecutionState(record.state)) {
696
- if (byUser) {
697
- this.deps.store.markUserCancelled(taskId);
698
- this.deps.store.withdraw(taskId);
699
- }
700
360
  return { ok: true, text: `Task ${taskId} is not running (${record ? record.state : "never started"}); nothing to cancel.` };
701
361
  }
702
362
  // Recorded first: the cancelled result that follows owes the user nothing.
@@ -717,13 +377,10 @@ export class ForemanHost {
717
377
  : `Cancel requested for task ${taskId}. The worker has not confirmed it stopped yet; do not tell the user it has stopped. The task is reported cancelled only after it does.`,
718
378
  };
719
379
  }
720
- delivered(threadId, taskId, purpose, eventId) {
721
- const dispatch = this.ownedDispatch(threadId, taskId);
722
- if (!dispatch)
380
+ delivered(threadId, taskId, purpose) {
381
+ if (!this.ownedDispatch(threadId, taskId))
723
382
  return { ok: false, text: `no background task ${taskId} in this conversation` };
724
- if (dispatch.metadata.children?.length && !eventId)
725
- return fail("A parent Task delivery must identify its result event/version");
726
- const satisfied = this.deps.store.markDelivered(taskId, purpose === "completion" ? "completion" : undefined, eventId);
383
+ const satisfied = this.deps.store.markDelivered(taskId, purpose === "completion" ? "completion" : undefined);
727
384
  return { ok: true, text: String(satisfied) };
728
385
  }
729
386
  /** General's turn that handled these events ended: whatever it did not deliver is due now. */
@@ -743,13 +400,13 @@ export class ForemanHost {
743
400
  }
744
401
  /** Open tasks of this conversation, for General's wake context. Empty when none. */
745
402
  summary(threadId) {
746
- const roots = this.deps.store.allForThread(threadId).filter((dispatch) => !dispatch.metadata.parentTaskId);
747
- const open = roots.filter((dispatch) => this.isOpen(dispatch));
748
- const recent = roots.filter((dispatch) => {
403
+ const open = this.deps.store.dispatchesForThread(threadId, 20).filter((dispatch) => {
404
+ const record = this.latest(dispatch.taskId);
749
405
  const delivery = this.deps.store.latest(dispatch.taskId);
750
- return !this.isOpen(dispatch) && delivery?.deliveryMode === "direct" && delivery.state === "delivered";
751
- }).slice(0, 5);
752
- if (open.length === 0 && recent.length === 0)
406
+ return (record !== null && !isTerminalExecutionState(record.state))
407
+ || delivery?.state === "pending" || delivery?.state === "delivery_failed";
408
+ });
409
+ if (open.length === 0)
753
410
  return "";
754
411
  let text = [
755
412
  "## Background Tasks",
@@ -763,30 +420,15 @@ export class ForemanHost {
763
420
  }
764
421
  text += `\n${entry}`;
765
422
  }
766
- for (const dispatch of recent) {
767
- const entry = `\n- Already delivered directly: ${dispatch.taskId} "${dispatch.title}": ${clip(this.deps.store.latest(dispatch.taskId)?.publicText ?? "", 1_000)}. Do not send it again.`;
768
- if (text.length + entry.length <= SUMMARY_CHAR_BUDGET)
769
- text += entry;
770
- }
771
423
  return text;
772
424
  }
773
425
  describeTask(dispatch, detail) {
774
- if (dispatch.metadata.children?.length) {
775
- const children = dispatch.metadata.children.map((id) => this.deps.store.dispatch(id))
776
- .filter((item) => item !== null);
777
- const delivery = this.deps.store.latest(dispatch.taskId);
778
- return [
779
- `- ${dispatch.taskId} "${dispatch.title}": ${this.parentState(dispatch)} (${children.length} child Tasks)`,
780
- ...children.map((child) => ` ${this.describeTask(child, detail)}`),
781
- ...(delivery ? [` parent delivery: ${delivery.state} (event ${delivery.eventId})`] : []),
782
- ].join("\n");
783
- }
784
426
  const record = this.latest(dispatch.taskId);
785
427
  const lines = [`- ${dispatch.taskId} "${dispatch.title}": ${record ? describeState(record) : "not started"}`];
786
- if (dispatch.metadata.contextTaskId)
787
- lines.push(` context from Task: ${dispatch.metadata.contextTaskId}`);
428
+ if (dispatch.worker)
429
+ lines.push(` worker: ${dispatch.worker}`);
788
430
  if (record) {
789
- const events = detail || record.state === "waiting_input" ? this.allEvents(record.executionId) : [];
431
+ const events = detail || record.state === "waiting_input" ? this.deps.executions.eventsAfter(record.executionId, 0) : [];
790
432
  const newestFirst = [...events].reverse();
791
433
  if (record.state === "waiting_input" && record.openQuestionId) {
792
434
  const question = newestFirst.find((event) => event.type === "question" && event.payload.questionId === record.openQuestionId);
@@ -806,9 +448,6 @@ export class ForemanHost {
806
448
  const result = record.result;
807
449
  if (result?.summary)
808
450
  lines.push(` result: ${clip(result.summary, detail ? 4_000 : 400)}`);
809
- const prepared = detail ? publicTaskText({ type: "result", payload: { data: result?.data } }) : null;
810
- if (prepared)
811
- lines.push(` prepared answer: ${prepared}`);
812
451
  if (result?.error)
813
452
  lines.push(` error: ${result.error.code}: ${clip(result.error.message, 1_000)}`);
814
453
  if (result?.artifactRefs?.length)
@@ -816,45 +455,15 @@ export class ForemanHost {
816
455
  }
817
456
  const delivery = this.deps.store.latest(dispatch.taskId);
818
457
  if (delivery && delivery.state !== "delivered" && delivery.state !== "withdrawn") {
819
- lines.push(` delivery to the user: ${delivery.state}${delivery.lastError ? ` (${delivery.lastError})` : ""} (event ${delivery.eventId})`);
820
- if (delivery.deliveryMode === "review" && delivery.state === "fallback_sent")
821
- lines.push(" Only a status notice was sent; the result still requires General to review and deliver it.");
822
- if (delivery.state === "transferred" && delivery.transferredToTaskId) {
823
- lines.push(` result forwarded to Task ${delivery.transferredToTaskId} (input ${delivery.transferredToInputSeq ?? "?"}); the target owns final delivery.`);
824
- }
458
+ lines.push(` delivery to the user: ${delivery.state}${delivery.lastError ? ` (${delivery.lastError})` : ""}`);
825
459
  }
826
460
  return lines.join("\n");
827
461
  }
828
- isOpen(dispatch) {
829
- const record = this.latest(dispatch.taskId);
830
- const workOpen = dispatch.metadata.children?.length
831
- ? this.childRecords(dispatch).some((child) => child.record !== null && !isTerminalExecutionState(child.record.state))
832
- : record !== null && !isTerminalExecutionState(record.state);
833
- const delivery = this.deps.store.latest(dispatch.taskId);
834
- return workOpen || delivery?.state === "pending" || delivery?.state === "delivery_failed"
835
- || (delivery?.state === "fallback_sent" && delivery.deliveryMode === "review" && delivery.kind === "result");
836
- }
837
- allEvents(executionId) {
838
- const events = [];
839
- let after = 0;
840
- for (;;) {
841
- const page = this.deps.executions.eventsAfter(executionId, after);
842
- const last = page.at(-1)?.seq ?? after;
843
- if (last <= after)
844
- break;
845
- events.push(...page);
846
- after = last;
847
- if (page.length < 500)
848
- break;
849
- }
850
- return events;
851
- }
852
462
  /** The system message that brings a question or result to the delegating thread. */
853
463
  eventMessage(event, threadId, dispatch, obligationOpens) {
854
464
  const payload = event.payload;
855
465
  const title = dispatch?.title ?? event.taskId;
856
- const taskQuote = `task_id="${event.taskId}"`;
857
- const quote = `${taskQuote}, event_id="${event.eventId}"`;
466
+ const quote = `task_id="${event.taskId}"`;
858
467
  const lines = [`[Background task update] ${event.type === "question" ? "Question from" : "Result of"} background task ${event.taskId} "${title}"`];
859
468
  if (dispatch?.sender)
860
469
  lines.push(`Requested by: ${dispatch.sender}`);
@@ -863,16 +472,13 @@ export class ForemanHost {
863
472
  if (Array.isArray(payload.choices) && payload.choices.length > 0) {
864
473
  lines.push(`Choices: ${payload.choices.map(String).join(" / ")}`);
865
474
  }
866
- lines.push(`If you can answer from what you know, answer with background_task_input(${taskQuote}, question_id="${textOf(payload.questionId)}"). Otherwise ask the requester with notify_user(${quote}) and pass their answer on. Do not do the task's work here.`);
475
+ lines.push(`If you can answer from what you know, answer with background_task_input(${quote}). Otherwise ask the requester with notify_user(${quote}) and pass their answer on. Do not do the task's work here.`);
867
476
  }
868
477
  else {
869
478
  const status = String(payload.status);
870
479
  lines.push(`Status: ${status}`);
871
480
  if (typeof payload.summary === "string")
872
481
  lines.push(`Summary: ${clip(payload.summary, 8_000)}`);
873
- const preparedReply = publicTaskText(event);
874
- if (preparedReply)
875
- lines.push(`Prepared reply for the requester (review before sending): ${preparedReply}`);
876
482
  const error = payload.error;
877
483
  if (error && typeof error === "object") {
878
484
  const { code, message } = error;
@@ -881,15 +487,12 @@ export class ForemanHost {
881
487
  if (Array.isArray(payload.artifactRefs) && payload.artifactRefs.length > 0) {
882
488
  lines.push(`Artifacts: ${payload.artifactRefs.slice(0, 30).map(String).join(", ")}`);
883
489
  }
884
- if (status === "cancelled" && !obligationOpens) {
885
- lines.push("The requester asked to stop this task; nothing more is needed unless they asked to hear back.");
886
- }
887
- else if (dispatch?.metadata.children?.length) {
888
- lines.push(`Review the child results together. Deliver the overall result with notify_user(${quote}, purpose="completion"). Retry only children that need further work using their child task_id; do not repeat successful children. The parent does not occupy a worker.`);
889
- }
890
- else if (status === "succeeded") {
490
+ if (status === "succeeded") {
891
491
  lines.push(`Check the result against the request before you deliver it. If it holds up, tell the requester with notify_user(${quote}, purpose="completion") in your own words, and send any drafts it contains with the right tool. If it falls short, run it again with delegate_task(retry_task_id="${event.taskId}") and say what must change.`);
892
492
  }
493
+ else if (status === "cancelled" && !obligationOpens) {
494
+ lines.push("The requester asked to stop this task; nothing more is needed unless they asked to hear back.");
495
+ }
893
496
  else {
894
497
  lines.push(`Tell the requester what happened with notify_user(${quote}, purpose="completion"), or run it again with delegate_task(retry_task_id="${event.taskId}") if a retry can succeed.`);
895
498
  }
@@ -908,7 +511,15 @@ export class ForemanHost {
908
511
  attachments: [],
909
512
  timestamp: event.createdAt,
910
513
  ...routing,
911
- meta: { ...routing.meta, [FOREMAN_EVENT_META_KEY]: { taskId: event.taskId, eventId: event.eventId } },
514
+ meta: {
515
+ ...routing.meta,
516
+ [FOREMAN_EVENT_META_KEY]: {
517
+ taskId: event.taskId,
518
+ eventId: event.eventId,
519
+ kind: event.type,
520
+ ...(event.type === "result" ? { status: String(payload.status) } : {}),
521
+ },
522
+ },
912
523
  ...(threadId !== DEFAULT_THREAD_ID ? { threadId } : {}),
913
524
  };
914
525
  }