@agents-ensemble/core 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/assets/pi/pi-cli-env-wrapper.cmd +28 -0
  2. package/assets/pi/pi-cli-env-wrapper.sh +19 -0
  3. package/assets/pi/settings.json +4 -0
  4. package/dist/acp/acp-preset-bins.d.ts +3 -3
  5. package/dist/acp/acp-preset-bins.d.ts.map +1 -1
  6. package/dist/acp/acp-preset-bins.js +3 -3
  7. package/dist/acp/acp-preset-bins.js.map +1 -1
  8. package/dist/acp/index.d.ts +3 -1
  9. package/dist/acp/index.d.ts.map +1 -1
  10. package/dist/acp/index.js +2 -1
  11. package/dist/acp/index.js.map +1 -1
  12. package/dist/acp/pi-acp-wrapper.d.ts +3 -0
  13. package/dist/acp/pi-acp-wrapper.d.ts.map +1 -0
  14. package/dist/acp/pi-acp-wrapper.js +41 -0
  15. package/dist/acp/pi-acp-wrapper.js.map +1 -0
  16. package/dist/acp/pi-builtin-config.d.ts +10 -0
  17. package/dist/acp/pi-builtin-config.d.ts.map +1 -0
  18. package/dist/acp/pi-builtin-config.js +36 -0
  19. package/dist/acp/pi-builtin-config.js.map +1 -0
  20. package/dist/acp/pi-runtime.d.ts +23 -0
  21. package/dist/acp/pi-runtime.d.ts.map +1 -0
  22. package/dist/acp/pi-runtime.js +160 -0
  23. package/dist/acp/pi-runtime.js.map +1 -0
  24. package/dist/acp/pi-session-map.d.ts +36 -0
  25. package/dist/acp/pi-session-map.d.ts.map +1 -0
  26. package/dist/acp/pi-session-map.js +266 -0
  27. package/dist/acp/pi-session-map.js.map +1 -0
  28. package/dist/acp/resolve-acp-spawn.d.ts +9 -3
  29. package/dist/acp/resolve-acp-spawn.d.ts.map +1 -1
  30. package/dist/acp/resolve-acp-spawn.js +41 -9
  31. package/dist/acp/resolve-acp-spawn.js.map +1 -1
  32. package/dist/acp/resolve-bundled-acp-bin.d.ts +2 -2
  33. package/dist/acp/resolve-bundled-acp-bin.d.ts.map +1 -1
  34. package/dist/acp/resolve-bundled-acp-bin.js +4 -4
  35. package/dist/acp/resolve-bundled-acp-bin.js.map +1 -1
  36. package/dist/acp/validate-acp-preset-prerequisites.d.ts +1 -1
  37. package/dist/acp/validate-acp-preset-prerequisites.d.ts.map +1 -1
  38. package/dist/acp/validate-acp-preset-prerequisites.js +30 -10
  39. package/dist/acp/validate-acp-preset-prerequisites.js.map +1 -1
  40. package/dist/conductor/conductor-agent.d.ts +15 -3
  41. package/dist/conductor/conductor-agent.d.ts.map +1 -1
  42. package/dist/conductor/conductor-auth.d.ts +4 -3
  43. package/dist/conductor/conductor-auth.d.ts.map +1 -1
  44. package/dist/conductor/conductor-auth.js +20 -4
  45. package/dist/conductor/conductor-auth.js.map +1 -1
  46. package/dist/conductor/conductor-dispatch-lifecycle.d.ts +85 -0
  47. package/dist/conductor/conductor-dispatch-lifecycle.d.ts.map +1 -0
  48. package/dist/conductor/conductor-dispatch-lifecycle.js +190 -0
  49. package/dist/conductor/conductor-dispatch-lifecycle.js.map +1 -0
  50. package/dist/conductor/conductor-history.d.ts +15 -0
  51. package/dist/conductor/conductor-history.d.ts.map +1 -1
  52. package/dist/conductor/conductor-history.js +38 -7
  53. package/dist/conductor/conductor-history.js.map +1 -1
  54. package/dist/conductor/conductor-path-safety.d.ts +23 -0
  55. package/dist/conductor/conductor-path-safety.d.ts.map +1 -0
  56. package/dist/conductor/conductor-path-safety.js +319 -0
  57. package/dist/conductor/conductor-path-safety.js.map +1 -0
  58. package/dist/conductor/conductor-pi-auth.d.ts +57 -2
  59. package/dist/conductor/conductor-pi-auth.d.ts.map +1 -1
  60. package/dist/conductor/conductor-pi-auth.js +718 -40
  61. package/dist/conductor/conductor-pi-auth.js.map +1 -1
  62. package/dist/conductor/conductor-recovery-inbox.d.ts +73 -0
  63. package/dist/conductor/conductor-recovery-inbox.d.ts.map +1 -0
  64. package/dist/conductor/conductor-recovery-inbox.js +181 -0
  65. package/dist/conductor/conductor-recovery-inbox.js.map +1 -0
  66. package/dist/conductor/conductor-send-reconnect.d.ts +33 -4
  67. package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
  68. package/dist/conductor/conductor-send-reconnect.js +89 -24
  69. package/dist/conductor/conductor-send-reconnect.js.map +1 -1
  70. package/dist/conductor/conductor-session-driver.d.ts +34 -1
  71. package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
  72. package/dist/conductor/conductor-session-driver.js +529 -75
  73. package/dist/conductor/conductor-session-driver.js.map +1 -1
  74. package/dist/conductor/conductor-session.d.ts +4 -0
  75. package/dist/conductor/conductor-session.d.ts.map +1 -1
  76. package/dist/conductor/conductor-session.js +416 -145
  77. package/dist/conductor/conductor-session.js.map +1 -1
  78. package/dist/conductor/conductor-setup.d.ts +30 -0
  79. package/dist/conductor/conductor-setup.d.ts.map +1 -0
  80. package/dist/conductor/conductor-setup.js +243 -0
  81. package/dist/conductor/conductor-setup.js.map +1 -0
  82. package/dist/conductor/cursor-bootstrap-state.d.ts +9 -0
  83. package/dist/conductor/cursor-bootstrap-state.d.ts.map +1 -0
  84. package/dist/conductor/cursor-bootstrap-state.js +11 -0
  85. package/dist/conductor/cursor-bootstrap-state.js.map +1 -0
  86. package/dist/conductor/cursor-run-snapshots.d.ts +30 -0
  87. package/dist/conductor/cursor-run-snapshots.d.ts.map +1 -0
  88. package/dist/conductor/cursor-run-snapshots.js +120 -0
  89. package/dist/conductor/cursor-run-snapshots.js.map +1 -0
  90. package/dist/conductor/index.d.ts +18 -7
  91. package/dist/conductor/index.d.ts.map +1 -1
  92. package/dist/conductor/index.js +9 -4
  93. package/dist/conductor/index.js.map +1 -1
  94. package/dist/conductor/operator-command.d.ts +5 -0
  95. package/dist/conductor/operator-command.d.ts.map +1 -1
  96. package/dist/conductor/operator-command.js +6 -0
  97. package/dist/conductor/operator-command.js.map +1 -1
  98. package/dist/conductor/operator-input-binding.d.ts +17 -0
  99. package/dist/conductor/operator-input-binding.d.ts.map +1 -1
  100. package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts +45 -0
  101. package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts.map +1 -0
  102. package/dist/conductor/pi-auth-recovery-snapshot-store.js +116 -0
  103. package/dist/conductor/pi-auth-recovery-snapshot-store.js.map +1 -0
  104. package/dist/conductor/pi-conductor-agent.d.ts +6 -3
  105. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  106. package/dist/conductor/pi-conductor-agent.js +276 -53
  107. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  108. package/dist/conductor/pi-conductor-session-file.d.ts +13 -0
  109. package/dist/conductor/pi-conductor-session-file.d.ts.map +1 -0
  110. package/dist/conductor/pi-conductor-session-file.js +21 -0
  111. package/dist/conductor/pi-conductor-session-file.js.map +1 -0
  112. package/dist/conductor/pi-resource-loader.d.ts +6 -0
  113. package/dist/conductor/pi-resource-loader.d.ts.map +1 -1
  114. package/dist/conductor/pi-resource-loader.js +26 -12
  115. package/dist/conductor/pi-resource-loader.js.map +1 -1
  116. package/dist/conductor/pi-user-root-lock.d.ts +8 -0
  117. package/dist/conductor/pi-user-root-lock.d.ts.map +1 -0
  118. package/dist/conductor/pi-user-root-lock.js +53 -0
  119. package/dist/conductor/pi-user-root-lock.js.map +1 -0
  120. package/dist/conductor/recovery-inbox-command.d.ts +29 -0
  121. package/dist/conductor/recovery-inbox-command.d.ts.map +1 -0
  122. package/dist/conductor/recovery-inbox-command.js +248 -0
  123. package/dist/conductor/recovery-inbox-command.js.map +1 -0
  124. package/dist/conductor/session/events/index.d.ts +1 -1
  125. package/dist/conductor/session/events/index.d.ts.map +1 -1
  126. package/dist/conductor/session/events/index.js.map +1 -1
  127. package/dist/conductor/session/events/session-event.d.ts +43 -1
  128. package/dist/conductor/session/events/session-event.d.ts.map +1 -1
  129. package/dist/conductor/session/events/session-log-event-groups.d.ts +1 -1
  130. package/dist/conductor/session/events/session-log-event-groups.d.ts.map +1 -1
  131. package/dist/conductor/session/events/session-log-event-groups.js +2 -0
  132. package/dist/conductor/session/events/session-log-event-groups.js.map +1 -1
  133. package/dist/conductor/session/events/session-log-event.d.ts +4 -0
  134. package/dist/conductor/session/events/session-log-event.d.ts.map +1 -1
  135. package/dist/conductor/session/format-session-event.js +57 -5
  136. package/dist/conductor/session/format-session-event.js.map +1 -1
  137. package/dist/conductor/session/select-dispatch-batch.d.ts +2 -0
  138. package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
  139. package/dist/conductor/session/select-dispatch-batch.js +36 -2
  140. package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
  141. package/dist/conductor/session/session-event-queue.d.ts +14 -0
  142. package/dist/conductor/session/session-event-queue.d.ts.map +1 -1
  143. package/dist/conductor/session/session-event-queue.js +82 -10
  144. package/dist/conductor/session/session-event-queue.js.map +1 -1
  145. package/dist/conductor/session/session-event.d.ts +1 -1
  146. package/dist/conductor/session/session-event.d.ts.map +1 -1
  147. package/dist/conductor/session-policy.d.ts.map +1 -1
  148. package/dist/conductor/session-policy.js +8 -4
  149. package/dist/conductor/session-policy.js.map +1 -1
  150. package/dist/conductor/submit-operator-input.d.ts +3 -0
  151. package/dist/conductor/submit-operator-input.d.ts.map +1 -1
  152. package/dist/conductor/submit-operator-input.js +21 -0
  153. package/dist/conductor/submit-operator-input.js.map +1 -1
  154. package/dist/config/deep-merge.d.ts.map +1 -1
  155. package/dist/config/deep-merge.js +13 -3
  156. package/dist/config/deep-merge.js.map +1 -1
  157. package/dist/config/defaults.d.ts.map +1 -1
  158. package/dist/config/defaults.js +1 -1
  159. package/dist/config/defaults.js.map +1 -1
  160. package/dist/config/doctor-settings.d.ts +59 -0
  161. package/dist/config/doctor-settings.d.ts.map +1 -0
  162. package/dist/config/doctor-settings.js +343 -0
  163. package/dist/config/doctor-settings.js.map +1 -0
  164. package/dist/config/ensemble-config-digest.d.ts +2 -4
  165. package/dist/config/ensemble-config-digest.d.ts.map +1 -1
  166. package/dist/config/ensemble-config-digest.js +2 -4
  167. package/dist/config/ensemble-config-digest.js.map +1 -1
  168. package/dist/config/index.d.ts +3 -1
  169. package/dist/config/index.d.ts.map +1 -1
  170. package/dist/config/index.js +1 -0
  171. package/dist/config/index.js.map +1 -1
  172. package/dist/config/load-ensemble-config.d.ts.map +1 -1
  173. package/dist/config/load-ensemble-config.js +8 -1
  174. package/dist/config/load-ensemble-config.js.map +1 -1
  175. package/dist/config/parse-config.d.ts.map +1 -1
  176. package/dist/config/parse-config.js +34 -14
  177. package/dist/config/parse-config.js.map +1 -1
  178. package/dist/config/resolve-settings.d.ts +3 -1
  179. package/dist/config/resolve-settings.d.ts.map +1 -1
  180. package/dist/config/resolve-settings.js +7 -4
  181. package/dist/config/resolve-settings.js.map +1 -1
  182. package/dist/config/types.d.ts +14 -5
  183. package/dist/config/types.d.ts.map +1 -1
  184. package/dist/dispatch/attach-worker.js +1 -1
  185. package/dist/dispatch/attach-worker.js.map +1 -1
  186. package/dist/dispatch/worker-acp-session.d.ts +21 -3
  187. package/dist/dispatch/worker-acp-session.d.ts.map +1 -1
  188. package/dist/dispatch/worker-acp-session.js +50 -13
  189. package/dist/dispatch/worker-acp-session.js.map +1 -1
  190. package/dist/ensemble-paths.d.ts +19 -19
  191. package/dist/ensemble-paths.d.ts.map +1 -1
  192. package/dist/ensemble-paths.js +40 -22
  193. package/dist/ensemble-paths.js.map +1 -1
  194. package/dist/issue/resolve-issue-url.d.ts +4 -0
  195. package/dist/issue/resolve-issue-url.d.ts.map +1 -1
  196. package/dist/issue/resolve-issue-url.js +2 -2
  197. package/dist/issue/resolve-issue-url.js.map +1 -1
  198. package/dist/operator-runtime.d.ts +19 -4
  199. package/dist/operator-runtime.d.ts.map +1 -1
  200. package/dist/operator-runtime.js +13 -2
  201. package/dist/operator-runtime.js.map +1 -1
  202. package/dist/profile/types.d.ts +4 -0
  203. package/dist/profile/types.d.ts.map +1 -1
  204. package/dist/profile/types.js +29 -12
  205. package/dist/profile/types.js.map +1 -1
  206. package/dist/representation/escape-terminal-control-characters.d.ts +9 -0
  207. package/dist/representation/escape-terminal-control-characters.d.ts.map +1 -0
  208. package/dist/representation/escape-terminal-control-characters.js +34 -0
  209. package/dist/representation/escape-terminal-control-characters.js.map +1 -0
  210. package/dist/representation/index.d.ts +1 -0
  211. package/dist/representation/index.d.ts.map +1 -1
  212. package/dist/representation/index.js +1 -0
  213. package/dist/representation/index.js.map +1 -1
  214. package/dist/runtime/types.d.ts +9 -0
  215. package/dist/runtime/types.d.ts.map +1 -1
  216. package/dist/runtime/worker-runtime.d.ts +14 -1
  217. package/dist/runtime/worker-runtime.d.ts.map +1 -1
  218. package/dist/runtime/worker-runtime.js +197 -50
  219. package/dist/runtime/worker-runtime.js.map +1 -1
  220. package/dist/runtime/worker-session.d.ts +2 -1
  221. package/dist/runtime/worker-session.d.ts.map +1 -1
  222. package/dist/runtime/worker-session.js +1 -0
  223. package/dist/runtime/worker-session.js.map +1 -1
  224. package/dist/session/export-session-bundle.d.ts +77 -0
  225. package/dist/session/export-session-bundle.d.ts.map +1 -0
  226. package/dist/session/export-session-bundle.js +640 -0
  227. package/dist/session/export-session-bundle.js.map +1 -0
  228. package/dist/session/index.d.ts +6 -3
  229. package/dist/session/index.d.ts.map +1 -1
  230. package/dist/session/index.js +4 -1
  231. package/dist/session/index.js.map +1 -1
  232. package/dist/session/open-question-control.d.ts +9 -0
  233. package/dist/session/open-question-control.d.ts.map +1 -1
  234. package/dist/session/open-question-control.js +26 -1
  235. package/dist/session/open-question-control.js.map +1 -1
  236. package/dist/session/open-questions-store.d.ts +1 -1
  237. package/dist/session/open-questions-store.d.ts.map +1 -1
  238. package/dist/session/open-questions-store.js +3 -6
  239. package/dist/session/open-questions-store.js.map +1 -1
  240. package/dist/session/persisted-file.d.ts +5 -0
  241. package/dist/session/persisted-file.d.ts.map +1 -0
  242. package/dist/session/persisted-file.js +56 -0
  243. package/dist/session/persisted-file.js.map +1 -0
  244. package/dist/session/session-resume-store.d.ts +79 -10
  245. package/dist/session/session-resume-store.d.ts.map +1 -1
  246. package/dist/session/session-resume-store.js +353 -83
  247. package/dist/session/session-resume-store.js.map +1 -1
  248. package/dist/session/session-snapshot-paths.d.ts +11 -0
  249. package/dist/session/session-snapshot-paths.d.ts.map +1 -0
  250. package/dist/session/session-snapshot-paths.js +41 -0
  251. package/dist/session/session-snapshot-paths.js.map +1 -0
  252. package/dist/session/session-state-store.d.ts +2 -2
  253. package/dist/session/session-state-store.d.ts.map +1 -1
  254. package/dist/session/session-state-store.js +5 -16
  255. package/dist/session/session-state-store.js.map +1 -1
  256. package/dist/session/tool-call-snapshot-history.d.ts +12 -1
  257. package/dist/session/tool-call-snapshot-history.d.ts.map +1 -1
  258. package/dist/session/tool-call-snapshot-history.js +75 -70
  259. package/dist/session/tool-call-snapshot-history.js.map +1 -1
  260. package/dist/session/worker-session-history.d.ts +51 -0
  261. package/dist/session/worker-session-history.d.ts.map +1 -0
  262. package/dist/session/worker-session-history.js +654 -0
  263. package/dist/session/worker-session-history.js.map +1 -0
  264. package/dist/workspace/index.d.ts +1 -1
  265. package/dist/workspace/index.d.ts.map +1 -1
  266. package/dist/worktree/worktree.d.ts +6 -1
  267. package/dist/worktree/worktree.d.ts.map +1 -1
  268. package/dist/worktree/worktree.js +123 -2
  269. package/dist/worktree/worktree.js.map +1 -1
  270. package/package.json +9 -4
@@ -1,4 +1,6 @@
1
1
  import { ensureMaxTurnsOpenQuestion } from '../escalation/enqueue-max-turns-question.js';
2
+ import { ConductorDispatchLifecycle, settleConductorDispatch, } from './conductor-dispatch-lifecycle.js';
3
+ import { ConductorRecoveryInbox } from './conductor-recovery-inbox.js';
2
4
  import { reconnectConductorAgent, sendConductorWithReconnect, } from './conductor-send-reconnect.js';
3
5
  import { formatSessionEventsForConductor } from './session/format-session-event.js';
4
6
  import { bufferDispatchHoldEvents, createDispatchHoldState, pruneStalePermissionEvents, } from './session/dispatch-hold.js';
@@ -19,9 +21,85 @@ export async function runConductorSessionDriver(options) {
19
21
  runId: '',
20
22
  status: 'finished',
21
23
  };
24
+ let cursorBootstrapPending = options.recoveryBackend === 'cursor' &&
25
+ (options.cursorBootstrapState
26
+ ? options.cursorBootstrapState.status === 'incomplete'
27
+ : options.cursorBootstrapComplete === undefined
28
+ ? !options.skipInitialSend
29
+ : !options.cursorBootstrapComplete);
30
+ let cursorBootstrapCompletedThisDriver = false;
22
31
  let inFlightSend;
23
32
  let postLoopWaiting = false;
33
+ const lifecycle = options.dispatchLifecycle ??
34
+ new ConductorDispatchLifecycle();
35
+ const recoveryInbox = options.recoveryInbox ?? new ConductorRecoveryInbox();
24
36
  const dispatchHoldState = options.dispatchHoldState ?? createDispatchHoldState();
37
+ const cursorBootstrapNeedsDecision = () => options.cursorBootstrapState?.status === 'unknown' ||
38
+ options.cursorBootstrapState?.status === 'saving';
39
+ const isCursorBootstrapPending = () => {
40
+ if (options.recoveryBackend !== 'cursor')
41
+ return false;
42
+ if (options.cursorBootstrapState) {
43
+ return (options.cursorBootstrapState.status === 'incomplete' &&
44
+ !cursorBootstrapCompletedThisDriver);
45
+ }
46
+ return cursorBootstrapPending;
47
+ };
48
+ const markCursorBootstrapCompleted = () => {
49
+ cursorBootstrapPending = false;
50
+ cursorBootstrapCompletedThisDriver = true;
51
+ };
52
+ let automaticReconnect;
53
+ const sendReconnect = withAgentAvailabilityCallbacks(options.sendReconnect, (reason, info) => {
54
+ lifecycle.setAgentAvailable(info.success);
55
+ if (info.source === 'automatic') {
56
+ automaticReconnect = { reason, succeeded: info.success };
57
+ }
58
+ });
59
+ const settleAttempt = (input) => {
60
+ const outcome = settleConductorDispatch({
61
+ lifecycle,
62
+ lease: input.lease,
63
+ inbox: recoveryInbox,
64
+ ...(input.reservation ? { reservation: input.reservation } : {}),
65
+ workflow: 'issue',
66
+ backend: options.recoveryBackend ?? 'pi',
67
+ sendCount: input.sendCount,
68
+ sourceKey: input.sourceKey,
69
+ eventTypes: input.events.map((event) => event.type),
70
+ operatorInputTexts: input.lease.payload.operatorInputTexts,
71
+ result: input.result,
72
+ ...(automaticReconnect
73
+ ? { automaticReconnect: { ...automaticReconnect } }
74
+ : {}),
75
+ toolCallStarted: input.toolCallStarted,
76
+ captureFailure: !options.shutdownSignal.aborted &&
77
+ options.operatorCanInspectRecovery === true,
78
+ ...(input.requiresConfirmation !== undefined
79
+ ? { requiresConfirmation: input.requiresConfirmation }
80
+ : {}),
81
+ ...(input.result.status === 'send_rejected'
82
+ ? { agentAvailable: false }
83
+ : {}),
84
+ });
85
+ automaticReconnect = undefined;
86
+ if (!outcome.completed &&
87
+ !options.shutdownSignal.aborted &&
88
+ outcome.failureClass) {
89
+ options.onRecoveryAttention?.({
90
+ attemptId: input.lease.attemptId,
91
+ sourceKey: input.sourceKey,
92
+ status: input.result.status,
93
+ failureClass: outcome.failureClass,
94
+ confirmationRequired: lifecycle.confirmationRequired,
95
+ pendingOperatorMessageCount: options.eventQueue
96
+ .snapshot()
97
+ .filter((event) => event.type === 'operator.message').length,
98
+ ...(outcome.snapshot ? { snapshot: outcome.snapshot } : {}),
99
+ agentAvailable: lifecycle.available,
100
+ });
101
+ }
102
+ };
25
103
  const hasDispatchableWorkerOutcome = () => selectDispatchBatch({
26
104
  queue: options.eventQueue.snapshot().filter((event) => event.type === 'worker.completed' || event.type === 'worker.failed'),
27
105
  state: dispatchBatchState,
@@ -38,7 +116,14 @@ export async function runConductorSessionDriver(options) {
38
116
  const holdBlocksStop = dispatchHoldState.heldEvents.length > 0 &&
39
117
  options.stopOnUnansweredInput !== true &&
40
118
  loopState.lastStatus !== 'error' &&
119
+ loopState.lastStatus !== 'send_rejected' &&
41
120
  loopState.lastStatus !== 'cancelled';
121
+ if (lifecycle.confirmationRequired &&
122
+ options.operatorCanInspectRecovery !== true) {
123
+ stopReason =
124
+ loopState.lastStatus === 'cancelled' ? 'cancelled' : 'error';
125
+ return true;
126
+ }
42
127
  if (holdBlocksStop) {
43
128
  postLoopWaiting = false;
44
129
  return false;
@@ -47,6 +132,13 @@ export async function runConductorSessionDriver(options) {
47
132
  postLoopWaiting = false;
48
133
  return false;
49
134
  }
135
+ if (options.operatorCanInspectRecovery && lifecycle.confirmationRequired) {
136
+ if (!postLoopWaiting) {
137
+ postLoopWaiting = true;
138
+ options.onIssueLoopStop?.();
139
+ }
140
+ return false;
141
+ }
50
142
  // A worker publishes its outcome to the inbox before its runtime round
51
143
  // leaves the running state. Drain that notification chain before
52
144
  // deciding the session is idle, otherwise the driver can tear down in
@@ -72,17 +164,71 @@ export async function runConductorSessionDriver(options) {
72
164
  }
73
165
  return false;
74
166
  };
167
+ if (options.shutdownSignal.aborted) {
168
+ stopReason = 'interrupted';
169
+ return {
170
+ stopReason,
171
+ sendCount,
172
+ lastSendResult,
173
+ autonomousTurns,
174
+ lastDispatchesThisTurn,
175
+ };
176
+ }
177
+ if (options.cursorBootstrapState && !options.skipInitialSend) {
178
+ try {
179
+ await waitForCursorBootstrapDecision({
180
+ state: options.cursorBootstrapState,
181
+ lifecycle,
182
+ eventQueue: options.eventQueue,
183
+ onOperatorQueueChanged: options.onOperatorQueueChanged,
184
+ signal: options.shutdownSignal,
185
+ });
186
+ }
187
+ catch (error) {
188
+ if (!isAbortError(error))
189
+ throw error;
190
+ stopReason = 'interrupted';
191
+ return {
192
+ stopReason,
193
+ sendCount,
194
+ lastSendResult,
195
+ autonomousTurns,
196
+ lastDispatchesThisTurn,
197
+ };
198
+ }
199
+ }
75
200
  if (!options.skipInitialSend) {
201
+ const initialPrompt = await Promise.resolve(options.initialPrompt);
202
+ if (options.shutdownSignal.aborted) {
203
+ stopReason = 'interrupted';
204
+ return {
205
+ stopReason,
206
+ sendCount,
207
+ lastSendResult,
208
+ autonomousTurns,
209
+ lastDispatchesThisTurn,
210
+ };
211
+ }
212
+ const initialLease = lifecycle.beginSend({
213
+ events: [],
214
+ sourceKey: 'initial',
215
+ operatorInputTexts: [],
216
+ });
217
+ if (!initialLease) {
218
+ throw new Error('Could not acquire the initial conductor send lease.');
219
+ }
220
+ automaticReconnect = undefined;
221
+ let initialToolCallStarted = false;
76
222
  options.onConductorInbound?.({
77
223
  triggers: 1,
78
224
  sources: ['initial'],
79
225
  });
80
- lastSendResult = await runEventConductorSend({
81
- // Keep the initial dispatch asynchronous as it was when the driver
82
- // fetched and compiled the prompt itself.
83
- message: await Promise.resolve(options.initialPrompt),
226
+ const initialSend = runEventConductorSend({
227
+ // The initial result is consumed before the event loop begins, but the
228
+ // wait still follows the same shutdown interruption contract as sends.
229
+ message: initialPrompt,
84
230
  conductorHandle: options.conductorHandle,
85
- sendReconnect: options.sendReconnect,
231
+ sendReconnect,
86
232
  signal: options.shutdownSignal,
87
233
  workerDispatches: options.workerDispatches,
88
234
  workerFailures: options.workerFailures,
@@ -93,16 +239,80 @@ export async function runConductorSessionDriver(options) {
93
239
  resetOutboundDispatchesThisSend: options.resetOutboundDispatchesThisSend,
94
240
  getOutboundDispatchesThisSend: options.getOutboundDispatchesThisSend,
95
241
  onSendStarted: options.onSendStarted,
96
- onSendProgress: options.onSendProgress,
242
+ onSendProgress: (info) => {
243
+ initialToolCallStarted = true;
244
+ options.onSendProgress?.(info);
245
+ },
97
246
  onSendComplete: (info) => {
98
247
  sendCount = info.sendCount;
99
248
  lastDispatchesThisTurn = info.conductorDispatchesThisTurn;
100
249
  autonomousTurns = info.autonomousTurns;
101
250
  options.onSendComplete(info);
102
251
  },
252
+ }).then((result) => {
253
+ if (options.recoveryBackend === 'cursor' &&
254
+ isIssueDispatchCompleted(result)) {
255
+ markCursorBootstrapCompleted();
256
+ }
257
+ const explicitInterruptQueued = result.status === 'cancelled' &&
258
+ options.eventQueue.snapshot().some((event) => event.type === 'operator.message' &&
259
+ event.delivery === 'interrupt');
260
+ settleAttempt({
261
+ lease: initialLease,
262
+ events: [],
263
+ sourceKey: 'initial',
264
+ sendCount,
265
+ result,
266
+ toolCallStarted: initialToolCallStarted,
267
+ ...(explicitInterruptQueued ? { requiresConfirmation: false } : {}),
268
+ });
269
+ return result;
103
270
  });
271
+ const awaitedInitialSend = await awaitWithAbort(initialSend, options.shutdownSignal);
272
+ if (awaitedInitialSend.aborted || options.shutdownSignal.aborted) {
273
+ stopReason = 'interrupted';
274
+ return {
275
+ stopReason,
276
+ sendCount,
277
+ lastSendResult,
278
+ autonomousTurns,
279
+ lastDispatchesThisTurn,
280
+ };
281
+ }
282
+ lastSendResult = awaitedInitialSend.value;
104
283
  }
105
284
  while (true) {
285
+ if (options.shutdownSignal.aborted) {
286
+ stopReason = 'interrupted';
287
+ break;
288
+ }
289
+ if (options.cursorBootstrapState && cursorBootstrapNeedsDecision()) {
290
+ try {
291
+ await waitForCursorBootstrapDecision({
292
+ state: options.cursorBootstrapState,
293
+ lifecycle,
294
+ eventQueue: options.eventQueue,
295
+ onOperatorQueueChanged: options.onOperatorQueueChanged,
296
+ signal: options.shutdownSignal,
297
+ });
298
+ }
299
+ catch (error) {
300
+ if (!isAbortError(error))
301
+ throw error;
302
+ stopReason = 'interrupted';
303
+ break;
304
+ }
305
+ if (options.shutdownSignal.aborted) {
306
+ stopReason = 'interrupted';
307
+ break;
308
+ }
309
+ }
310
+ if (lifecycle.confirmationRequired &&
311
+ options.operatorCanInspectRecovery !== true) {
312
+ stopReason =
313
+ lastSendResult.status === 'cancelled' ? 'cancelled' : 'error';
314
+ break;
315
+ }
106
316
  if (options.isPermissionPending) {
107
317
  pruneStalePermissionEvents({
108
318
  state: dispatchHoldState,
@@ -130,6 +340,10 @@ export async function runConductorSessionDriver(options) {
130
340
  stopReason = 'interrupted';
131
341
  break;
132
342
  }
343
+ if (options.shutdownSignal.aborted) {
344
+ stopReason = 'interrupted';
345
+ break;
346
+ }
133
347
  lastSendResult = awaited.value;
134
348
  inFlightSend = undefined;
135
349
  if (lastSendResult.status === 'cancelled' &&
@@ -167,7 +381,8 @@ export async function runConductorSessionDriver(options) {
167
381
  const shouldBreak = await shouldBreakAfterLoopState(loopState);
168
382
  const idleOperatorMessageReady = queuedDispatch?.batch.events.some((event) => event.type === 'operator.message' && event.delivery === 'idle');
169
383
  if (shouldBreak &&
170
- (!idleOperatorMessageReady || options.stopOnUnansweredInput === true)) {
384
+ (!idleOperatorMessageReady ||
385
+ options.stopOnUnansweredInput === true)) {
171
386
  break;
172
387
  }
173
388
  continue;
@@ -199,10 +414,14 @@ export async function runConductorSessionDriver(options) {
199
414
  dispatchBatchState,
200
415
  autonomousTurns,
201
416
  maxTurns: options.maxTurns,
417
+ lifecycle,
418
+ recoveryInbox,
419
+ operatorCanInspectRecovery: options.operatorCanInspectRecovery === true,
202
420
  signal: options.shutdownSignal,
203
421
  dispatchHoldState,
204
422
  onDispatchHoldChanged: options.onDispatchHoldChanged,
205
423
  onOperatorQueueChanged: options.onOperatorQueueChanged,
424
+ onRecoveryCapacityBlocked: options.onRecoveryCapacityBlocked,
206
425
  isPermissionPending: options.isPermissionPending,
207
426
  canDispatchIdleOperatorMessage: () => canDispatchIdleOperatorMessage({
208
427
  eventQueue: options.eventQueue,
@@ -224,16 +443,57 @@ export async function runConductorSessionDriver(options) {
224
443
  if (!dispatchResult) {
225
444
  continue;
226
445
  }
227
- const { events: batch, selected } = dispatchResult;
228
- postLoopWaiting = false;
229
- if (selected) {
230
- dispatchBatchState = markContinuationConsumed(dispatchBatchState, selected);
446
+ if (options.shutdownSignal.aborted) {
447
+ if (dispatchResult.reservation) {
448
+ recoveryInbox.release(dispatchResult.reservation);
449
+ }
450
+ stopReason = 'interrupted';
451
+ break;
231
452
  }
453
+ const { events: batch, selected, reservation } = dispatchResult;
454
+ postLoopWaiting = false;
232
455
  if (dispatchResult.sourceKey === 'operator.reconnect') {
456
+ const reconnectLease = lifecycle.beginReconnect();
457
+ if (!reconnectLease)
458
+ continue;
459
+ options.eventQueue.removeEvents(batch);
460
+ if (selected) {
461
+ dispatchBatchState = markContinuationConsumed(dispatchBatchState, selected);
462
+ }
233
463
  dispatchBatchState = dispatchBatchStateAfterSend(dispatchResult.sourceKey);
234
- await runOperatorReconnect(options);
464
+ const reconnected = await runOperatorReconnect(options);
465
+ lifecycle.settleReconnect(reconnectLease, reconnected);
466
+ if (reconnected && !options.shutdownSignal.aborted) {
467
+ // 再接続待ちの間に積まれた /reconnect は今回の復旧で満たされた。
468
+ options.eventQueue.findSendEvents((event) => event.type === 'operator.reconnect');
469
+ }
470
+ continue;
471
+ }
472
+ const payload = {
473
+ events: batch,
474
+ sourceKey: dispatchResult.sourceKey,
475
+ operatorInputTexts: dispatchResult.operatorInputTexts,
476
+ };
477
+ const lease = lifecycle.beginSend(payload);
478
+ if (!lease) {
479
+ if (reservation)
480
+ recoveryInbox.release(reservation);
235
481
  continue;
236
482
  }
483
+ if (dispatchResult.fromHeldEvents) {
484
+ const selectedEvents = new Set(batch);
485
+ dispatchHoldState.heldEvents = dispatchHoldState.heldEvents.filter((event) => !selectedEvents.has(event));
486
+ notifyHeldDispatchProgress({
487
+ dispatchHoldState,
488
+ onDispatchHoldChanged: options.onDispatchHoldChanged,
489
+ });
490
+ }
491
+ else {
492
+ options.eventQueue.removeEvents(batch);
493
+ }
494
+ if (selected) {
495
+ dispatchBatchState = markContinuationConsumed(dispatchBatchState, selected);
496
+ }
237
497
  const autonomousTurnsAfter = autonomousTurnsAfterConductorBatch(batch, autonomousTurns);
238
498
  const { workerDispatches, workerFailures } = countWorkerOutcomesInBatch(batch);
239
499
  options.onOperatorDispatched?.(batch);
@@ -241,10 +501,17 @@ export async function runConductorSessionDriver(options) {
241
501
  triggers: batch.filter(isTriggerSessionEvent).length,
242
502
  sources: uniqueSources(batch, dispatchResult.sourceKey),
243
503
  });
504
+ const eventMessage = formatSessionEventsForConductor(batch);
505
+ const restoreCursorBootstrap = isCursorBootstrapPending();
506
+ const message = restoreCursorBootstrap
507
+ ? `${options.initialPrompt}\n\n## 今回のセッションイベント\n\n${eventMessage}`
508
+ : eventMessage;
509
+ let dispatchHadToolCalls = false;
244
510
  const dispatchSend = runEventConductorSend({
245
- message: formatSessionEventsForConductor(batch),
511
+ message,
512
+ ...(restoreCursorBootstrap ? { historyInbound: eventMessage } : {}),
246
513
  conductorHandle: options.conductorHandle,
247
- sendReconnect: options.sendReconnect,
514
+ sendReconnect,
248
515
  signal: options.shutdownSignal,
249
516
  workerDispatches: options.workerDispatches,
250
517
  workerFailures: options.workerFailures,
@@ -259,27 +526,42 @@ export async function runConductorSessionDriver(options) {
259
526
  resetOutboundDispatchesThisSend: options.resetOutboundDispatchesThisSend,
260
527
  getOutboundDispatchesThisSend: options.getOutboundDispatchesThisSend,
261
528
  onSendStarted: options.onSendStarted,
262
- onSendProgress: options.onSendProgress,
529
+ onSendProgress: (info) => {
530
+ dispatchHadToolCalls = true;
531
+ options.onSendProgress?.(info);
532
+ },
263
533
  onSendComplete: (info) => {
264
534
  sendCount = info.sendCount;
265
535
  lastDispatchesThisTurn = info.conductorDispatchesThisTurn;
266
536
  autonomousTurns = info.autonomousTurns;
267
537
  options.onSendComplete(info);
268
538
  },
269
- });
270
- if (options.onDispatchBatchSettled &&
271
- batch.some((event) => event.type === 'operator.message')) {
272
- inFlightSend = dispatchSend.then((result) => {
273
- options.onDispatchBatchSettled?.({ events: batch, result });
274
- return result;
275
- }, (error) => {
276
- options.onDispatchBatchSettled?.({ events: batch, error });
277
- throw error;
539
+ }).then((result) => {
540
+ if (restoreCursorBootstrap && isIssueDispatchCompleted(result)) {
541
+ markCursorBootstrapCompleted();
542
+ }
543
+ const explicitInterruptQueued = result.status === 'cancelled' &&
544
+ options.eventQueue.snapshot().some((event) => event.type === 'operator.message' &&
545
+ event.delivery === 'interrupt');
546
+ settleAttempt({
547
+ lease,
548
+ events: batch,
549
+ sourceKey: dispatchResult.sourceKey,
550
+ sendCount,
551
+ result,
552
+ ...(reservation ? { reservation } : {}),
553
+ toolCallStarted: dispatchHadToolCalls,
554
+ ...(explicitInterruptQueued
555
+ ? { requiresConfirmation: false }
556
+ : {}),
278
557
  });
279
- }
280
- else {
281
- inFlightSend = dispatchSend;
282
- }
558
+ if (options.onDispatchBatchSettled &&
559
+ batch.some((event) => event.type === 'operator.message')) {
560
+ options.onDispatchBatchSettled({ events: batch, result });
561
+ }
562
+ return result;
563
+ });
564
+ inFlightSend = dispatchSend;
283
565
  dispatchBatchState = dispatchBatchStateAfterSend(dispatchResult.sourceKey);
284
566
  }
285
567
  if (inFlightSend && stopReason !== 'interrupted') {
@@ -297,6 +579,19 @@ function uniqueSources(batch, fallback) {
297
579
  const sources = [...new Set(batch.map((event) => eventSourceKey(event)))];
298
580
  return sources.length > 0 ? sources : [fallback];
299
581
  }
582
+ function withAgentAvailabilityCallbacks(options, onAvailabilityChanged) {
583
+ return {
584
+ ...options,
585
+ onAuthReconnectComplete: (info) => {
586
+ onAvailabilityChanged('auth', info);
587
+ (options.onAuthReconnectComplete ?? options.onReconnectComplete)?.(info);
588
+ },
589
+ onTransportReconnectComplete: (info) => {
590
+ onAvailabilityChanged('transport', info);
591
+ (options.onTransportReconnectComplete ?? options.onReconnectComplete)?.(info);
592
+ },
593
+ };
594
+ }
300
595
  function runEventConductorSend(input) {
301
596
  const workersBefore = input.workerDispatches.length;
302
597
  const failuresBefore = input.workerFailures.length;
@@ -320,7 +615,14 @@ function runEventConductorSend(input) {
320
615
  tool: info.tool,
321
616
  });
322
617
  },
323
- }).then((sendResult) => {
618
+ })
619
+ .catch(() => {
620
+ // Keep sendConductorWithReconnect's public rejection contract. A rejected
621
+ // send has an unknown provider outcome, so this workflow settles it
622
+ // locally without retaining the raw exception or retrying the prompt.
623
+ return { runId: '', status: 'send_rejected' };
624
+ })
625
+ .then((sendResult) => {
324
626
  const completedDuringSend = input.workerDispatches.length - workersBefore;
325
627
  const outboundDispatches = input.getOutboundDispatchesThisSend?.() ?? 0;
326
628
  const conductorDispatches = Math.max(completedDuringSend, outboundDispatches);
@@ -330,6 +632,9 @@ function runEventConductorSend(input) {
330
632
  runId: sendResult.runId,
331
633
  status: sendResult.status,
332
634
  inbound: input.message,
635
+ ...(input.historyInbound !== undefined
636
+ ? { historyInbound: input.historyInbound }
637
+ : {}),
333
638
  dispatchSource: input.dispatchSource,
334
639
  result: sendResult.result,
335
640
  error: sendResult.error,
@@ -344,8 +649,33 @@ function runEventConductorSend(input) {
344
649
  return sendResult;
345
650
  });
346
651
  }
652
+ function isIssueDispatchCompleted(result) {
653
+ return (result.status === 'finished' ||
654
+ result.status === 'completed' ||
655
+ result.status === 'running');
656
+ }
347
657
  async function waitForDispatchBatch(input) {
658
+ let capacityNoticeSent = false;
348
659
  for (;;) {
660
+ if (!input.lifecycle.canDispatch) {
661
+ const queuedReconnects = input.eventQueue
662
+ .snapshot()
663
+ .filter((event) => event.type === 'operator.reconnect');
664
+ if (queuedReconnects.length > 0) {
665
+ if (input.eventQueue
666
+ .snapshot()
667
+ .some((event) => event.type === 'operator.message')) {
668
+ input.onOperatorQueueChanged?.();
669
+ }
670
+ return {
671
+ events: queuedReconnects,
672
+ sourceKey: 'operator.reconnect',
673
+ operatorInputTexts: [],
674
+ };
675
+ }
676
+ await waitForDispatchChange(input);
677
+ continue;
678
+ }
349
679
  if (input.dispatchHoldState.dispatchHold) {
350
680
  bufferDispatchHoldEvents({
351
681
  state: input.dispatchHoldState,
@@ -368,62 +698,188 @@ async function waitForDispatchBatch(input) {
368
698
  maxTurns: input.maxTurns,
369
699
  allowIdleOperatorMessages: input.canDispatchIdleOperatorMessage(),
370
700
  });
371
- // operator.message は hold 中も通す。permission.stall も hold 対象外の
372
- // 低優先度回復通知として queue に残る。release 後は held buffer の
373
- // permission.pending を worker より先に選び、max-turns 到達後も dispatch する。
374
- if (selected && isImmediateDispatchSource(selected.batch.sourceKey)) {
375
- input.eventQueue.replaceQueue(selected.remainingQueue);
701
+ const prepareDispatch = (dispatch) => {
702
+ const operatorInputTexts = dispatch.events.flatMap((event) => event.type === 'operator.message'
703
+ ? [event.operatorInput ?? event.text]
704
+ : []);
705
+ const reserved = input.operatorCanInspectRecovery
706
+ ? input.recoveryInbox.reserve(operatorInputTexts)
707
+ : { ok: true, reservation: undefined };
708
+ if (!reserved.ok) {
709
+ if (reserved.reason === 'input_too_large') {
710
+ throw new Error('An operator input reached dispatch without passing the recovery admission limit.');
711
+ }
712
+ if (!capacityNoticeSent) {
713
+ capacityNoticeSent = true;
714
+ input.onRecoveryCapacityBlocked?.();
715
+ }
716
+ return undefined;
717
+ }
376
718
  return {
719
+ events: dispatch.events,
720
+ sourceKey: dispatch.sourceKey,
721
+ operatorInputTexts,
722
+ ...(dispatch.selected ? { selected: dispatch.selected } : {}),
723
+ ...(reserved.reservation ? { reservation: reserved.reservation } : {}),
724
+ ...(dispatch.fromHeldEvents ? { fromHeldEvents: true } : {}),
725
+ };
726
+ };
727
+ // Operator input, reconnect, and permission decisions retain their
728
+ // immediate priority. Lower-priority events must wait while a released hold
729
+ // contains an eligible permission or worker event.
730
+ if (selected && isImmediateDispatchSource(selected.batch.sourceKey)) {
731
+ const dispatch = prepareDispatch({
377
732
  events: selected.batch.events,
378
733
  sourceKey: selected.batch.sourceKey,
379
734
  selected,
380
- };
735
+ });
736
+ if (dispatch)
737
+ return dispatch;
738
+ await waitForDispatchChange(input);
739
+ continue;
381
740
  }
382
741
  if (!input.dispatchHoldState.dispatchHold &&
383
742
  input.dispatchHoldState.heldEvents.length > 0) {
384
743
  const heldEvents = input.dispatchHoldState.heldEvents;
385
- if (heldEvents.every((event) => canDispatchConductorSend(event, input.autonomousTurns, input.maxTurns))) {
386
- const events = heldEvents.splice(0);
387
- notifyHeldDispatchProgress(input);
388
- return {
389
- events,
390
- sourceKey: 'dispatch-hold',
391
- };
392
- }
393
- // max-turns blocks worker/GitHub events, but held permission.pending events
394
- // remain dispatchable. Reuse the normal selector to preserve their priority.
395
- const selectedHeld = selectDispatchBatch({
396
- queue: heldEvents,
397
- state: input.dispatchBatchState,
398
- autonomousTurns: input.autonomousTurns,
399
- maxTurns: input.maxTurns,
400
- });
401
- if (selectedHeld) {
402
- input.dispatchHoldState.heldEvents = selectedHeld.remainingQueue;
403
- notifyHeldDispatchProgress(input);
404
- return {
405
- events: selectedHeld.batch.events,
406
- sourceKey: selectedHeld.batch.sourceKey,
407
- selected: selectedHeld,
408
- };
744
+ const allHeldEventsDispatchable = heldEvents.every((event) => canDispatchConductorSend(event, input.autonomousTurns, input.maxTurns));
745
+ const selectedHeld = allHeldEventsDispatchable
746
+ ? undefined
747
+ : selectDispatchBatch({
748
+ queue: heldEvents,
749
+ state: input.dispatchBatchState,
750
+ autonomousTurns: input.autonomousTurns,
751
+ maxTurns: input.maxTurns,
752
+ });
753
+ const selectedHeldBatch = selectedHeld?.batch ??
754
+ (allHeldEventsDispatchable
755
+ ? { events: [...heldEvents], sourceKey: 'dispatch-hold' }
756
+ : undefined);
757
+ if (selectedHeldBatch && selectedHeldBatch.events.length > 0) {
758
+ const dispatch = prepareDispatch({
759
+ events: selectedHeldBatch.events,
760
+ sourceKey: selectedHeldBatch.sourceKey,
761
+ ...(selectedHeld ? { selected: selectedHeld } : {}),
762
+ fromHeldEvents: true,
763
+ });
764
+ if (dispatch)
765
+ return dispatch;
766
+ await waitForDispatchChange(input);
767
+ continue;
409
768
  }
410
769
  }
411
- if (selected) {
412
- input.eventQueue.replaceQueue(selected.remainingQueue);
413
- return {
770
+ if (selected && selected.batch.events.length > 0) {
771
+ const dispatch = prepareDispatch({
414
772
  events: selected.batch.events,
415
773
  sourceKey: selected.batch.sourceKey,
416
774
  selected,
417
- };
775
+ });
776
+ if (dispatch)
777
+ return dispatch;
778
+ await waitForDispatchChange(input);
779
+ continue;
780
+ }
781
+ await waitForDispatchChange(input);
782
+ }
783
+ }
784
+ function isImmediateDispatchSource(sourceKey) {
785
+ return (sourceKey === 'operator' ||
786
+ sourceKey === 'operator.reconnect' ||
787
+ sourceKey === 'permission');
788
+ }
789
+ function waitForDispatchChange(input) {
790
+ if (input.signal.aborted) {
791
+ return Promise.reject(new DOMException('Conductor dispatch wait aborted', 'AbortError'));
792
+ }
793
+ return new Promise((resolve, reject) => {
794
+ const subscriptions = [];
795
+ const queuedOperatorMessages = input.eventQueue
796
+ .snapshot()
797
+ .filter((event) => event.type === 'operator.message').length;
798
+ const cleanup = () => {
799
+ for (const unsubscribe of subscriptions)
800
+ unsubscribe();
801
+ input.signal.removeEventListener('abort', onAbort);
802
+ };
803
+ const onChange = () => {
804
+ cleanup();
805
+ resolve();
806
+ };
807
+ const onAbort = () => {
808
+ cleanup();
809
+ reject(new DOMException('Conductor dispatch wait aborted', 'AbortError'));
810
+ };
811
+ const onQueueChanged = () => {
812
+ const nextOperatorMessageCount = input.eventQueue
813
+ .snapshot()
814
+ .filter((event) => event.type === 'operator.message').length;
815
+ if (nextOperatorMessageCount > queuedOperatorMessages) {
816
+ input.onOperatorQueueChanged?.();
817
+ }
818
+ onChange();
819
+ };
820
+ subscriptions.push(input.eventQueue.subscribe(onQueueChanged), input.lifecycle.subscribe(onChange), input.recoveryInbox.subscribe(onChange));
821
+ if (input.signal.aborted) {
822
+ onAbort();
418
823
  }
419
- const hadQueued = input.eventQueue.size > 0;
420
- const incoming = await input.eventQueue.waitForEvent(input.signal, {
421
- onlyNew: hadQueued,
824
+ else {
825
+ input.signal.addEventListener('abort', onAbort, { once: true });
826
+ }
827
+ });
828
+ }
829
+ async function waitForCursorBootstrapDecision(input) {
830
+ while (input.state.status === 'unknown' ||
831
+ input.state.status === 'saving') {
832
+ if (input.signal.aborted) {
833
+ throw new DOMException('Cursor bootstrap decision wait aborted', 'AbortError');
834
+ }
835
+ await new Promise((resolve, reject) => {
836
+ let unsubscribe = () => { };
837
+ let unsubscribeFromEventQueue = () => { };
838
+ let queuedOperatorMessages = input.eventQueue
839
+ .snapshot()
840
+ .filter((event) => event.type === 'operator.message').length;
841
+ if (queuedOperatorMessages > 0) {
842
+ input.onOperatorQueueChanged?.();
843
+ }
844
+ const cleanup = () => {
845
+ unsubscribe();
846
+ unsubscribeFromEventQueue();
847
+ input.signal.removeEventListener('abort', onAbort);
848
+ };
849
+ const finish = () => {
850
+ cleanup();
851
+ resolve();
852
+ };
853
+ const onStatusChanged = () => {
854
+ if (input.state.status === 'unknown' ||
855
+ input.state.status === 'saving') {
856
+ return;
857
+ }
858
+ finish();
859
+ };
860
+ const onQueueChanged = () => {
861
+ const nextOperatorMessageCount = input.eventQueue
862
+ .snapshot()
863
+ .filter((event) => event.type === 'operator.message').length;
864
+ if (nextOperatorMessageCount > queuedOperatorMessages) {
865
+ input.onOperatorQueueChanged?.();
866
+ }
867
+ queuedOperatorMessages = nextOperatorMessageCount;
868
+ };
869
+ const onAbort = () => {
870
+ cleanup();
871
+ reject(new DOMException('Cursor bootstrap decision wait aborted', 'AbortError'));
872
+ };
873
+ unsubscribe = input.lifecycle.subscribe(onStatusChanged);
874
+ unsubscribeFromEventQueue = input.eventQueue.subscribe(onQueueChanged);
875
+ if (input.signal.aborted) {
876
+ onAbort();
877
+ }
878
+ else {
879
+ input.signal.addEventListener('abort', onAbort, { once: true });
880
+ onStatusChanged();
881
+ }
422
882
  });
423
- // waiter 経由のイベントは queue に載らない。到着順を保つため先頭へ戻す。
424
- input.eventQueue.prependSilent(incoming);
425
- if (incoming.type === 'operator.message')
426
- input.onOperatorQueueChanged?.();
427
883
  }
428
884
  }
429
885
  function canDispatchIdleOperatorMessage(input) {
@@ -451,26 +907,24 @@ function canDispatchIdleOperatorMessage(input) {
451
907
  maxTurns: input.maxTurns,
452
908
  }) === undefined);
453
909
  }
454
- function isImmediateDispatchSource(sourceKey) {
455
- return (sourceKey === 'operator' ||
456
- sourceKey === 'operator.reconnect' ||
457
- sourceKey === 'permission');
458
- }
459
910
  /** `/reconnect` は conductor へ送らず、直近 send 完了後に agent だけ差し替える。 */
460
911
  async function runOperatorReconnect(options) {
461
912
  try {
462
913
  await reconnectConductorAgent(options.conductorHandle, {
463
914
  conductorAgentFactory: options.sendReconnect.conductorAgentFactory,
464
915
  conductorOptions: options.sendReconnect.conductorOptions,
916
+ signal: options.shutdownSignal,
465
917
  onReconnectAttempt: options.sendReconnect.onTransportReconnectAttempt ??
466
918
  options.sendReconnect.onReconnectAttempt,
467
919
  onReconnectComplete: options.sendReconnect.onTransportReconnectComplete ??
468
920
  options.sendReconnect.onReconnectComplete,
469
921
  });
922
+ return true;
470
923
  }
471
924
  catch {
472
925
  // Reconnect failures are observable through the callback above. Keep the
473
926
  // session alive so the operator can retry or submit another message.
927
+ return false;
474
928
  }
475
929
  }
476
930
  function notifyHeldDispatchProgress(input) {