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
@@ -5,7 +5,7 @@
5
5
  * sleep/wake loop. Each concern is delegated to a focused module:
6
6
  *
7
7
  * - HeartbeatManager: runtime heartbeat lifecycle
8
- * - ReplyWatchdog: deterministic acknowledgment and handoff pressure timer
8
+ * - ReplyWatchdog: fast-responder fallback timer
9
9
  * - WakeCycleRunner: wake loop, stream processing, error retry,
10
10
  * transcript indexing
11
11
  * - InterruptHandler: mid-turn message injection
@@ -13,11 +13,11 @@
13
13
  * - ChannelManager: messaging channel adapters
14
14
  */
15
15
  import { createDefaultExternalContextRegistry, ExternalContextRegistry } from "./external-context/registry.js";
16
- import { createHash, randomUUID } from "node:crypto";
16
+ import { randomUUID } from "node:crypto";
17
17
  import { beginLifecycleRun, createLifecycleRunId, deriveShadowLifecycleState, endLifecycleRun, traceLifecycle, traceShadowLifecycle } from "./lifecycle-trace.js";
18
- import { closeHostEntry, endForegroundWake } from "./lifecycle-transitions.js";
19
- import { isProactiveWakeEnabled, isTunnelEnabled, isWorkspaceEnabled } from "../config/types.js";
20
- import { DEFAULT_THREAD_ID, normalizeChannelName, resolveThreadId } from "../channels/interface.js";
18
+ import { endForegroundWake } from "./lifecycle-transitions.js";
19
+ import { isProactiveWakeEnabled, isThreadTasksEnabled, isTunnelEnabled, isWorkspaceEnabled } from "../config/types.js";
20
+ import { DEFAULT_THREAD_ID, resolveThreadId } from "../channels/interface.js";
21
21
  import { buildSystemPrompt } from "./system-prompt.js";
22
22
  import { SessionManager } from "./session-manager.js";
23
23
  import { buildWakeContext } from "./context.js";
@@ -27,7 +27,7 @@ import { ActivityTracker } from "./activity-tracker.js";
27
27
  import { ThreadActivityController } from "../threads/thread-activity-controller.js";
28
28
  import { shouldNotifyUpdate } from "../utils/version-check.js";
29
29
  import { shouldRunProactiveUpgradeCheck } from "./update-reminder-policy.js";
30
- import { loadOwnerConfig, consumePendingUpgrade, consumePendingRestart, writePendingRestart, getConfigDir, ownerConfigExists, saveOwnerConfig, deleteOnboardingSession, } from "../config/index.js";
30
+ import { getConfigFile, loadOwnerConfig, consumePendingUpgrade, consumePendingRestart, writePendingRestart, getConfigDir, ownerConfigExists, saveOwnerConfig, deleteOnboardingSession, } from "../config/index.js";
31
31
  import { syncSystemTimezone } from "../utils/timezone.js";
32
32
  import { logger } from "../logger.js";
33
33
  import { cleanOldScreenshots } from "../tools/screenshot.js";
@@ -40,7 +40,7 @@ import { runOnboardingGate } from "./onboarding-gate.js";
40
40
  import { OnboardingServiceClient } from "../onboarding/onboarding-service-client.js";
41
41
  import { deliverPendingQqWelcome, sendWelcomeMessages } from "../onboarding/send-invite.js";
42
42
  import { resumePendingChannelBinding } from "../onboarding/resume-channel-binding.js";
43
- import { answerAndDrop, executionGateReply, runServiceGate, serviceGateMessage, shouldEnforceServiceGate } from "./service-gate.js";
43
+ import { answerAndDrop, executionGateReply, persistedServiceStopped, runServiceGate, serviceGateMessage, shouldEnforceServiceGate } from "./service-gate.js";
44
44
  import { InterruptHandler } from "./interrupt-handler.js";
45
45
  import { ownerTranscriptionContext } from "../transcription-context.js";
46
46
  import { transcribeVoiceMessages } from "../utils/transcribe.js";
@@ -55,8 +55,7 @@ import { t, resolveLocale } from "../i18n/index.js";
55
55
  import { formatMessageSummary, previewIncomingForConsole, buildIncomingLogData } from "./message-format.js";
56
56
  import { buildReplyMapKeyFromMessage, buildReplyTarget, groupActivityDeliveryTargetForMessage, resolveTaskReplyTarget } from "./reply-target.js";
57
57
  import { notifyOwnerViaResolvedChain } from "./owner-notify.js";
58
- import { hasUsableOwnerChannel, listBoundOwnerChannels, resolveOwnerDefaultTarget } from "./owner-channel-resolver.js";
59
- import { runForemanBusinessRead } from "../threads/foreman-business.js";
58
+ import { hasUsableOwnerChannel, resolveOwnerDefaultTarget } from "./owner-channel-resolver.js";
60
59
  import { buildRuntimeSurface, } from "./runtime-surface.js";
61
60
  import { getAgentEngine } from "./engines/engine-factory.js";
62
61
  import { prepareEngineSwitchBoundary } from "./engine-switch.js";
@@ -92,11 +91,12 @@ import { backfillMissingThreadApplications, migrateLegacyThreadApplications, thr
92
91
  import { currentApplicationContext, inheritThreadApplication, threadApplication } from "../threads/application-context.js";
93
92
  import { ForemanHost } from "../threads/foreman-host.js";
94
93
  import { ForemanStore } from "../threads/foreman-store.js";
95
- import { canDelegateTasks, foremanEventsOf, getForemanClient, installForemanClient, isForemanRuntime, supportsTaskRuntime } from "../threads/foreman-protocol.js";
94
+ import { foremanEventsOf, getForemanClient, installForemanClient, isForemanRuntime } from "../threads/foreman-protocol.js";
96
95
  import { ForegroundTaskTracker, installForegroundTaskTracker } from "../threads/foreground-task.js";
97
96
  import { ThreadApplicationConflict } from "../threads/thread-meta.js";
98
97
  import { assertRuntimeApplicationId } from "../config/application-config.js";
99
98
  import { migrateProfileToAgentsMd } from "../config/agents-md-migration.js";
99
+ import { ThreadTaskRuntime, endWakeThreadTasks, isTaskRelevantMessage, isTaskResumeMessage, retireStaleResume, setThreadTaskRuntime, } from "../threads/thread-task-runtime.js";
100
100
  /**
101
101
  * Grace period before an idle cross-runtime engine recovery restarts.
102
102
  * Short on purpose: the gate only fires when nothing is in flight.
@@ -112,8 +112,6 @@ const WORKER_EXECUTION_IDLE_GRACE_MS = 10_000;
112
112
  * Main agent loop. This is the entry point that runs forever.
113
113
  */
114
114
  export async function startAgentLoop(config, options) {
115
- if (options?.signal?.aborted)
116
- return;
117
115
  const workerMode = options?.workerMode ?? null;
118
116
  const workerExecution = workerMode?.execution ?? null;
119
117
  if (workerMode && workerExecution) {
@@ -179,6 +177,19 @@ export async function startAgentLoop(config, options) {
179
177
  // orphaned worker can no longer commit claims owned by its successor.
180
178
  ...(workerMode ? { ownerId: workerMode.relay.workerInstanceId } : {}),
181
179
  });
180
+ // Thread task store (#1996): opened by the same queue owner at the same
181
+ // moment it recovers in-flight claims, and fenced by the same identity.
182
+ // Bounded executions keep caller-owned task state outside this loop.
183
+ // Conversational task tracking is off unless the local config opts in
184
+ // (`threadTasksEnabled: true`). Null means disabled for this process.
185
+ const threadTasks = workerExecution || !isThreadTasksEnabled(config)
186
+ ? null
187
+ : ThreadTaskRuntime.start({
188
+ threadId: executionContext.threadId,
189
+ writerId: workerMode ? workerMode.relay.workerInstanceId : randomUUID(),
190
+ config,
191
+ });
192
+ setThreadTaskRuntime(threadTasks);
182
193
  const channelManager = new ChannelManager(config, messageQueue);
183
194
  const activityTracker = new ActivityTracker();
184
195
  const mainActivityController = workerMode
@@ -197,20 +208,20 @@ export async function startAgentLoop(config, options) {
197
208
  // --- Thread supervisor (multi-thread architecture) ---
198
209
  // Install before adapters start: connect/hello can immediately replay work.
199
210
  // Recover old queues after channels are available for replies.
200
- // The host routes chats to in-process conversations and admitted Tasks to
201
- // the independent worker pool. Execution workers never spawn nested hosts.
202
- if (!workerMode) {
211
+ // Main process only. Spawns/routes worker processes for non-default threads
212
+ // via the ChannelManager threadRouter hook. Disabled in worker mode (workers
213
+ // never route to sub-workers) and when multiThread is turned off in config.
214
+ if (!workerMode && config.multiThread.enabled) {
203
215
  const { ThreadSupervisor } = await import("../threads/supervisor.js");
204
216
  // Foreman (#2805): the host owns General's background tasks, their
205
217
  // delivery obligations and fallback. Built before the supervisor so the
206
218
  // supervisor can route workers' commands and `general` events to it.
207
219
  let foremanHost = null;
208
- let taskStore = null;
209
220
  const supervisor = new ThreadSupervisor({
210
221
  channelManager,
211
222
  messageQueue,
212
223
  deferRecovery: true,
213
- maxConcurrentThreads: config.workers?.maxConcurrent ?? config.multiThread.maxConcurrentThreads,
224
+ maxConcurrentThreads: config.multiThread.maxConcurrentThreads,
214
225
  idleTimeoutMs: config.multiThread.idleTtlMs,
215
226
  config,
216
227
  desktopRuntimeResolution: options?.desktopRuntimeResolution,
@@ -232,24 +243,16 @@ export async function startAgentLoop(config, options) {
232
243
  // for the worker the supervisor actually talks to.
233
244
  handleForemanCommand: (threadId, command) => foremanHost?.handle(threadId, command)
234
245
  ?? Promise.resolve({ ok: false, text: "Error: background tasks are not available in this conversation." }),
235
- handleForemanBusinessRead: (executionId, command, workspace) => foremanHost?.readBusiness(executionId, command.tool, command.args, workspace)
236
- ?? Promise.resolve({ ok: false, text: "Business reads are unavailable" }),
237
246
  });
238
247
  supervisor.onExecutionEvent((event, caller) => {
239
248
  if (caller.kind !== "tui")
240
249
  return;
241
250
  channelManager.getAdapter("tui")?.publishExecutionEvent(event);
242
251
  });
243
- const foremanExecutions = supervisor.executionService();
252
+ const foremanExecutions = isForemanRuntime(config) ? supervisor.executionService() : null;
244
253
  if (foremanExecutions) {
245
254
  const store = new ForemanStore();
246
- taskStore = store;
247
255
  foremanHost = new ForemanHost({
248
- canReadBusiness: (_threadId, target) => isWorkspaceEnabled(config) && target !== null
249
- && listBoundOwnerChannels(config, loadOwnerConfig()).some((owner) => owner.validationStatus === "verified"
250
- && owner.kind === "dm" && normalizeChannelName(owner.channel) === normalizeChannelName(target.channel) && owner.to === target.to),
251
- readBusiness: runForemanBusinessRead,
252
- allowDelegation: () => isForemanRuntime(config),
253
256
  executions: foremanExecutions,
254
257
  store,
255
258
  enqueueToGeneral: (threadId, message) => {
@@ -258,15 +261,16 @@ export async function startAgentLoop(config, options) {
258
261
  wakeGeneralLoop?.();
259
262
  return;
260
263
  }
261
- // The conversation's durable queue and application admission.
264
+ // A thread's own durable queue; the supervisor starts its worker.
265
+ // The thread's recorded application keeps strict admission satisfied.
262
266
  const app = threadApplication(threadId, config);
263
267
  if (!supervisor.tryRouteMessage({ ...message, threadId, ...(app ? { app } : {}) }, threadId)) {
264
268
  throw new Error(`Thread ${threadId} did not accept a background-task update`);
265
269
  }
266
270
  },
267
271
  deliver: (target, text, threadId) => channelManager.tryDeliver(target, text, {
268
- logContext: "background task delivery",
269
- context: { kind: "task_update", ...(threadId === DEFAULT_THREAD_ID ? {} : { businessThreadId: threadId }) },
272
+ logContext: "background task fallback",
273
+ context: threadId === DEFAULT_THREAD_ID ? {} : { businessThreadId: threadId },
270
274
  }),
271
275
  ownerTarget: () => {
272
276
  const owner = resolveOwnerDefaultTarget(config, loadOwnerConfig());
@@ -275,16 +279,14 @@ export async function startAgentLoop(config, options) {
275
279
  locale: () => resolveLocale(loadOwnerConfig().language),
276
280
  });
277
281
  const host = foremanHost;
278
- installForemanClient(supportsTaskRuntime() ? { delegationEnabled: isForemanRuntime(config), request: (command) => host.handle(DEFAULT_THREAD_ID, command) } : null);
282
+ installForemanClient({ request: (command) => host.handle(DEFAULT_THREAD_ID, command) });
283
+ registerShutdownCallback(() => {
284
+ host.stop();
285
+ store.close();
286
+ return Promise.resolve();
287
+ });
279
288
  }
280
289
  activeForemanHost = foremanHost;
281
- // Keep the Task ledger and delivery adapters alive until conversations and
282
- // workers settle. Their final receipts may still arrive during shutdown.
283
- registerShutdownCallback(async () => {
284
- foremanHost?.stop();
285
- await supervisor.shutdownAll();
286
- taskStore?.close();
287
- });
288
290
  channelManager.setThreadRouter((msg, threadId) => supervisor.routeMessage(msg, threadId));
289
291
  // Expose for the main-loop wake context to refresh its own awareness.
290
292
  threadSupervisor = supervisor;
@@ -321,6 +323,9 @@ export async function startAgentLoop(config, options) {
321
323
  catch (err) {
322
324
  logger.warn(`Graceful shutdown did not complete cleanly: ${err instanceof Error ? err.message : String(err)}`);
323
325
  }
326
+ // After the channels, so it never delays them. A short grace lets an
327
+ // in-flight sidecar call commit; unconsumed events wait for the next start.
328
+ await threadTasks?.close(2_000);
324
329
  messageQueue.close();
325
330
  logger.system("Graceful shutdown complete.");
326
331
  });
@@ -340,6 +345,11 @@ export async function startAgentLoop(config, options) {
340
345
  threadSupervisor.startExecutions();
341
346
  // Its first sweep sends fallbacks a restart left overdue.
342
347
  activeForemanHost?.start();
348
+ const supervisor = threadSupervisor;
349
+ registerShutdownCallback(async () => {
350
+ logger.system("Graceful shutdown: stopping thread workers...");
351
+ await supervisor.shutdownAll();
352
+ });
343
353
  }
344
354
  // --- Resume an interrupted channel binding ---
345
355
  // Runs BEFORE the onboarding gate on purpose. A binding whose completion tail
@@ -545,7 +555,7 @@ export async function startAgentLoop(config, options) {
545
555
  // Foreman's foreground limit, for conversational sessions that can delegate.
546
556
  // Thread-local: getConfigDir() is the thread's own directory in a worker.
547
557
  // Skill Learning drives this same session and is exempt.
548
- const foregroundTasks = !workerExecution && canDelegateTasks()
558
+ const foregroundTasks = !workerExecution && getForemanClient()
549
559
  ? new ForegroundTaskTracker(getConfigDir(), () => new Date(), () => agentState.learning)
550
560
  : null;
551
561
  installForegroundTaskTracker(foregroundTasks);
@@ -677,7 +687,7 @@ export async function startAgentLoop(config, options) {
677
687
  browserSelection: runtimeSurface.browserSelection,
678
688
  credentialBacked: runtimeSurface.credentialBacked,
679
689
  conversation: { threadId: workerMode?.threadId },
680
- foreman: canDelegateTasks(),
690
+ foreman: getForemanClient() !== null,
681
691
  });
682
692
  }, runtimeSurface, options?.workspaceCwd ? { workspaceCwd: options.workspaceCwd } : {});
683
693
  // Hand the engine-switch summary to the freshly created session (rotation
@@ -727,6 +737,26 @@ export async function startAgentLoop(config, options) {
727
737
  // Expose the wake function so out-of-loop components can trigger an
728
738
  // immediate wake without threading the closure through every call site.
729
739
  agentState.wakeMainLoop = wakeMainLoop;
740
+ // Thread task resumes (#1996 PR 5): after a wake the runtime queues the
741
+ // next runnable task as a system row and wakes the loop, so queued work
742
+ // continues without a new owner message, with proactive wakes off and in
743
+ // workers. It never queues while a wake runs or execution is gated; the
744
+ // row is still checked against the task when it is dequeued.
745
+ // TODO: a resume queued just before the service gate engages is discarded
746
+ // with the gated queue (main) or still runs (workers, which have no gate
747
+ // pass and whose system rows skip the gate reply). The window is tiny.
748
+ threadTasks?.enableResumes({
749
+ queue: messageQueue,
750
+ isBusy: () => agentState.busy || !messageQueue.isEmpty(),
751
+ isGated: () => readFundLeagueHold() !== null
752
+ // A worker's heartbeat holds the status from its start; the main
753
+ // process persists every change, so read that instead.
754
+ || (workerMode ? persistedServiceStopped(getConfigFile()) : shouldEnforceServiceGate(heartbeat.lastServiceStatus)),
755
+ wake: wakeMainLoop,
756
+ });
757
+ // Startup: work that was queued, or resumed and interrupted, before the
758
+ // previous process stopped continues now.
759
+ void threadTasks?.requestResume();
730
760
  const interruptHandler = new InterruptHandler({
731
761
  externalContextRegistry,
732
762
  session: session.activeSession,
@@ -883,7 +913,6 @@ export async function startAgentLoop(config, options) {
883
913
  else {
884
914
  registerShutdownCallback(() => {
885
915
  closeTranscriptMemoryDb();
886
- closeUsageReportQueue();
887
916
  return Promise.resolve();
888
917
  });
889
918
  }
@@ -927,17 +956,10 @@ export async function startAgentLoop(config, options) {
927
956
  dreamingRunner?.beginStep();
928
957
  heartbeat.nudge();
929
958
  return () => {
930
- // Maintenance holds the slot the way a foreground wake does. Release
931
- // it through Stopping so workers resume only after the slot is free.
932
- endForegroundWake({
933
- from: "Running",
934
- cleanup: () => { agentState.learning = false; },
935
- release: () => {
936
- executionOwner.setBusy(false, "maintenance_release");
937
- resumeWorkers?.();
938
- heartbeat.nudge();
939
- },
940
- });
959
+ agentState.learning = false;
960
+ executionOwner.setBusy(false, "maintenance_release");
961
+ resumeWorkers?.();
962
+ heartbeat.nudge();
941
963
  };
942
964
  },
943
965
  hasPendingSkills: hasPendingSkillLearning,
@@ -981,12 +1003,6 @@ export async function startAgentLoop(config, options) {
981
1003
  if (options?.onReady) {
982
1004
  options.onReady({ agentState, sessionManager: session, messageQueue, ownerConfig });
983
1005
  }
984
- const wakeOnShutdown = () => { session.requestStop(); wakeMainLoop(); };
985
- options?.signal?.addEventListener("abort", wakeOnShutdown, { once: true });
986
- registerShutdownCallback(() => {
987
- options?.signal?.removeEventListener("abort", wakeOnShutdown);
988
- return Promise.resolve();
989
- });
990
1006
  logger.system(workerExecution
991
1007
  ? "Single-shot worker loop started. Waiting for its execution dispatch..."
992
1008
  : "Agent loop started. Waiting for messages or heartbeat...");
@@ -1006,21 +1022,10 @@ export async function startAgentLoop(config, options) {
1006
1022
  let lastWakeScreenshotMs = 0;
1007
1023
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop
1008
1024
  while (true) {
1009
- if (options?.signal?.aborted)
1010
- break;
1011
1025
  const leagueHold = readFundLeagueHold();
1012
1026
  if (leagueHold) {
1013
- // The durable marker already blocks the next wake. A slot that is still
1014
- // held stops first; an idle loop closes the entry directly. Neither
1015
- // confirms an engine barrier.
1016
- closeHostEntry({
1017
- busy: agentState.busy,
1018
- requestStop: () => { agentState.requestStop(); },
1019
- markClosed: () => {
1020
- executionOwner.setBusy(false, "league_hold");
1021
- agentState.gated = true;
1022
- },
1023
- });
1027
+ executionOwner.setBusy(false, "league_hold");
1028
+ agentState.gated = true;
1024
1029
  await new Promise((resolve) => { wakeResolver = resolve; });
1025
1030
  wakeResolver = null;
1026
1031
  continue;
@@ -1068,6 +1073,11 @@ export async function startAgentLoop(config, options) {
1068
1073
  });
1069
1074
  nextProactiveWakeAt = sleepMs === null ? null : nextProactiveWakeAt ?? Date.now() + sleepMs;
1070
1075
  const isFirstWake = firstIteration;
1076
+ // Worker: report idle status before sleeping so the supervisor's
1077
+ // cross-thread awareness snapshot reflects this thread as idle.
1078
+ if (workerMode && !isFirstWake) {
1079
+ workerMode.relay.relayStatus(false, "idle");
1080
+ }
1071
1081
  // We are about to go idle, which is the only safe moment to move back
1072
1082
  // across a runtime boundary: that costs a restart and collapses the
1073
1083
  // session into a summary, so doing it mid-conversation would cut a live
@@ -1116,8 +1126,6 @@ export async function startAgentLoop(config, options) {
1116
1126
  resolve();
1117
1127
  }
1118
1128
  });
1119
- if (options?.signal?.aborted)
1120
- break;
1121
1129
  if (proactiveWakeSchedule.changed && messageQueue.isEmpty()) {
1122
1130
  proactiveWakeSchedule.changed = false;
1123
1131
  nextProactiveWakeAt = null;
@@ -1227,23 +1235,21 @@ export async function startAgentLoop(config, options) {
1227
1235
  stopRequested: agentState.stopRequested,
1228
1236
  busy: agentState.busy,
1229
1237
  recovering,
1230
- // Pi confirms this with `agent_settled`. An abort is not a receipt,
1231
- // so an engine that cannot prove it stopped stays unconfirmed.
1232
- barrierConfirmed: session.executionBarrierConfirmed(),
1238
+ // No engine has a confirmed execution barrier yet. Pi settlement
1239
+ // only prevents a send collision; it does not prove the engine stopped.
1240
+ barrierConfirmed: false,
1233
1241
  }),
1234
1242
  legacy: {
1235
1243
  busy: agentState.busy,
1236
1244
  gated: agentState.gated,
1237
1245
  stopRequested: agentState.stopRequested,
1238
1246
  },
1239
- barrierConfirmed: session.executionBarrierConfirmed(),
1247
+ barrierConfirmed: false,
1240
1248
  });
1241
1249
  };
1242
1250
  const releaseLifecycleRun = (reason) => {
1243
1251
  // Stopping is one beat when nothing is still in flight. The slot stays
1244
1252
  // busy until cleanup finishes, so busy=false is not what ends the wake.
1245
- // A worker thread is the same loop: its busy flag lives on the
1246
- // supervisor, and it is cleared only after this cleanup.
1247
1253
  endForegroundWake({
1248
1254
  // A failed compaction is still this same foreground run. Phase 3 is
1249
1255
  // what moves that path into Recovering before the wake ends.
@@ -1264,10 +1270,6 @@ export async function startAgentLoop(config, options) {
1264
1270
  endLifecycleRun(lifecycleRunId);
1265
1271
  },
1266
1272
  release: () => {
1267
- // The supervisor learns a worker is idle here, after cleanup, not
1268
- // at the next sleep. The local slot is released for both loops.
1269
- if (workerMode)
1270
- workerMode.relay.relayStatus(false, "idle");
1271
1273
  executionOwner.setBusy(false, reason, lifecycleRunId);
1272
1274
  },
1273
1275
  });
@@ -1308,6 +1310,10 @@ export async function startAgentLoop(config, options) {
1308
1310
  continue;
1309
1311
  }
1310
1312
  }
1313
+ // Re-check a queued task resume against the task itself: a cancel,
1314
+ // change, wait or block since it was queued retires it unexecuted.
1315
+ if (retireStaleResume(messageQueue, threadTasks, msg))
1316
+ continue;
1311
1317
  const gateReply = msg.channel === "system" ? null : executionGateReply(heartbeat, resolveLocale(ownerConfig.language));
1312
1318
  if (gateReply !== null) {
1313
1319
  await answerAndDrop(channelManager, messageQueue, msg, gateReply, tuiAdapter);
@@ -1320,6 +1326,9 @@ export async function startAgentLoop(config, options) {
1320
1326
  if (trigger === "message" && messages.length === 0) {
1321
1327
  releaseLifecycleRun("empty_message_batch");
1322
1328
  heartbeat.nudge();
1329
+ // A retired stale resume ends here without a wake end; queue the next
1330
+ // runnable task (or the same one at its new version) instead.
1331
+ void threadTasks?.requestResume();
1323
1332
  continue;
1324
1333
  }
1325
1334
  const correlatedTuiMessage = messages.find((message) => typeof message.meta?.tuiRequestId === "string");
@@ -1335,10 +1344,16 @@ export async function startAgentLoop(config, options) {
1335
1344
  // other channels get the same cloud/local pipeline.
1336
1345
  if (trigger === "message" && !workerExecution) {
1337
1346
  for (const msg of messages) {
1347
+ // A task resume is internal: no "typing…" for the owner.
1348
+ if (isTaskResumeMessage(msg))
1349
+ continue;
1338
1350
  sendTypingToMessage(channelManager, msg).catch(() => { });
1339
1351
  }
1340
1352
  }
1341
1353
  await transcribeVoiceMessages(messages, ownerTranscriptionContext(ownerConfig));
1354
+ // Event first, sidecar second (#1996): after transcription so a voice
1355
+ // request is stored with its text. Idempotent and fail-safe.
1356
+ threadTasks?.recordMessages(messages.filter(isTaskRelevantMessage));
1342
1357
  for (const msg of messages) {
1343
1358
  agentState.replyMap.set(buildReplyMapKeyFromMessage(msg), buildReplyTarget(msg));
1344
1359
  }
@@ -1485,18 +1500,8 @@ export async function startAgentLoop(config, options) {
1485
1500
  operation: "query",
1486
1501
  outcome: "started",
1487
1502
  });
1488
- let releaseConversationTurn;
1489
- agentState.currentDispatchId = createHash("sha256").update(JSON.stringify(messages.map((message) => message.id))).digest("hex");
1490
1503
  try {
1491
- if (!workerExecution) {
1492
- releaseConversationTurn = await (options?.acquireConversationTurn?.(options.signal)
1493
- ?? threadSupervisor?.acquireConversationTurn(options?.signal));
1494
- }
1495
- if (options?.signal?.aborted) {
1496
- messageQueue.requeueOwnedClaims();
1497
- break;
1498
- }
1499
- // Accepted inputs are also replayed from the ledger on executor recovery.
1504
+ // Execution inputs in this batch reach the model with this turn.
1500
1505
  noteMessagesHandedToModel(messages);
1501
1506
  const result = await cycleRunner.run(contentBlocks, trigger, messages, consecutiveErrors);
1502
1507
  traceLifecycle({
@@ -1507,10 +1512,6 @@ export async function startAgentLoop(config, options) {
1507
1512
  evidence: { terminalState: result.terminalState },
1508
1513
  });
1509
1514
  consecutiveErrors = result.consecutiveErrors;
1510
- if (options?.signal?.aborted) {
1511
- messageQueue.requeueOwnedClaims();
1512
- break;
1513
- }
1514
1515
  // A queue claim is not terminal until the complete wake cycle returns.
1515
1516
  // If the worker dies during model/tool execution, the in-flight rows
1516
1517
  // remain durable and are replayed by the next queue owner.
@@ -1521,26 +1522,47 @@ export async function startAgentLoop(config, options) {
1521
1522
  // Foreman: the background-task updates this turn handled, read before
1522
1523
  // their claims are released (mid-turn interrupts included).
1523
1524
  const handledForemanEvents = failoverRestartPending ? [] : foremanEventsOf(messageQueue.currentCycleClaims());
1524
- // A completed foreground wake acknowledges its claims. Accepted background
1525
- // work already belongs to the canonical Task ledger.
1526
- if (!failoverRestartPending)
1527
- messageQueue.markAllInFlightHandled();
1525
+ const foregroundRecord = foregroundTasks?.current;
1526
+ const handedOffTaskId = foregroundRecord?.handedOff ? foregroundRecord.taskId : undefined;
1527
+ // Claims become handled only once each message is a durable task event,
1528
+ // so their follow-up survives the queue row (#1996). handled ≠ completed.
1529
+ endWakeThreadTasks(messageQueue, threadTasks, { failoverRestartPending });
1528
1530
  if (!failoverRestartPending) {
1529
1531
  // An accepted finish that is not waiting for the user ends the
1530
1532
  // foreground task; the next one starts from zero. A wake that only
1531
1533
  // ran out of recovery attempts or timed out keeps the count.
1532
- if (result.finished === true && result.terminalState === "completed")
1534
+ if (result.finished === true && result.terminalState === "completed" && !handedOffTaskId)
1533
1535
  foregroundTasks?.reset();
1534
1536
  // Whatever the turn did not deliver to the user is due for the fallback now.
1535
1537
  if (handledForemanEvents.length > 0) {
1536
1538
  void getForemanClient()?.request({ type: "turn_ended", events: handledForemanEvents });
1537
1539
  }
1538
1540
  }
1541
+ if (result.finished === true && result.terminalState === "completed" && handedOffTaskId) {
1542
+ if (workerMode)
1543
+ workerMode.relay.markActivityWaitingForBackground(handedOffTaskId);
1544
+ else
1545
+ mainActivityController?.markWaitingForBackground(handedOffTaskId);
1546
+ foregroundTasks?.reset();
1547
+ }
1548
+ for (const event of handledForemanEvents) {
1549
+ if (event.kind !== "result")
1550
+ continue;
1551
+ const state = event.status === "succeeded" ? "completed" : "failed";
1552
+ if (workerMode)
1553
+ workerMode.relay.finishBackgroundActivity(event.taskId, state);
1554
+ else
1555
+ mainActivityController?.finishBackgroundTask(event.taskId, state);
1556
+ }
1539
1557
  if (workerMode) {
1540
- workerMode.relay.finishActivityRun(result.terminalState);
1558
+ if (!(result.finished === true && result.terminalState === "completed" && handedOffTaskId)) {
1559
+ workerMode.relay.finishActivityRun(result.terminalState);
1560
+ }
1541
1561
  }
1542
1562
  else {
1543
- mainActivityController?.finishRun(result.terminalState);
1563
+ if (!(result.finished === true && result.terminalState === "completed" && handedOffTaskId)) {
1564
+ mainActivityController?.finishRun(result.terminalState);
1565
+ }
1544
1566
  }
1545
1567
  tuiTerminalOutcome = resolveTuiTerminalOutcome(result.terminalState);
1546
1568
  }
@@ -1549,6 +1571,9 @@ export async function startAgentLoop(config, options) {
1549
1571
  // terminal boundary. A later wake must not inherit this turn's claims
1550
1572
  // and accidentally confirm them together with unrelated work.
1551
1573
  try {
1574
+ // Released before the requeue: a replayed resume for an interrupted
1575
+ // task must run when it comes back (#2664 review P1-B).
1576
+ threadTasks?.releaseResume(messageQueue.currentCycleClaims());
1552
1577
  messageQueue.requeueOwnedClaims();
1553
1578
  }
1554
1579
  catch (requeueErr) {
@@ -1570,7 +1595,6 @@ export async function startAgentLoop(config, options) {
1570
1595
  throw err;
1571
1596
  }
1572
1597
  finally {
1573
- releaseConversationTurn?.();
1574
1598
  // Cleanup must always run, even if the extracted cycle runner throws
1575
1599
  // unexpectedly, otherwise the reply watchdog timer can leak across
1576
1600
  // wake cycles. The runtime heartbeat loop runs independently and