@agents-ensemble/core 0.13.0 → 0.14.2

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 (149) hide show
  1. package/dist/acp/acp-client.d.ts.map +1 -1
  2. package/dist/acp/acp-client.js +19 -1
  3. package/dist/acp/acp-client.js.map +1 -1
  4. package/dist/acp/testing/fake-acp-server.d.ts +3 -2
  5. package/dist/acp/testing/fake-acp-server.d.ts.map +1 -1
  6. package/dist/acp/testing/fake-acp-server.js +1 -0
  7. package/dist/acp/testing/fake-acp-server.js.map +1 -1
  8. package/dist/conductor/conductor-agent.d.ts +2 -0
  9. package/dist/conductor/conductor-agent.d.ts.map +1 -1
  10. package/dist/conductor/conductor-send-reconnect.d.ts +3 -0
  11. package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
  12. package/dist/conductor/conductor-send-reconnect.js +7 -1
  13. package/dist/conductor/conductor-send-reconnect.js.map +1 -1
  14. package/dist/conductor/conductor-session-driver.d.ts +6 -0
  15. package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
  16. package/dist/conductor/conductor-session-driver.js +76 -2
  17. package/dist/conductor/conductor-session-driver.js.map +1 -1
  18. package/dist/conductor/conductor-session.d.ts +4 -0
  19. package/dist/conductor/conductor-session.d.ts.map +1 -1
  20. package/dist/conductor/conductor-session.js +414 -62
  21. package/dist/conductor/conductor-session.js.map +1 -1
  22. package/dist/conductor/cursor-sdk-conductor-agent.d.ts +3 -0
  23. package/dist/conductor/cursor-sdk-conductor-agent.d.ts.map +1 -1
  24. package/dist/conductor/cursor-sdk-conductor-agent.js +48 -15
  25. package/dist/conductor/cursor-sdk-conductor-agent.js.map +1 -1
  26. package/dist/conductor/index.d.ts +3 -1
  27. package/dist/conductor/index.d.ts.map +1 -1
  28. package/dist/conductor/index.js +1 -0
  29. package/dist/conductor/index.js.map +1 -1
  30. package/dist/conductor/operator-command.d.ts +22 -0
  31. package/dist/conductor/operator-command.d.ts.map +1 -1
  32. package/dist/conductor/operator-command.js +29 -0
  33. package/dist/conductor/operator-command.js.map +1 -1
  34. package/dist/conductor/operator-input-binding.d.ts +3 -0
  35. package/dist/conductor/operator-input-binding.d.ts.map +1 -1
  36. package/dist/conductor/pi-conductor-agent.d.ts +1 -0
  37. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  38. package/dist/conductor/pi-conductor-agent.js +5 -0
  39. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  40. package/dist/conductor/session/events/index.d.ts +1 -1
  41. package/dist/conductor/session/events/index.d.ts.map +1 -1
  42. package/dist/conductor/session/events/index.js.map +1 -1
  43. package/dist/conductor/session/events/session-event.d.ts +2 -0
  44. package/dist/conductor/session/events/session-event.d.ts.map +1 -1
  45. package/dist/conductor/session/events/session-log-event.d.ts +3 -1
  46. package/dist/conductor/session/events/session-log-event.d.ts.map +1 -1
  47. package/dist/conductor/session/select-dispatch-batch.d.ts +2 -0
  48. package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
  49. package/dist/conductor/session/select-dispatch-batch.js +8 -5
  50. package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
  51. package/dist/conductor/session/session-event.d.ts +1 -1
  52. package/dist/conductor/session/session-event.d.ts.map +1 -1
  53. package/dist/conductor/session/session-logger.d.ts +1 -0
  54. package/dist/conductor/session/session-logger.d.ts.map +1 -1
  55. package/dist/conductor/session/session-logger.js +4 -0
  56. package/dist/conductor/session/session-logger.js.map +1 -1
  57. package/dist/conductor/submit-operator-input.d.ts +15 -1
  58. package/dist/conductor/submit-operator-input.d.ts.map +1 -1
  59. package/dist/conductor/submit-operator-input.js +29 -5
  60. package/dist/conductor/submit-operator-input.js.map +1 -1
  61. package/dist/dispatch/attach-worker.d.ts +2 -1
  62. package/dist/dispatch/attach-worker.d.ts.map +1 -1
  63. package/dist/dispatch/attach-worker.js +1 -0
  64. package/dist/dispatch/attach-worker.js.map +1 -1
  65. package/dist/dispatch/report-session-state-tool.d.ts +3 -1
  66. package/dist/dispatch/report-session-state-tool.d.ts.map +1 -1
  67. package/dist/dispatch/report-session-state-tool.js +2 -1
  68. package/dist/dispatch/report-session-state-tool.js.map +1 -1
  69. package/dist/escalation/apply-operator-message.d.ts +2 -0
  70. package/dist/escalation/apply-operator-message.d.ts.map +1 -1
  71. package/dist/escalation/apply-operator-message.js +2 -0
  72. package/dist/escalation/apply-operator-message.js.map +1 -1
  73. package/dist/escalation/open-question.d.ts +1 -0
  74. package/dist/escalation/open-question.d.ts.map +1 -1
  75. package/dist/escalation/open-question.js +1 -1
  76. package/dist/escalation/open-question.js.map +1 -1
  77. package/dist/escalation/record-open-question-answer.d.ts +1 -0
  78. package/dist/escalation/record-open-question-answer.d.ts.map +1 -1
  79. package/dist/escalation/record-open-question-answer.js +1 -0
  80. package/dist/escalation/record-open-question-answer.js.map +1 -1
  81. package/dist/github/github-auth.d.ts +4 -0
  82. package/dist/github/github-auth.d.ts.map +1 -1
  83. package/dist/github/github-auth.js +37 -0
  84. package/dist/github/github-auth.js.map +1 -1
  85. package/dist/github/github-monitor-error.d.ts +1 -1
  86. package/dist/github/github-monitor-error.d.ts.map +1 -1
  87. package/dist/github/github-monitor-error.js +13 -1
  88. package/dist/github/github-monitor-error.js.map +1 -1
  89. package/dist/github/github-monitor.d.ts +3 -3
  90. package/dist/github/github-monitor.d.ts.map +1 -1
  91. package/dist/github/github-monitor.js +3 -3
  92. package/dist/github/github-monitor.js.map +1 -1
  93. package/dist/github/index.d.ts +1 -1
  94. package/dist/github/index.d.ts.map +1 -1
  95. package/dist/github/index.js +1 -1
  96. package/dist/github/index.js.map +1 -1
  97. package/dist/github/register-github-watch-tool.d.ts +4 -1
  98. package/dist/github/register-github-watch-tool.d.ts.map +1 -1
  99. package/dist/github/register-github-watch-tool.js +17 -1
  100. package/dist/github/register-github-watch-tool.js.map +1 -1
  101. package/dist/github/unregister-github-watch-tool.d.ts +4 -1
  102. package/dist/github/unregister-github-watch-tool.d.ts.map +1 -1
  103. package/dist/github/unregister-github-watch-tool.js +14 -3
  104. package/dist/github/unregister-github-watch-tool.js.map +1 -1
  105. package/dist/operator-runtime.d.ts +9 -2
  106. package/dist/operator-runtime.d.ts.map +1 -1
  107. package/dist/operator-runtime.js +6 -1
  108. package/dist/operator-runtime.js.map +1 -1
  109. package/dist/prompt/build-worker-attach-prompt.d.ts +2 -1
  110. package/dist/prompt/build-worker-attach-prompt.d.ts.map +1 -1
  111. package/dist/prompt/build-worker-attach-prompt.js +4 -1
  112. package/dist/prompt/build-worker-attach-prompt.js.map +1 -1
  113. package/dist/prompt/build-worker-prompt.d.ts +2 -1
  114. package/dist/prompt/build-worker-prompt.d.ts.map +1 -1
  115. package/dist/prompt/build-worker-prompt.js +4 -1
  116. package/dist/prompt/build-worker-prompt.js.map +1 -1
  117. package/dist/prompt/load-user-instructions.d.ts +1 -1
  118. package/dist/prompt/load-user-instructions.js +1 -1
  119. package/dist/prompt/modules/ensemble/worker-base-module.d.ts.map +1 -1
  120. package/dist/prompt/modules/ensemble/worker-base-module.js +7 -0
  121. package/dist/prompt/modules/ensemble/worker-base-module.js.map +1 -1
  122. package/dist/runtime/worker-runtime.d.ts +2 -0
  123. package/dist/runtime/worker-runtime.d.ts.map +1 -1
  124. package/dist/runtime/worker-runtime.js +1 -0
  125. package/dist/runtime/worker-runtime.js.map +1 -1
  126. package/dist/runtime/worker-session.d.ts +2 -1
  127. package/dist/runtime/worker-session.d.ts.map +1 -1
  128. package/dist/runtime/worker-session.js +3 -0
  129. package/dist/runtime/worker-session.js.map +1 -1
  130. package/dist/session/index.d.ts +5 -1
  131. package/dist/session/index.d.ts.map +1 -1
  132. package/dist/session/index.js +2 -0
  133. package/dist/session/index.js.map +1 -1
  134. package/dist/session/open-question-control.d.ts +73 -0
  135. package/dist/session/open-question-control.d.ts.map +1 -0
  136. package/dist/session/open-question-control.js +440 -0
  137. package/dist/session/open-question-control.js.map +1 -0
  138. package/dist/session/open-questions-store.d.ts.map +1 -1
  139. package/dist/session/open-questions-store.js +6 -3
  140. package/dist/session/open-questions-store.js.map +1 -1
  141. package/dist/session/session-sidecar.d.ts +5 -0
  142. package/dist/session/session-sidecar.d.ts.map +1 -1
  143. package/dist/session/session-sidecar.js +24 -3
  144. package/dist/session/session-sidecar.js.map +1 -1
  145. package/dist/session/session-state-store.d.ts +21 -0
  146. package/dist/session/session-state-store.d.ts.map +1 -0
  147. package/dist/session/session-state-store.js +131 -0
  148. package/dist/session/session-state-store.js.map +1 -0
  149. package/package.json +2 -2
@@ -4,7 +4,7 @@ import { createAnswerOpenQuestionTool } from '../escalation/answer-open-question
4
4
  import { createAskHumanTool } from '../escalation/ask-human-tool.js';
5
5
  import { createOpenQuestionListTools } from '../escalation/open-question-list-tools.js';
6
6
  import { openQuestionToEscalationRecord } from '../escalation/open-question-to-escalation.js';
7
- import { OpenQuestionRegistry } from '../escalation/open-question.js';
7
+ import { OpenQuestionRegistry, } from '../escalation/open-question.js';
8
8
  import { deny } from '../permission/permission-broker.js';
9
9
  import { PermissionPipeline } from '../permission/permission-pipeline.js';
10
10
  import { createPermissionDeadlockMonitor, } from '../permission/permission-deadlock-monitor.js';
@@ -36,13 +36,16 @@ import { runConductorSessionDriver } from './conductor-session-driver.js';
36
36
  import { isOperatorExitCommand } from './operator-exit.js';
37
37
  import { isOperatorCloseCommand } from './operator-close.js';
38
38
  import { isOperatorReconnectCommand } from './operator-reconnect.js';
39
+ import { parseOperatorMessageDelivery } from './operator-command.js';
39
40
  import { assertSessionSidecarMatches, requireSessionSidecarForResume, saveSessionSidecar, SESSION_SIDECAR_VERSION, sessionSidecarPath, } from '../session/session-sidecar.js';
41
+ import { saveSessionStatusLine, sessionStateStorePath, loadSessionStateStore, } from '../session/session-state-store.js';
40
42
  import { loadToolCallSnapshotHistory, pruneExpiredToolCallSnapshotHistoryFiles, saveToolCallSnapshotHistory, toolCallSnapshotHistoryPath, } from '../session/tool-call-snapshot-history.js';
41
43
  import { loadOpenQuestionsStore, openQuestionsStorePath, saveOpenQuestionsStore, } from '../session/open-questions-store.js';
44
+ import { OpenQuestionControlOperationError, startOpenQuestionControlServer, } from '../session/open-question-control.js';
42
45
  import { formatPermissionSummaryForOperator } from '../permission/format-permission-summary-for-operator.js';
43
46
  import { enrichSessionUsageWithCost } from '../usage/enrich-session-usage-cost.js';
44
47
  import { SessionUsageTracker } from '../usage/session-usage-tracker.js';
45
- import { submitOperatorInput } from './submit-operator-input.js';
48
+ import { enqueueOperatorMessage as enqueueOperatorMessageEvent, submitOperatorInput, } from './submit-operator-input.js';
46
49
  import { createGitHubMonitor, } from '../github/github-monitor.js';
47
50
  import { resolveConductorBackendSetting, resolveConductorPiResourcePaths, resolveGitHubMonitorEnabled, } from '../config/resolve-settings.js';
48
51
  import { GitHubMonitorError } from '../github/github-monitor-error.js';
@@ -54,6 +57,12 @@ import { createRegisterGitHubWatchTool } from '../github/register-github-watch-t
54
57
  import { createUnregisterGitHubWatchTool } from '../github/unregister-github-watch-tool.js';
55
58
  import { compileConductorSystemPrompt } from '../prompt/compile-system-prompt.js';
56
59
  import { loadUserInstructions } from '../prompt/load-user-instructions.js';
60
+ const OPEN_QUESTION_CONTROL_DRAIN_TIMEOUT_MS = 5_000;
61
+ const RECORDED_ANSWER_UNKNOWN_DISPATCH = {
62
+ outcome: 'unknown',
63
+ answerStatus: 'recorded',
64
+ dispatchStatus: 'unknown',
65
+ };
57
66
  export async function runConductorSession(options) {
58
67
  const userInstructions = await loadUserInstructions({
59
68
  userEnsembleRoot: options.mcpConfigOptions?.userEnsembleRoot,
@@ -79,7 +88,12 @@ export async function runConductorSession(options) {
79
88
  }
80
89
  const escalations = [];
81
90
  const openQuestions = new OpenQuestionRegistry();
91
+ let harnessSocketPath;
92
+ let openQuestionControlServer;
93
+ let lastConductorMessage = null;
82
94
  const eventQueue = new SessionEventQueue();
95
+ const openQuestionDispatchReceipts = new WeakMap();
96
+ const pendingOpenQuestionDispatchReceipts = new Set();
83
97
  const dispatchHoldState = createDispatchHoldState();
84
98
  const onDispatchHoldChanged = (change) => {
85
99
  sessionLogger.emit({
@@ -105,11 +119,11 @@ export async function runConductorSession(options) {
105
119
  let resumeToolCallSnapshots = [];
106
120
  let toolCallHistoryNeedsRewrite = false;
107
121
  if (options.resumeAgentId) {
108
- const sidecar = await requireSessionSidecarForResume({
122
+ const loadedSidecar = await requireSessionSidecarForResume({
109
123
  repoRoot: options.repoRoot,
110
124
  conductorAgentId: options.resumeAgentId,
111
125
  });
112
- assertSessionSidecarMatches(sidecar, {
126
+ assertSessionSidecarMatches(loadedSidecar, {
113
127
  conductorAgentId: options.resumeAgentId,
114
128
  issueUrl: options.issueUrl,
115
129
  repoRoot: options.repoRoot,
@@ -117,6 +131,20 @@ export async function runConductorSession(options) {
117
131
  mcpConfigDigest,
118
132
  mcpConfigConfigured,
119
133
  });
134
+ // A previously completed lifecycle no longer describes this instance once
135
+ // resume begins. Persist unknown before restoring or attaching anything so
136
+ // a crash before the first regular sidecar flush cannot leave stale
137
+ // `closed` / `exited` data behind.
138
+ const sidecar = {
139
+ ...loadedSidecar,
140
+ lifecycle: 'unknown',
141
+ harnessSocketPath: undefined,
142
+ updatedAt: Date.now(),
143
+ };
144
+ await saveSessionSidecar(sessionSidecarPath({
145
+ repoRoot: options.repoRoot,
146
+ conductorAgentId: options.resumeAgentId,
147
+ }), sidecar);
120
148
  const history = await loadToolCallSnapshotHistory({
121
149
  repoRoot: options.repoRoot,
122
150
  conductorAgentId: options.resumeAgentId,
@@ -195,12 +223,62 @@ export async function runConductorSession(options) {
195
223
  ? new AbortController()
196
224
  : undefined;
197
225
  const shutdownSignal = options.shutdownSignal ?? shutdownController.signal;
226
+ const shutdownIntentController = new AbortController();
198
227
  const operatorTerminationController = new AbortController();
228
+ const drainedShutdownController = new AbortController();
199
229
  const driverShutdownSignal = AbortSignal.any([
200
- shutdownSignal,
230
+ drainedShutdownController.signal,
201
231
  operatorTerminationController.signal,
202
232
  ]);
233
+ const abortAfterOpenQuestionDrain = (controller) => {
234
+ const activeControlServer = openQuestionControlServer;
235
+ if (!activeControlServer) {
236
+ controller.abort();
237
+ return;
238
+ }
239
+ const reportDrainFailure = (error) => {
240
+ const detail = error instanceof Error ? error.message : String(error);
241
+ sessionLogger.emit({
242
+ type: 'harness.warning',
243
+ message: `Open Question control drain failed: ${detail}`,
244
+ });
245
+ };
246
+ const quiescing = activeControlServer.quiesce();
247
+ if (!activeControlServer.hasInFlightRequests()) {
248
+ void quiescing.catch(reportDrainFailure);
249
+ controller.abort();
250
+ return;
251
+ }
252
+ let drainTimeout;
253
+ const drainDeadline = new Promise((resolve) => {
254
+ drainTimeout = setTimeout(() => resolve('timeout'), OPEN_QUESTION_CONTROL_DRAIN_TIMEOUT_MS);
255
+ });
256
+ const drainResult = quiescing.then(() => 'drained', (error) => {
257
+ reportDrainFailure(error);
258
+ return 'failed';
259
+ });
260
+ void Promise.race([drainResult, drainDeadline]).then((result) => {
261
+ if (drainTimeout)
262
+ clearTimeout(drainTimeout);
263
+ if (result === 'timeout') {
264
+ sessionLogger.emit({
265
+ type: 'harness.warning',
266
+ message: `Open Question control drain exceeded ${OPEN_QUESTION_CONTROL_DRAIN_TIMEOUT_MS}ms; aborting conductor dispatch. Accepted answers may have unknown notification outcomes.`,
267
+ });
268
+ }
269
+ controller.abort();
270
+ });
271
+ };
272
+ const onShutdownSignal = () => {
273
+ shutdownIntentController.abort();
274
+ abortAfterOpenQuestionDrain(drainedShutdownController);
275
+ };
276
+ shutdownSignal.addEventListener('abort', onShutdownSignal, { once: true });
277
+ if (shutdownSignal.aborted)
278
+ onShutdownSignal();
203
279
  let unregisterProcessSignalHandlers = () => { };
280
+ const removeShutdownDrainListener = () => shutdownSignal.removeEventListener('abort', onShutdownSignal);
281
+ unregisterProcessSignalHandlers = removeShutdownDrainListener;
204
282
  if (ownsShutdownController &&
205
283
  options.registerProcessSignalHandlers !== false) {
206
284
  const onSignal = () => shutdownController.abort();
@@ -209,11 +287,13 @@ export async function runConductorSession(options) {
209
287
  unregisterProcessSignalHandlers = () => {
210
288
  process.off('SIGINT', onSignal);
211
289
  process.off('SIGTERM', onSignal);
290
+ removeShutdownDrainListener();
212
291
  };
213
292
  }
214
293
  const permissionPipeline = options.permissionPipeline ??
215
294
  new PermissionPipeline({ policy: options.permissionPolicy });
216
295
  let scheduleSidecarFlush = () => { };
296
+ let flushSidecar = async () => { };
217
297
  let scheduleToolCallHistoryFlush = () => { };
218
298
  let flushToolCallHistory = async () => { };
219
299
  let toolCallHistoryDirty = toolCallHistoryNeedsRewrite;
@@ -279,6 +359,7 @@ export async function runConductorSession(options) {
279
359
  workers,
280
360
  lazyWorkers: activeLazyWorkers,
281
361
  sessionState,
362
+ ...(userInstructions ? { userInstructions } : {}),
282
363
  restoredWorkerSessions: Object.fromEntries(workerSessions),
283
364
  permissionPipeline,
284
365
  ...(options.connectAcp ? { connectAcp: options.connectAcp } : {}),
@@ -535,9 +616,12 @@ export async function runConductorSession(options) {
535
616
  githubMonitorCursor ??= emptyGitHubMonitorCursor();
536
617
  return githubMonitorCursor;
537
618
  },
538
- onRegistered: (registration) => {
619
+ onRegistered: async (registration) => {
539
620
  githubMonitor?.registerPullRequest?.(registration);
540
- scheduleSidecarFlush();
621
+ // Do not report success to the conductor until the explicit watch is
622
+ // durable. This closes the crash window between the tool result and a
623
+ // later best-effort flush.
624
+ await flushSidecar();
541
625
  },
542
626
  });
543
627
  const unregisterGitHubWatchTools = createUnregisterGitHubWatchTool({
@@ -546,9 +630,9 @@ export async function runConductorSession(options) {
546
630
  githubMonitorCursor ??= emptyGitHubMonitorCursor();
547
631
  return githubMonitorCursor;
548
632
  },
549
- onUnregistered: (prNumber) => {
633
+ onUnregistered: async (prNumber) => {
550
634
  githubMonitor?.unregisterPullRequest?.(prNumber);
551
- scheduleSidecarFlush();
635
+ await flushSidecar();
552
636
  },
553
637
  });
554
638
  let conductorAgent;
@@ -560,7 +644,20 @@ export async function runConductorSession(options) {
560
644
  return usage.cost;
561
645
  },
562
646
  });
563
- const reportSessionStateTool = createReportSessionStateTool();
647
+ let sessionStateWriteQueue = Promise.resolve();
648
+ const reportSessionStateTool = createReportSessionStateTool({
649
+ onReport: (statusLine) => {
650
+ if (teardownStarted) {
651
+ throw new Error('report_session_state is unavailable during teardown');
652
+ }
653
+ const write = sessionStateWriteQueue.then(() => saveSessionStatusLine(sessionStateStorePath({
654
+ repoRoot: options.repoRoot,
655
+ conductorAgentId: conductorAgent.agentId,
656
+ }), statusLine));
657
+ sessionStateWriteQueue = write.catch(() => { });
658
+ return write;
659
+ },
660
+ });
564
661
  const dispatchHoldTools = createSetDispatchHoldTool({
565
662
  state: dispatchHoldState,
566
663
  onChanged: onDispatchHoldChanged,
@@ -632,6 +729,7 @@ export async function runConductorSession(options) {
632
729
  const sendReconnect = {
633
730
  conductorAgentFactory,
634
731
  conductorOptions,
732
+ retrySuppressionSignal: shutdownIntentController.signal,
635
733
  onAuthReconnectAttempt: ({ agentId }) => {
636
734
  sessionLogger.emit({
637
735
  type: 'conductor.auth.reconnect',
@@ -655,46 +753,59 @@ export async function runConductorSession(options) {
655
753
  },
656
754
  };
657
755
  let sidecarFlushQueue = Promise.resolve();
658
- let flushSidecar = async () => { };
659
- flushSidecar = () => {
660
- const nextFlush = sidecarFlushQueue.then(async () => {
661
- const workers = {};
662
- for (const [name, worker] of workerSessions) {
663
- workers[name] = {
664
- acpSessionId: worker.acpSessionId,
665
- ...(worker.acpCwd ? { acpCwd: worker.acpCwd } : {}),
666
- ...(worker.acpSpawn ? { acpSpawn: worker.acpSpawn } : {}),
667
- };
668
- }
669
- const snapshot = openQuestions.snapshot();
670
- const sidecar = {
671
- version: SESSION_SIDECAR_VERSION,
672
- conductorAgentId: conductorHandle.conductor.agentId,
673
- conductorBackend,
674
- lazyWorkers: activeLazyWorkers,
675
- issueUrl: options.issueUrl,
676
- repoRoot: options.repoRoot,
677
- profile: structuredClone(activeProfile),
678
- ...(options.profilePath ? { profilePath: options.profilePath } : {}),
679
- openQuestions: snapshot.openQuestions,
680
- sequence: snapshot.sequence,
681
- workers,
682
- mcpConfigDigest,
683
- ...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
684
- updatedAt: Date.now(),
756
+ let terminalLifecycle;
757
+ const enqueueSidecarWrite = (write) => {
758
+ const nextWrite = sidecarFlushQueue.then(write);
759
+ sidecarFlushQueue = nextWrite.catch(() => { });
760
+ return nextWrite;
761
+ };
762
+ const createSessionSidecar = (snapshot, lifecycle) => {
763
+ const workers = {};
764
+ for (const [name, worker] of workerSessions) {
765
+ workers[name] = {
766
+ acpSessionId: worker.acpSessionId,
767
+ ...(worker.acpCwd ? { acpCwd: worker.acpCwd } : {}),
768
+ ...(worker.acpSpawn ? { acpSpawn: worker.acpSpawn } : {}),
685
769
  };
686
- await saveOpenQuestionsStore(openQuestionsStorePath({
687
- repoRoot: options.repoRoot,
688
- conductorAgentId: conductorHandle.conductor.agentId,
689
- }), snapshot);
690
- await saveSessionSidecar(sessionSidecarPath({
691
- repoRoot: options.repoRoot,
692
- conductorAgentId: conductorHandle.conductor.agentId,
693
- }), sidecar);
694
- });
695
- sidecarFlushQueue = nextFlush.catch(() => { });
696
- return nextFlush;
770
+ }
771
+ return {
772
+ version: SESSION_SIDECAR_VERSION,
773
+ conductorAgentId: conductorHandle.conductor.agentId,
774
+ conductorBackend,
775
+ lazyWorkers: activeLazyWorkers,
776
+ ...(harnessSocketPath ? { harnessSocketPath } : {}),
777
+ issueUrl: options.issueUrl,
778
+ repoRoot: options.repoRoot,
779
+ profile: structuredClone(activeProfile),
780
+ ...(options.profilePath ? { profilePath: options.profilePath } : {}),
781
+ openQuestions: snapshot.openQuestions,
782
+ sequence: snapshot.sequence,
783
+ workers,
784
+ mcpConfigDigest,
785
+ ...((terminalLifecycle ?? lifecycle)
786
+ ? { lifecycle: terminalLifecycle ?? lifecycle }
787
+ : {}),
788
+ ...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
789
+ updatedAt: Date.now(),
790
+ };
697
791
  };
792
+ const persistOpenQuestionsSnapshot = async (snapshot) => {
793
+ await saveOpenQuestionsStore(openQuestionsStorePath({
794
+ repoRoot: options.repoRoot,
795
+ conductorAgentId: conductorHandle.conductor.agentId,
796
+ }), snapshot);
797
+ };
798
+ const persistSessionSidecar = async (snapshot, lifecycle) => {
799
+ await saveSessionSidecar(sessionSidecarPath({
800
+ repoRoot: options.repoRoot,
801
+ conductorAgentId: conductorHandle.conductor.agentId,
802
+ }), createSessionSidecar(snapshot, lifecycle));
803
+ };
804
+ flushSidecar = (lifecycle) => enqueueSidecarWrite(async () => {
805
+ const snapshot = openQuestions.snapshot();
806
+ await persistOpenQuestionsSnapshot(snapshot);
807
+ await persistSessionSidecar(snapshot, lifecycle);
808
+ });
698
809
  const reportSidecarFlushFailure = (error) => {
699
810
  const detail = error instanceof Error ? error.message : String(error);
700
811
  sessionLogger.emit({
@@ -702,6 +813,13 @@ export async function runConductorSession(options) {
702
813
  message: `Open Question YAML / session sidecar を保存できませんでした。最新状態から再開できず、直前に保存された状態からの再開になる場合があります。 error=${detail}`,
703
814
  });
704
815
  };
816
+ const reportSessionSidecarWriteFailure = (error) => {
817
+ const detail = error instanceof Error ? error.message : String(error);
818
+ sessionLogger.emit({
819
+ type: 'harness.warning',
820
+ message: `Session sidecar の更新に失敗しました。Open Question YAML の保存は完了しており、sidecar は後続 flush で同期します。 error=${detail}`,
821
+ });
822
+ };
705
823
  scheduleSidecarFlush = () => {
706
824
  void flushSidecar().catch(reportSidecarFlushFailure);
707
825
  };
@@ -871,6 +989,57 @@ export async function runConductorSession(options) {
871
989
  let operatorRequestedExit = false;
872
990
  let operatorRequestedClose = false;
873
991
  let disposeOperatorInput;
992
+ const onOperatorMessageQueued = (delivery) => {
993
+ scheduleSidecarFlush();
994
+ if (delivery !== 'interrupt')
995
+ return;
996
+ const cancel = conductorHandle.conductor.cancel;
997
+ if (!cancel) {
998
+ sessionLogger.emit({
999
+ type: 'harness.warning',
1000
+ message: 'Operator interrupt cancellation is unavailable; the message will use normal next dispatch.',
1001
+ });
1002
+ return;
1003
+ }
1004
+ void cancel.call(conductorHandle.conductor).catch((error) => {
1005
+ const detail = error instanceof Error ? error.message : String(error);
1006
+ sessionLogger.emit({
1007
+ type: 'harness.warning',
1008
+ message: `Operator interrupt cancellation failed; the message will use normal next dispatch: ${detail}`,
1009
+ });
1010
+ });
1011
+ };
1012
+ const submitOperatorMessage = (input) => {
1013
+ const delivery = input.delivery ?? 'next';
1014
+ const received = submitOperatorInput({
1015
+ ...input,
1016
+ delivery,
1017
+ conductorTurn: sendCount + 1,
1018
+ openQuestions,
1019
+ escalations,
1020
+ eventQueue,
1021
+ sessionLogger,
1022
+ onEscalated: input.onEscalated ?? ((record) => options.onEscalated?.(record)),
1023
+ });
1024
+ if (!received)
1025
+ return false;
1026
+ onOperatorMessageQueued(delivery);
1027
+ return true;
1028
+ };
1029
+ const sendOperatorMessage = (input) => {
1030
+ const delivery = input.delivery ?? 'next';
1031
+ const received = enqueueOperatorMessageEvent({
1032
+ ...input,
1033
+ delivery,
1034
+ conductorTurn: sendCount + 1,
1035
+ eventQueue,
1036
+ sessionLogger,
1037
+ });
1038
+ if (!received)
1039
+ return false;
1040
+ onOperatorMessageQueued(delivery);
1041
+ return true;
1042
+ };
874
1043
  if (options.bindOperatorInput) {
875
1044
  const bindingDispose = options.bindOperatorInput({
876
1045
  submit: (message, submitOptions) => {
@@ -878,34 +1047,32 @@ export async function runConductorSession(options) {
878
1047
  sessionLogger.emit({ type: 'session.operator_exit' });
879
1048
  workerSession.runtime.cancelAllActivePrompts();
880
1049
  operatorRequestedExit = true;
881
- operatorTerminationController.abort();
1050
+ shutdownIntentController.abort();
1051
+ abortAfterOpenQuestionDrain(operatorTerminationController);
882
1052
  return false;
883
1053
  }
884
1054
  if (isOperatorCloseCommand(message)) {
885
1055
  sessionLogger.emit({ type: 'session.operator_close' });
886
1056
  workerSession.runtime.cancelAllActivePrompts();
887
1057
  operatorRequestedClose = true;
888
- operatorTerminationController.abort();
1058
+ shutdownIntentController.abort();
1059
+ abortAfterOpenQuestionDrain(operatorTerminationController);
889
1060
  return false;
890
1061
  }
891
1062
  if (isOperatorReconnectCommand(message)) {
892
1063
  eventQueue.enqueue({ type: 'operator.reconnect' });
893
1064
  return true;
894
1065
  }
895
- const received = submitOperatorInput({
896
- message,
1066
+ const parsed = submitOptions?.targetOpenQuestionId !== undefined
1067
+ ? { message, delivery: 'next' }
1068
+ : submitOptions?.delivery === undefined
1069
+ ? parseOperatorMessageDelivery(message)
1070
+ : { message, delivery: submitOptions.delivery };
1071
+ return submitOperatorMessage({
1072
+ message: parsed.message,
1073
+ delivery: parsed.delivery,
897
1074
  targetOpenQuestionId: submitOptions?.targetOpenQuestionId,
898
- conductorTurn: sendCount + 1,
899
- openQuestions,
900
- escalations,
901
- eventQueue,
902
- sessionLogger,
903
- onEscalated: (record) => options.onEscalated?.(record),
904
1075
  });
905
- if (received) {
906
- scheduleSidecarFlush();
907
- }
908
- return received;
909
1076
  },
910
1077
  getContext: () => ({
911
1078
  conductorTurn: sendCount + 1,
@@ -919,6 +1086,128 @@ export async function runConductorSession(options) {
919
1086
  }
920
1087
  }
921
1088
  try {
1089
+ openQuestionControlServer = await startOpenQuestionControlServer({
1090
+ listOpenQuestions: () => openQuestions.listOpen(),
1091
+ getLastConductorMessage: () => lastConductorMessage,
1092
+ observeSession: async () => {
1093
+ await sessionStateWriteQueue;
1094
+ const sessionState = await loadSessionStateStore(sessionStateStorePath({
1095
+ repoRoot: options.repoRoot,
1096
+ conductorAgentId: conductorAgent.agentId,
1097
+ }));
1098
+ return {
1099
+ ...openQuestions.snapshot(),
1100
+ ...(sessionState ? { sessionState } : {}),
1101
+ };
1102
+ },
1103
+ sendOperatorMessage: ({ text, delivery }) => {
1104
+ if (teardownStarted) {
1105
+ throw new OpenQuestionControlOperationError(-32000, 'The session is shutting down.');
1106
+ }
1107
+ return sendOperatorMessage({ message: text, delivery });
1108
+ },
1109
+ answerOpenQuestion: async ({ questionId, answer }) => {
1110
+ if (teardownStarted) {
1111
+ throw new OpenQuestionControlOperationError(-32000, 'The session is shutting down.');
1112
+ }
1113
+ const current = openQuestions.get(questionId);
1114
+ if (!current || current.status !== 'open') {
1115
+ throw new OpenQuestionControlOperationError(-32004, 'Open Question is not available.');
1116
+ }
1117
+ let accepted = false;
1118
+ let sidecarWriteError;
1119
+ let dispatchReceipt;
1120
+ let answered;
1121
+ const escalatedRecords = [];
1122
+ await enqueueSidecarWrite(async () => {
1123
+ const stagedOpenQuestions = new OpenQuestionRegistry();
1124
+ stagedOpenQuestions.restore(openQuestions.snapshot());
1125
+ const stagedAccepted = submitOperatorInput({
1126
+ message: answer,
1127
+ targetOpenQuestionId: questionId,
1128
+ conductorTurn: sendCount + 1,
1129
+ openQuestions: stagedOpenQuestions,
1130
+ escalations: [],
1131
+ eventQueue: new SessionEventQueue(),
1132
+ sessionLogger: { emit: () => { } },
1133
+ });
1134
+ const stagedAnswer = stagedOpenQuestions.get(questionId);
1135
+ if (!stagedAccepted ||
1136
+ !stagedAnswer ||
1137
+ stagedAnswer.status !== 'answered') {
1138
+ return;
1139
+ }
1140
+ // The Open Questions YAML is the durable commit point. Do not expose
1141
+ // the answer in the live Registry or event queue until this succeeds.
1142
+ await persistOpenQuestionsSnapshot(stagedOpenQuestions.snapshot());
1143
+ // Another answer path may have won while the atomic YAML write was
1144
+ // in flight. Restore the current Registry snapshot before rejecting.
1145
+ if (openQuestions.get(questionId)?.status !== 'open') {
1146
+ await persistOpenQuestionsSnapshot(openQuestions.snapshot());
1147
+ return;
1148
+ }
1149
+ accepted = submitOperatorMessage({
1150
+ message: answer,
1151
+ delivery: 'next',
1152
+ targetOpenQuestionId: questionId,
1153
+ answeredAt: stagedAnswer.answeredAt,
1154
+ onEscalated: (record) => escalatedRecords.push(record),
1155
+ onOperatorMessageQueued: (event) => {
1156
+ let receipt;
1157
+ dispatchReceipt = new Promise((resolve, reject) => {
1158
+ receipt = {
1159
+ resolve: () => resolve(),
1160
+ reject: (error) => reject(error),
1161
+ };
1162
+ openQuestionDispatchReceipts.set(event, receipt);
1163
+ pendingOpenQuestionDispatchReceipts.add(receipt);
1164
+ });
1165
+ void dispatchReceipt.catch(() => { });
1166
+ },
1167
+ });
1168
+ answered = openQuestions.get(questionId);
1169
+ if (!accepted || !answered || answered.status !== 'answered') {
1170
+ await persistOpenQuestionsSnapshot(openQuestions.snapshot());
1171
+ accepted = false;
1172
+ return;
1173
+ }
1174
+ try {
1175
+ await persistSessionSidecar(openQuestions.snapshot());
1176
+ }
1177
+ catch (error) {
1178
+ // YAML is authoritative and is already durable. Keep the accepted
1179
+ // answer/notification, report the mirror failure, then repair it.
1180
+ sidecarWriteError = error;
1181
+ }
1182
+ });
1183
+ if (!accepted || !answered || answered.status !== 'answered') {
1184
+ throw new OpenQuestionControlOperationError(-32004, 'Open Question could not be answered.');
1185
+ }
1186
+ for (const record of escalatedRecords) {
1187
+ try {
1188
+ options.onEscalated?.(record);
1189
+ }
1190
+ catch (error) {
1191
+ const detail = error instanceof Error ? error.message : String(error);
1192
+ sessionLogger.emit({
1193
+ type: 'harness.warning',
1194
+ message: `Open Question observer callback failed: ${detail}`,
1195
+ });
1196
+ }
1197
+ }
1198
+ if (sidecarWriteError !== undefined) {
1199
+ reportSessionSidecarWriteFailure(sidecarWriteError);
1200
+ scheduleSidecarFlush();
1201
+ }
1202
+ if (!dispatchReceipt) {
1203
+ throw new OpenQuestionControlOperationError(-32603, 'The answer was recorded, but its conductor notification was not queued. The outcome is unknown; do not resubmit the answer and inspect the session state.', RECORDED_ANSWER_UNKNOWN_DISPATCH);
1204
+ }
1205
+ await dispatchReceipt;
1206
+ return answered;
1207
+ },
1208
+ });
1209
+ harnessSocketPath = openQuestionControlServer.socketPath;
1210
+ await flushSidecar().catch(reportSidecarFlushFailure);
922
1211
  const driverResult = await runConductorSessionDriver({
923
1212
  issueUrl: options.issueUrl,
924
1213
  initialPrompt: conductorBackend === 'pi'
@@ -979,6 +1268,23 @@ export async function runConductorSession(options) {
979
1268
  });
980
1269
  },
981
1270
  onSendComplete: recordSendComplete,
1271
+ onDispatchBatchSettled: ({ events, result, error }) => {
1272
+ for (const event of events) {
1273
+ if (event.type !== 'operator.message')
1274
+ continue;
1275
+ const receipt = openQuestionDispatchReceipts.get(event);
1276
+ if (!receipt)
1277
+ continue;
1278
+ openQuestionDispatchReceipts.delete(event);
1279
+ pendingOpenQuestionDispatchReceipts.delete(receipt);
1280
+ if (error || result?.status !== 'finished') {
1281
+ receipt.reject(new OpenQuestionControlOperationError(-32005, 'The answer was recorded and persisted, but conductor notification could not be confirmed. The outcome is unknown; do not resubmit the answer and inspect the session state.', RECORDED_ANSWER_UNKNOWN_DISPATCH));
1282
+ }
1283
+ else {
1284
+ receipt.resolve();
1285
+ }
1286
+ }
1287
+ },
982
1288
  onOpenQuestionEnqueued: (question) => {
983
1289
  sessionLogger.emit({ type: 'open.question.enqueued', question });
984
1290
  options.onOpenQuestionEnqueued?.(question);
@@ -995,6 +1301,14 @@ export async function runConductorSession(options) {
995
1301
  function recordSendComplete(info) {
996
1302
  sendCount = info.sendCount;
997
1303
  autonomousTurns = info.autonomousTurns;
1304
+ const responseText = info.result?.trim();
1305
+ if (info.status === 'finished' && responseText) {
1306
+ lastConductorMessage = {
1307
+ text: responseText,
1308
+ at: Date.now(),
1309
+ sendCount: info.sendCount,
1310
+ };
1311
+ }
998
1312
  const githubUpdateIds = info.githubUpdateIds ?? [];
999
1313
  if (info.status === 'finished' &&
1000
1314
  !operatorRequestedClose &&
@@ -1069,7 +1383,19 @@ export async function runConductorSession(options) {
1069
1383
  }
1070
1384
  }
1071
1385
  finally {
1386
+ const lifecycle = operatorRequestedClose
1387
+ ? 'closed'
1388
+ : operatorRequestedExit
1389
+ ? 'exited'
1390
+ : shutdownSignal.aborted
1391
+ ? 'interrupted'
1392
+ : 'unknown';
1393
+ terminalLifecycle = lifecycle;
1072
1394
  teardownStarted = true;
1395
+ for (const receipt of pendingOpenQuestionDispatchReceipts) {
1396
+ receipt.reject(new OpenQuestionControlOperationError(-32005, 'The answer was recorded and persisted, but the conductor session stopped before notification completed. The outcome is unknown; do not resubmit the answer and inspect the session state.', RECORDED_ANSWER_UNKNOWN_DISPATCH));
1397
+ }
1398
+ pendingOpenQuestionDispatchReceipts.clear();
1073
1399
  const forceShutdown = operatorRequestedExit ||
1074
1400
  operatorRequestedClose ||
1075
1401
  shutdownSignal.aborted;
@@ -1095,6 +1421,24 @@ export async function runConductorSession(options) {
1095
1421
  sessionLogger.emit({ type: 'harness.teardown.phase', phase });
1096
1422
  };
1097
1423
  try {
1424
+ const activeControlServer = openQuestionControlServer;
1425
+ openQuestionControlServer = undefined;
1426
+ harnessSocketPath = undefined;
1427
+ if (activeControlServer) {
1428
+ emitTeardownPhase('openQuestionControl');
1429
+ const phaseStart = Date.now();
1430
+ try {
1431
+ await activeControlServer.close();
1432
+ }
1433
+ catch (error) {
1434
+ const detail = error instanceof Error ? error.message : String(error);
1435
+ sessionLogger.emit({
1436
+ type: 'harness.warning',
1437
+ message: `Open Question control socket cleanup failed: ${detail}`,
1438
+ });
1439
+ }
1440
+ teardownPhases.openQuestionControl = Date.now() - phaseStart;
1441
+ }
1098
1442
  disposeOperatorInput?.();
1099
1443
  if (permissionDeadlockMonitor) {
1100
1444
  emitTeardownPhase('permissionDeadlockMonitor');
@@ -1212,6 +1556,14 @@ export async function runConductorSession(options) {
1212
1556
  }
1213
1557
  }
1214
1558
  finally {
1559
+ try {
1560
+ emitTeardownPhase('flushSidecar');
1561
+ await sessionStateWriteQueue;
1562
+ await flushSidecar(lifecycle);
1563
+ }
1564
+ catch (error) {
1565
+ reportSidecarFlushFailure(error);
1566
+ }
1215
1567
  process.off('SIGINT', onTeardownSignal);
1216
1568
  process.off('SIGTERM', onTeardownSignal);
1217
1569
  }