@agents-ensemble/core 0.21.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 (266) 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 +2 -3
  43. package/dist/conductor/conductor-auth.d.ts.map +1 -1
  44. package/dist/conductor/conductor-auth.js +5 -2
  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 +6 -0
  51. package/dist/conductor/conductor-history.d.ts.map +1 -1
  52. package/dist/conductor/conductor-history.js +11 -6
  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 +27 -5
  67. package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
  68. package/dist/conductor/conductor-send-reconnect.js +88 -39
  69. package/dist/conductor/conductor-send-reconnect.js.map +1 -1
  70. package/dist/conductor/conductor-session-driver.d.ts +32 -7
  71. package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
  72. package/dist/conductor/conductor-session-driver.js +474 -233
  73. package/dist/conductor/conductor-session-driver.js.map +1 -1
  74. package/dist/conductor/conductor-session.d.ts +2 -0
  75. package/dist/conductor/conductor-session.d.ts.map +1 -1
  76. package/dist/conductor/conductor-session.js +404 -156
  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 +15 -6
  91. package/dist/conductor/index.d.ts.map +1 -1
  92. package/dist/conductor/index.js +8 -4
  93. package/dist/conductor/index.js.map +1 -1
  94. package/dist/conductor/operator-command.d.ts +1 -1
  95. package/dist/conductor/operator-command.js +1 -1
  96. package/dist/conductor/operator-command.js.map +1 -1
  97. package/dist/conductor/operator-input-binding.d.ts +12 -0
  98. package/dist/conductor/operator-input-binding.d.ts.map +1 -1
  99. package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts +4 -1
  100. package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts.map +1 -1
  101. package/dist/conductor/pi-auth-recovery-snapshot-store.js +4 -1
  102. package/dist/conductor/pi-auth-recovery-snapshot-store.js.map +1 -1
  103. package/dist/conductor/pi-conductor-agent.d.ts +6 -3
  104. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  105. package/dist/conductor/pi-conductor-agent.js +272 -53
  106. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  107. package/dist/conductor/pi-conductor-session-file.d.ts +13 -0
  108. package/dist/conductor/pi-conductor-session-file.d.ts.map +1 -0
  109. package/dist/conductor/pi-conductor-session-file.js +21 -0
  110. package/dist/conductor/pi-conductor-session-file.js.map +1 -0
  111. package/dist/conductor/pi-resource-loader.d.ts +6 -0
  112. package/dist/conductor/pi-resource-loader.d.ts.map +1 -1
  113. package/dist/conductor/pi-resource-loader.js +26 -12
  114. package/dist/conductor/pi-resource-loader.js.map +1 -1
  115. package/dist/conductor/pi-user-root-lock.d.ts +8 -0
  116. package/dist/conductor/pi-user-root-lock.d.ts.map +1 -0
  117. package/dist/conductor/pi-user-root-lock.js +53 -0
  118. package/dist/conductor/pi-user-root-lock.js.map +1 -0
  119. package/dist/conductor/recovery-inbox-command.d.ts +29 -0
  120. package/dist/conductor/recovery-inbox-command.d.ts.map +1 -0
  121. package/dist/conductor/recovery-inbox-command.js +248 -0
  122. package/dist/conductor/recovery-inbox-command.js.map +1 -0
  123. package/dist/conductor/session/events/session-event.d.ts +15 -2
  124. package/dist/conductor/session/events/session-event.d.ts.map +1 -1
  125. package/dist/conductor/session/events/session-log-event-groups.d.ts +1 -1
  126. package/dist/conductor/session/events/session-log-event-groups.d.ts.map +1 -1
  127. package/dist/conductor/session/events/session-log-event-groups.js +1 -0
  128. package/dist/conductor/session/events/session-log-event-groups.js.map +1 -1
  129. package/dist/conductor/session/events/session-log-event.d.ts +4 -0
  130. package/dist/conductor/session/events/session-log-event.d.ts.map +1 -1
  131. package/dist/conductor/session/format-session-event.js +25 -4
  132. package/dist/conductor/session/format-session-event.js.map +1 -1
  133. package/dist/conductor/session/select-dispatch-batch.d.ts +2 -0
  134. package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
  135. package/dist/conductor/session/select-dispatch-batch.js +31 -2
  136. package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
  137. package/dist/conductor/session/session-event-queue.d.ts +5 -0
  138. package/dist/conductor/session/session-event-queue.d.ts.map +1 -1
  139. package/dist/conductor/session/session-event-queue.js +41 -4
  140. package/dist/conductor/session/session-event-queue.js.map +1 -1
  141. package/dist/conductor/session/session-event.d.ts +1 -1
  142. package/dist/conductor/session/session-event.d.ts.map +1 -1
  143. package/dist/conductor/session-policy.d.ts.map +1 -1
  144. package/dist/conductor/session-policy.js +4 -2
  145. package/dist/conductor/session-policy.js.map +1 -1
  146. package/dist/conductor/submit-operator-input.d.ts +3 -0
  147. package/dist/conductor/submit-operator-input.d.ts.map +1 -1
  148. package/dist/conductor/submit-operator-input.js +21 -0
  149. package/dist/conductor/submit-operator-input.js.map +1 -1
  150. package/dist/config/deep-merge.d.ts.map +1 -1
  151. package/dist/config/deep-merge.js +13 -3
  152. package/dist/config/deep-merge.js.map +1 -1
  153. package/dist/config/defaults.d.ts.map +1 -1
  154. package/dist/config/defaults.js +1 -1
  155. package/dist/config/defaults.js.map +1 -1
  156. package/dist/config/doctor-settings.d.ts +59 -0
  157. package/dist/config/doctor-settings.d.ts.map +1 -0
  158. package/dist/config/doctor-settings.js +343 -0
  159. package/dist/config/doctor-settings.js.map +1 -0
  160. package/dist/config/ensemble-config-digest.d.ts +2 -4
  161. package/dist/config/ensemble-config-digest.d.ts.map +1 -1
  162. package/dist/config/ensemble-config-digest.js +2 -4
  163. package/dist/config/ensemble-config-digest.js.map +1 -1
  164. package/dist/config/index.d.ts +3 -1
  165. package/dist/config/index.d.ts.map +1 -1
  166. package/dist/config/index.js +1 -0
  167. package/dist/config/index.js.map +1 -1
  168. package/dist/config/load-ensemble-config.d.ts.map +1 -1
  169. package/dist/config/load-ensemble-config.js +8 -1
  170. package/dist/config/load-ensemble-config.js.map +1 -1
  171. package/dist/config/parse-config.d.ts.map +1 -1
  172. package/dist/config/parse-config.js +34 -14
  173. package/dist/config/parse-config.js.map +1 -1
  174. package/dist/config/resolve-settings.d.ts +3 -1
  175. package/dist/config/resolve-settings.d.ts.map +1 -1
  176. package/dist/config/resolve-settings.js +7 -4
  177. package/dist/config/resolve-settings.js.map +1 -1
  178. package/dist/config/types.d.ts +14 -5
  179. package/dist/config/types.d.ts.map +1 -1
  180. package/dist/dispatch/attach-worker.js +1 -1
  181. package/dist/dispatch/attach-worker.js.map +1 -1
  182. package/dist/dispatch/worker-acp-session.d.ts +21 -3
  183. package/dist/dispatch/worker-acp-session.d.ts.map +1 -1
  184. package/dist/dispatch/worker-acp-session.js +50 -13
  185. package/dist/dispatch/worker-acp-session.js.map +1 -1
  186. package/dist/ensemble-paths.d.ts +19 -19
  187. package/dist/ensemble-paths.d.ts.map +1 -1
  188. package/dist/ensemble-paths.js +40 -22
  189. package/dist/ensemble-paths.js.map +1 -1
  190. package/dist/issue/resolve-issue-url.d.ts +4 -0
  191. package/dist/issue/resolve-issue-url.d.ts.map +1 -1
  192. package/dist/issue/resolve-issue-url.js +2 -2
  193. package/dist/issue/resolve-issue-url.js.map +1 -1
  194. package/dist/operator-runtime.d.ts +17 -4
  195. package/dist/operator-runtime.d.ts.map +1 -1
  196. package/dist/operator-runtime.js +12 -3
  197. package/dist/operator-runtime.js.map +1 -1
  198. package/dist/profile/types.d.ts +4 -0
  199. package/dist/profile/types.d.ts.map +1 -1
  200. package/dist/profile/types.js +29 -12
  201. package/dist/profile/types.js.map +1 -1
  202. package/dist/representation/escape-terminal-control-characters.d.ts +9 -0
  203. package/dist/representation/escape-terminal-control-characters.d.ts.map +1 -0
  204. package/dist/representation/escape-terminal-control-characters.js +34 -0
  205. package/dist/representation/escape-terminal-control-characters.js.map +1 -0
  206. package/dist/representation/index.d.ts +1 -0
  207. package/dist/representation/index.d.ts.map +1 -1
  208. package/dist/representation/index.js +1 -0
  209. package/dist/representation/index.js.map +1 -1
  210. package/dist/runtime/types.d.ts +9 -0
  211. package/dist/runtime/types.d.ts.map +1 -1
  212. package/dist/runtime/worker-runtime.d.ts +14 -1
  213. package/dist/runtime/worker-runtime.d.ts.map +1 -1
  214. package/dist/runtime/worker-runtime.js +197 -50
  215. package/dist/runtime/worker-runtime.js.map +1 -1
  216. package/dist/runtime/worker-session.d.ts +2 -1
  217. package/dist/runtime/worker-session.d.ts.map +1 -1
  218. package/dist/runtime/worker-session.js +1 -0
  219. package/dist/runtime/worker-session.js.map +1 -1
  220. package/dist/session/export-session-bundle.d.ts +77 -0
  221. package/dist/session/export-session-bundle.d.ts.map +1 -0
  222. package/dist/session/export-session-bundle.js +640 -0
  223. package/dist/session/export-session-bundle.js.map +1 -0
  224. package/dist/session/index.d.ts +4 -3
  225. package/dist/session/index.d.ts.map +1 -1
  226. package/dist/session/index.js +2 -1
  227. package/dist/session/index.js.map +1 -1
  228. package/dist/session/open-question-control.d.ts +9 -0
  229. package/dist/session/open-question-control.d.ts.map +1 -1
  230. package/dist/session/open-question-control.js +26 -1
  231. package/dist/session/open-question-control.js.map +1 -1
  232. package/dist/session/open-questions-store.d.ts +1 -1
  233. package/dist/session/open-questions-store.d.ts.map +1 -1
  234. package/dist/session/open-questions-store.js +3 -6
  235. package/dist/session/open-questions-store.js.map +1 -1
  236. package/dist/session/persisted-file.d.ts +5 -0
  237. package/dist/session/persisted-file.d.ts.map +1 -0
  238. package/dist/session/persisted-file.js +56 -0
  239. package/dist/session/persisted-file.js.map +1 -0
  240. package/dist/session/session-resume-store.d.ts +77 -10
  241. package/dist/session/session-resume-store.d.ts.map +1 -1
  242. package/dist/session/session-resume-store.js +346 -83
  243. package/dist/session/session-resume-store.js.map +1 -1
  244. package/dist/session/session-snapshot-paths.d.ts +7 -1
  245. package/dist/session/session-snapshot-paths.d.ts.map +1 -1
  246. package/dist/session/session-snapshot-paths.js +29 -26
  247. package/dist/session/session-snapshot-paths.js.map +1 -1
  248. package/dist/session/session-state-store.d.ts +2 -2
  249. package/dist/session/session-state-store.d.ts.map +1 -1
  250. package/dist/session/session-state-store.js +5 -16
  251. package/dist/session/session-state-store.js.map +1 -1
  252. package/dist/session/tool-call-snapshot-history.d.ts +12 -1
  253. package/dist/session/tool-call-snapshot-history.d.ts.map +1 -1
  254. package/dist/session/tool-call-snapshot-history.js +75 -70
  255. package/dist/session/tool-call-snapshot-history.js.map +1 -1
  256. package/dist/session/worker-session-history.d.ts +13 -1
  257. package/dist/session/worker-session-history.d.ts.map +1 -1
  258. package/dist/session/worker-session-history.js +171 -50
  259. package/dist/session/worker-session-history.js.map +1 -1
  260. package/dist/workspace/index.d.ts +1 -1
  261. package/dist/workspace/index.d.ts.map +1 -1
  262. package/dist/worktree/worktree.d.ts +6 -1
  263. package/dist/worktree/worktree.d.ts.map +1 -1
  264. package/dist/worktree/worktree.js +123 -2
  265. package/dist/worktree/worktree.js.map +1 -1
  266. package/package.json +9 -4
@@ -1,5 +1,6 @@
1
1
  import { ensureMaxTurnsOpenQuestion } from '../escalation/enqueue-max-turns-question.js';
2
- import { isPiApiKeyResolutionFailure } from './conductor-auth.js';
2
+ import { ConductorDispatchLifecycle, settleConductorDispatch, } from './conductor-dispatch-lifecycle.js';
3
+ import { ConductorRecoveryInbox } from './conductor-recovery-inbox.js';
3
4
  import { reconnectConductorAgent, sendConductorWithReconnect, } from './conductor-send-reconnect.js';
4
5
  import { formatSessionEventsForConductor } from './session/format-session-event.js';
5
6
  import { bufferDispatchHoldEvents, createDispatchHoldState, pruneStalePermissionEvents, } from './session/dispatch-hold.js';
@@ -20,15 +21,85 @@ export async function runConductorSessionDriver(options) {
20
21
  runId: '',
21
22
  status: 'finished',
22
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;
23
31
  let inFlightSend;
24
32
  let postLoopWaiting = false;
33
+ const lifecycle = options.dispatchLifecycle ??
34
+ new ConductorDispatchLifecycle();
35
+ const recoveryInbox = options.recoveryInbox ?? new ConductorRecoveryInbox();
25
36
  const dispatchHoldState = options.dispatchHoldState ?? createDispatchHoldState();
26
- let conductorAvailable = true;
27
- let pendingAuthRecoveryCandidate;
28
- let pendingRecoveryReport;
29
- const sendReconnect = withAgentAvailabilityCallbacks(options.sendReconnect, (available) => {
30
- conductorAvailable = available;
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
+ }
31
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
+ };
32
103
  const hasDispatchableWorkerOutcome = () => selectDispatchBatch({
33
104
  queue: options.eventQueue.snapshot().filter((event) => event.type === 'worker.completed' || event.type === 'worker.failed'),
34
105
  state: dispatchBatchState,
@@ -45,7 +116,14 @@ export async function runConductorSessionDriver(options) {
45
116
  const holdBlocksStop = dispatchHoldState.heldEvents.length > 0 &&
46
117
  options.stopOnUnansweredInput !== true &&
47
118
  loopState.lastStatus !== 'error' &&
119
+ loopState.lastStatus !== 'send_rejected' &&
48
120
  loopState.lastStatus !== 'cancelled';
121
+ if (lifecycle.confirmationRequired &&
122
+ options.operatorCanInspectRecovery !== true) {
123
+ stopReason =
124
+ loopState.lastStatus === 'cancelled' ? 'cancelled' : 'error';
125
+ return true;
126
+ }
49
127
  if (holdBlocksStop) {
50
128
  postLoopWaiting = false;
51
129
  return false;
@@ -54,6 +132,13 @@ export async function runConductorSessionDriver(options) {
54
132
  postLoopWaiting = false;
55
133
  return false;
56
134
  }
135
+ if (options.operatorCanInspectRecovery && lifecycle.confirmationRequired) {
136
+ if (!postLoopWaiting) {
137
+ postLoopWaiting = true;
138
+ options.onIssueLoopStop?.();
139
+ }
140
+ return false;
141
+ }
57
142
  // A worker publishes its outcome to the inbox before its runtime round
58
143
  // leaves the running state. Drain that notification chain before
59
144
  // deciding the session is idle, otherwise the driver can tear down in
@@ -79,15 +164,69 @@ export async function runConductorSessionDriver(options) {
79
164
  }
80
165
  return false;
81
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
+ }
82
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;
83
222
  options.onConductorInbound?.({
84
223
  triggers: 1,
85
224
  sources: ['initial'],
86
225
  });
87
- lastSendResult = await runEventConductorSend({
88
- // Keep the initial dispatch asynchronous as it was when the driver
89
- // fetched and compiled the prompt itself.
90
- 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,
91
230
  conductorHandle: options.conductorHandle,
92
231
  sendReconnect,
93
232
  signal: options.shutdownSignal,
@@ -100,20 +239,80 @@ export async function runConductorSessionDriver(options) {
100
239
  resetOutboundDispatchesThisSend: options.resetOutboundDispatchesThisSend,
101
240
  getOutboundDispatchesThisSend: options.getOutboundDispatchesThisSend,
102
241
  onSendStarted: options.onSendStarted,
103
- onSendProgress: options.onSendProgress,
242
+ onSendProgress: (info) => {
243
+ initialToolCallStarted = true;
244
+ options.onSendProgress?.(info);
245
+ },
104
246
  onSendComplete: (info) => {
105
247
  sendCount = info.sendCount;
106
248
  lastDispatchesThisTurn = info.conductorDispatchesThisTurn;
107
249
  autonomousTurns = info.autonomousTurns;
108
250
  options.onSendComplete(info);
109
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;
110
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;
111
283
  }
112
284
  while (true) {
113
285
  if (options.shutdownSignal.aborted) {
114
286
  stopReason = 'interrupted';
115
287
  break;
116
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
+ }
117
316
  if (options.isPermissionPending) {
118
317
  pruneStalePermissionEvents({
119
318
  state: dispatchHoldState,
@@ -182,7 +381,8 @@ export async function runConductorSessionDriver(options) {
182
381
  const shouldBreak = await shouldBreakAfterLoopState(loopState);
183
382
  const idleOperatorMessageReady = queuedDispatch?.batch.events.some((event) => event.type === 'operator.message' && event.delivery === 'idle');
184
383
  if (shouldBreak &&
185
- (!idleOperatorMessageReady || options.stopOnUnansweredInput === true)) {
384
+ (!idleOperatorMessageReady ||
385
+ options.stopOnUnansweredInput === true)) {
186
386
  break;
187
387
  }
188
388
  continue;
@@ -214,13 +414,15 @@ export async function runConductorSessionDriver(options) {
214
414
  dispatchBatchState,
215
415
  autonomousTurns,
216
416
  maxTurns: options.maxTurns,
417
+ lifecycle,
418
+ recoveryInbox,
419
+ operatorCanInspectRecovery: options.operatorCanInspectRecovery === true,
217
420
  signal: options.shutdownSignal,
218
421
  dispatchHoldState,
219
422
  onDispatchHoldChanged: options.onDispatchHoldChanged,
220
423
  onOperatorQueueChanged: options.onOperatorQueueChanged,
424
+ onRecoveryCapacityBlocked: options.onRecoveryCapacityBlocked,
221
425
  isPermissionPending: options.isPermissionPending,
222
- isConductorAvailable: () => conductorAvailable,
223
- hasPendingRecoveryReport: () => pendingRecoveryReport !== undefined,
224
426
  canDispatchIdleOperatorMessage: () => canDispatchIdleOperatorMessage({
225
427
  eventQueue: options.eventQueue,
226
428
  dispatchHoldState,
@@ -242,38 +444,56 @@ export async function runConductorSessionDriver(options) {
242
444
  continue;
243
445
  }
244
446
  if (options.shutdownSignal.aborted) {
447
+ if (dispatchResult.reservation) {
448
+ recoveryInbox.release(dispatchResult.reservation);
449
+ }
245
450
  stopReason = 'interrupted';
246
451
  break;
247
452
  }
248
- const { events: selectedBatch, selected } = dispatchResult;
453
+ const { events: batch, selected, reservation } = dispatchResult;
249
454
  postLoopWaiting = false;
250
- if (selected) {
251
- dispatchBatchState = markContinuationConsumed(dispatchBatchState, selected);
252
- }
253
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
+ }
254
463
  dispatchBatchState = dispatchBatchStateAfterSend(dispatchResult.sourceKey);
255
464
  const reconnected = await runOperatorReconnect(options);
256
- conductorAvailable = reconnected;
465
+ lifecycle.settleReconnect(reconnectLease, reconnected);
257
466
  if (reconnected && !options.shutdownSignal.aborted) {
258
- // 再接続の await 中に届いた reconnect は今回の成功で満たされた。
259
- // 新しい handle を重ねて close / resume しないよう、ここでまとめて消費する。
467
+ // 再接続待ちの間に積まれた /reconnect は今回の復旧で満たされた。
260
468
  options.eventQueue.findSendEvents((event) => event.type === 'operator.reconnect');
261
- if (pendingAuthRecoveryCandidate) {
262
- pendingRecoveryReport = {
263
- type: 'conductor.recovery.report',
264
- ...pendingAuthRecoveryCandidate,
265
- reconnect: { kind: 'manual', succeeded: true },
266
- };
267
- pendingAuthRecoveryCandidate = undefined;
268
- }
269
469
  }
270
470
  continue;
271
471
  }
272
- const recoveryReport = pendingRecoveryReport;
273
- const batch = recoveryReport
274
- ? [recoveryReport, ...selectedBatch]
275
- : selectedBatch;
276
- const originalEvents = batch.filter((event) => event.type !== 'conductor.recovery.report');
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);
481
+ continue;
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
+ }
277
497
  const autonomousTurnsAfter = autonomousTurnsAfterConductorBatch(batch, autonomousTurns);
278
498
  const { workerDispatches, workerFailures } = countWorkerOutcomesInBatch(batch);
279
499
  options.onOperatorDispatched?.(batch);
@@ -281,18 +501,15 @@ export async function runConductorSessionDriver(options) {
281
501
  triggers: batch.filter(isTriggerSessionEvent).length,
282
502
  sources: uniqueSources(batch, dispatchResult.sourceKey),
283
503
  });
284
- // 通常 send を開始したら未報告 candidate は失効する。
285
- // 準備済み report はこの send に一度だけ加え、開始時に消費して replay しない。
286
- const message = formatSessionEventsForConductor(batch);
287
- const recoverySnapshotContent = recoveryReport
288
- ? formatSessionEventsForConductor(originalEvents)
289
- : message;
290
- pendingRecoveryReport = undefined;
291
- pendingAuthRecoveryCandidate = undefined;
504
+ const eventMessage = formatSessionEventsForConductor(batch);
505
+ const restoreCursorBootstrap = isCursorBootstrapPending();
506
+ const message = restoreCursorBootstrap
507
+ ? `${options.initialPrompt}\n\n## 今回のセッションイベント\n\n${eventMessage}`
508
+ : eventMessage;
292
509
  let dispatchHadToolCalls = false;
293
- let automaticRetryAttempts = 0;
294
510
  const dispatchSend = runEventConductorSend({
295
511
  message,
512
+ ...(restoreCursorBootstrap ? { historyInbound: eventMessage } : {}),
296
513
  conductorHandle: options.conductorHandle,
297
514
  sendReconnect,
298
515
  signal: options.shutdownSignal,
@@ -313,9 +530,6 @@ export async function runConductorSessionDriver(options) {
313
530
  dispatchHadToolCalls = true;
314
531
  options.onSendProgress?.(info);
315
532
  },
316
- onAutomaticRetryAttempt: ({ attempt }) => {
317
- automaticRetryAttempts = attempt;
318
- },
319
533
  onSendComplete: (info) => {
320
534
  sendCount = info.sendCount;
321
535
  lastDispatchesThisTurn = info.conductorDispatchesThisTurn;
@@ -323,38 +537,31 @@ export async function runConductorSessionDriver(options) {
323
537
  options.onSendComplete(info);
324
538
  },
325
539
  }).then((result) => {
326
- if (isPiApiKeyResolutionFailure(result) && originalEvents.length > 0) {
327
- // Pi は1回の send で複数回 auth を解決するため、後続の失敗から
328
- // 先行 tool round に副作用がなかったとは判断できない。
329
- // event は再送せず、report 用 candidate と operator-only snapshot を別々に保持する。
330
- pendingAuthRecoveryCandidate = createPiAuthRecoveryCandidate({
331
- events: originalEvents,
332
- sendCount,
333
- runId: result.runId,
334
- sourceKey: dispatchResult.sourceKey,
335
- automaticRetryAttempts,
336
- toolCallStarted: dispatchHadToolCalls,
337
- });
338
- const storedSnapshot = options.piAuthRecoverySnapshotStore?.add(pendingAuthRecoveryCandidate, recoverySnapshotContent);
339
- if (storedSnapshot) {
340
- options.onPiAuthRecoverySnapshotStored?.(storedSnapshot);
341
- }
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
+ : {}),
557
+ });
558
+ if (options.onDispatchBatchSettled &&
559
+ batch.some((event) => event.type === 'operator.message')) {
560
+ options.onDispatchBatchSettled({ events: batch, result });
342
561
  }
343
562
  return result;
344
563
  });
345
- if (options.onDispatchBatchSettled &&
346
- batch.some((event) => event.type === 'operator.message')) {
347
- inFlightSend = dispatchSend.then((result) => {
348
- options.onDispatchBatchSettled?.({ events: batch, result });
349
- return result;
350
- }, (error) => {
351
- options.onDispatchBatchSettled?.({ events: batch, error });
352
- throw error;
353
- });
354
- }
355
- else {
356
- inFlightSend = dispatchSend;
357
- }
564
+ inFlightSend = dispatchSend;
358
565
  dispatchBatchState = dispatchBatchStateAfterSend(dispatchResult.sourceKey);
359
566
  }
360
567
  if (inFlightSend && stopReason !== 'interrupted') {
@@ -372,95 +579,15 @@ function uniqueSources(batch, fallback) {
372
579
  const sources = [...new Set(batch.map((event) => eventSourceKey(event)))];
373
580
  return sources.length > 0 ? sources : [fallback];
374
581
  }
375
- const MAX_RECOVERY_EVENT_REFERENCES = 20;
376
- function createPiAuthRecoveryCandidate(input) {
377
- const eventReferences = input.events.map((event, index) => ({
378
- eventIndex: index + 1,
379
- ...recoveryEventReference(event),
380
- }));
381
- return {
382
- recoveryId: `pi-auth-recovery-${input.sendCount}`,
383
- failedDispatch: {
384
- sendCount: input.sendCount,
385
- runId: input.runId,
386
- sourceKey: input.sourceKey,
387
- eventTypes: [...new Set(input.events.map((event) => event.type))],
388
- eventCount: input.events.length,
389
- eventReferences: eventReferences.slice(0, MAX_RECOVERY_EVENT_REFERENCES),
390
- eventReferencesTruncated: eventReferences.length > MAX_RECOVERY_EVENT_REFERENCES,
391
- },
392
- failure: {
393
- kind: 'pi_api_key_resolution',
394
- automaticRetryAttempts: input.automaticRetryAttempts,
395
- toolCallStarted: input.toolCallStarted,
396
- },
397
- };
398
- }
399
- function recoveryEventReference(event) {
400
- switch (event.type) {
401
- case 'operator.message':
402
- return {
403
- type: event.type,
404
- reference: `delivery:${event.delivery ?? 'next'}`,
405
- };
406
- case 'operator.reconnect':
407
- return { type: event.type };
408
- case 'worker.completed':
409
- return {
410
- type: event.type,
411
- reference: `worker:${event.result.name}:${event.result.kind}`,
412
- };
413
- case 'worker.failed':
414
- return {
415
- type: event.type,
416
- reference: `worker:${event.failure.name}:${event.failure.kind}`,
417
- };
418
- case 'permission.pending':
419
- return {
420
- type: event.type,
421
- reference: `permission:${event.permission.id}`,
422
- };
423
- case 'permission.stall':
424
- return {
425
- type: event.type,
426
- reference: summarizeRecoveryReferences('permissions', event.pendingPermissionIds),
427
- };
428
- case 'harness.connectivity.recovered':
429
- return {
430
- type: event.type,
431
- reference: `${event.source}:${event.suppressedErrorCount}`,
432
- };
433
- case 'conductor.recovery.report':
434
- return {
435
- type: event.type,
436
- reference: `recovery:${event.recoveryId}`,
437
- };
438
- case 'github.update':
439
- return {
440
- type: event.type,
441
- reference: summarizeRecoveryReferences('items', event.items.map((item) => item.id)),
442
- };
443
- default: {
444
- const _exhaustive = event;
445
- return { type: String(_exhaustive) };
446
- }
447
- }
448
- }
449
- function summarizeRecoveryReferences(prefix, values) {
450
- const sampleCount = 5;
451
- const sample = values.slice(0, sampleCount).join(',');
452
- const remaining = values.length - sampleCount;
453
- return `${prefix}:${sample}${remaining > 0 ? `,+${remaining} more` : ''}`;
454
- }
455
582
  function withAgentAvailabilityCallbacks(options, onAvailabilityChanged) {
456
583
  return {
457
584
  ...options,
458
585
  onAuthReconnectComplete: (info) => {
459
- onAvailabilityChanged(info.success);
586
+ onAvailabilityChanged('auth', info);
460
587
  (options.onAuthReconnectComplete ?? options.onReconnectComplete)?.(info);
461
588
  },
462
589
  onTransportReconnectComplete: (info) => {
463
- onAvailabilityChanged(info.success);
590
+ onAvailabilityChanged('transport', info);
464
591
  (options.onTransportReconnectComplete ?? options.onReconnectComplete)?.(info);
465
592
  },
466
593
  };
@@ -488,11 +615,14 @@ function runEventConductorSend(input) {
488
615
  tool: info.tool,
489
616
  });
490
617
  },
491
- onAutomaticRetry: (info) => {
492
- input.onAutomaticRetryAttempt?.(info);
493
- input.sendReconnect.onAutomaticRetry?.(info);
494
- },
495
- }).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) => {
496
626
  const completedDuringSend = input.workerDispatches.length - workersBefore;
497
627
  const outboundDispatches = input.getOutboundDispatchesThisSend?.() ?? 0;
498
628
  const conductorDispatches = Math.max(completedDuringSend, outboundDispatches);
@@ -502,6 +632,9 @@ function runEventConductorSend(input) {
502
632
  runId: sendResult.runId,
503
633
  status: sendResult.status,
504
634
  inbound: input.message,
635
+ ...(input.historyInbound !== undefined
636
+ ? { historyInbound: input.historyInbound }
637
+ : {}),
505
638
  dispatchSource: input.dispatchSource,
506
639
  result: sendResult.result,
507
640
  error: sendResult.error,
@@ -516,10 +649,18 @@ function runEventConductorSend(input) {
516
649
  return sendResult;
517
650
  });
518
651
  }
652
+ function isIssueDispatchCompleted(result) {
653
+ return (result.status === 'finished' ||
654
+ result.status === 'completed' ||
655
+ result.status === 'running');
656
+ }
519
657
  async function waitForDispatchBatch(input) {
658
+ let capacityNoticeSent = false;
520
659
  for (;;) {
521
- if (!input.isConductorAvailable()) {
522
- const queuedReconnects = input.eventQueue.findSendEvents((event) => event.type === 'operator.reconnect');
660
+ if (!input.lifecycle.canDispatch) {
661
+ const queuedReconnects = input.eventQueue
662
+ .snapshot()
663
+ .filter((event) => event.type === 'operator.reconnect');
523
664
  if (queuedReconnects.length > 0) {
524
665
  if (input.eventQueue
525
666
  .snapshot()
@@ -529,20 +670,11 @@ async function waitForDispatchBatch(input) {
529
670
  return {
530
671
  events: queuedReconnects,
531
672
  sourceKey: 'operator.reconnect',
673
+ operatorInputTexts: [],
532
674
  };
533
675
  }
534
- const reconnect = await input.eventQueue.waitForMatchingEvent((event) => event.type === 'operator.reconnect', input.signal);
535
- const reconnects = [
536
- reconnect,
537
- ...input.eventQueue.findSendEvents((event) => event.type === 'operator.reconnect'),
538
- ];
539
- if (input.eventQueue.snapshot().some((event) => event.type === 'operator.message')) {
540
- input.onOperatorQueueChanged?.();
541
- }
542
- return {
543
- events: reconnects,
544
- sourceKey: 'operator.reconnect',
545
- };
676
+ await waitForDispatchChange(input);
677
+ continue;
546
678
  }
547
679
  if (input.dispatchHoldState.dispatchHold) {
548
680
  bufferDispatchHoldEvents({
@@ -566,75 +698,188 @@ async function waitForDispatchBatch(input) {
566
698
  maxTurns: input.maxTurns,
567
699
  allowIdleOperatorMessages: input.canDispatchIdleOperatorMessage(),
568
700
  });
569
- // operator.message は hold 中も通す。permission.stall も hold 対象外の
570
- // 低優先度回復通知として queue に残る。release 後は held buffer の
571
- // permission.pending を worker より先に選び、max-turns 到達後も dispatch する。
572
- if (selected && isImmediateDispatchSource(selected.batch.sourceKey)) {
573
- 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
+ }
574
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({
575
732
  events: selected.batch.events,
576
733
  sourceKey: selected.batch.sourceKey,
577
734
  selected,
578
- };
735
+ });
736
+ if (dispatch)
737
+ return dispatch;
738
+ await waitForDispatchChange(input);
739
+ continue;
579
740
  }
580
741
  if (!input.dispatchHoldState.dispatchHold &&
581
742
  input.dispatchHoldState.heldEvents.length > 0) {
582
743
  const heldEvents = input.dispatchHoldState.heldEvents;
583
- if (heldEvents.every((event) => canDispatchConductorSend(event, input.autonomousTurns, input.maxTurns))) {
584
- const events = heldEvents.splice(0);
585
- notifyHeldDispatchProgress(input);
586
- return {
587
- events,
588
- sourceKey: 'dispatch-hold',
589
- };
590
- }
591
- // max-turns blocks worker/GitHub events, but held permission.pending events
592
- // remain dispatchable. Reuse the normal selector to preserve their priority.
593
- const selectedHeld = selectDispatchBatch({
594
- queue: heldEvents,
595
- state: input.dispatchBatchState,
596
- autonomousTurns: input.autonomousTurns,
597
- maxTurns: input.maxTurns,
598
- });
599
- if (selectedHeld) {
600
- input.dispatchHoldState.heldEvents = selectedHeld.remainingQueue;
601
- notifyHeldDispatchProgress(input);
602
- return {
603
- events: selectedHeld.batch.events,
604
- sourceKey: selectedHeld.batch.sourceKey,
605
- selected: selectedHeld,
606
- };
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;
607
768
  }
608
769
  }
609
- if (selected) {
610
- input.eventQueue.replaceQueue(selected.remainingQueue);
611
- return {
770
+ if (selected && selected.batch.events.length > 0) {
771
+ const dispatch = prepareDispatch({
612
772
  events: selected.batch.events,
613
773
  sourceKey: selected.batch.sourceKey,
614
774
  selected,
615
- };
775
+ });
776
+ if (dispatch)
777
+ return dispatch;
778
+ await waitForDispatchChange(input);
779
+ continue;
616
780
  }
617
- const hasDeferredIdleOperatorMessage = input.eventQueue
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
618
796
  .snapshot()
619
- .some((event) => event.type === 'operator.message' && event.delivery === 'idle') && !input.canDispatchIdleOperatorMessage();
620
- // report 単独 send で dispatch hold を迂回しない。hold 解除後は held event が
621
- // maxTurns で保留中でも、この有限の通知は送れる。
622
- if (input.hasPendingRecoveryReport() &&
623
- !input.dispatchHoldState.dispatchHold &&
624
- !hasDeferredIdleOperatorMessage) {
625
- return {
626
- events: [],
627
- sourceKey: 'conductor.recovery.report',
628
- };
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();
823
+ }
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');
629
834
  }
630
- const hadQueued = input.eventQueue.size > 0;
631
- const incoming = await input.eventQueue.waitForEvent(input.signal, {
632
- onlyNew: hadQueued,
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
+ }
633
882
  });
634
- // waiter 経由のイベントは queue に載らない。到着順を保つため先頭へ戻す。
635
- input.eventQueue.prependSilent(incoming);
636
- if (incoming.type === 'operator.message')
637
- input.onOperatorQueueChanged?.();
638
883
  }
639
884
  }
640
885
  function canDispatchIdleOperatorMessage(input) {
@@ -662,17 +907,13 @@ function canDispatchIdleOperatorMessage(input) {
662
907
  maxTurns: input.maxTurns,
663
908
  }) === undefined);
664
909
  }
665
- function isImmediateDispatchSource(sourceKey) {
666
- return (sourceKey === 'operator' ||
667
- sourceKey === 'operator.reconnect' ||
668
- sourceKey === 'permission');
669
- }
670
910
  /** `/reconnect` は conductor へ送らず、直近 send 完了後に agent だけ差し替える。 */
671
911
  async function runOperatorReconnect(options) {
672
912
  try {
673
913
  await reconnectConductorAgent(options.conductorHandle, {
674
914
  conductorAgentFactory: options.sendReconnect.conductorAgentFactory,
675
915
  conductorOptions: options.sendReconnect.conductorOptions,
916
+ signal: options.shutdownSignal,
676
917
  onReconnectAttempt: options.sendReconnect.onTransportReconnectAttempt ??
677
918
  options.sendReconnect.onReconnectAttempt,
678
919
  onReconnectComplete: options.sendReconnect.onTransportReconnectComplete ??