@agents-ensemble/core 0.12.0 → 0.14.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 (181) hide show
  1. package/dist/acp/testing/fake-acp-server.d.ts +2 -0
  2. package/dist/acp/testing/fake-acp-server.d.ts.map +1 -1
  3. package/dist/acp/testing/fake-acp-server.js +3 -0
  4. package/dist/acp/testing/fake-acp-server.js.map +1 -1
  5. package/dist/conductor/conductor-agent-factory.d.ts +5 -0
  6. package/dist/conductor/conductor-agent-factory.d.ts.map +1 -0
  7. package/dist/conductor/conductor-agent-factory.js +10 -0
  8. package/dist/conductor/conductor-agent-factory.js.map +1 -0
  9. package/dist/conductor/conductor-agent.d.ts +7 -0
  10. package/dist/conductor/conductor-agent.d.ts.map +1 -1
  11. package/dist/conductor/conductor-send-reconnect.d.ts +3 -0
  12. package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
  13. package/dist/conductor/conductor-send-reconnect.js +7 -1
  14. package/dist/conductor/conductor-send-reconnect.js.map +1 -1
  15. package/dist/conductor/conductor-session-driver.d.ts +6 -0
  16. package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
  17. package/dist/conductor/conductor-session-driver.js +19 -2
  18. package/dist/conductor/conductor-session-driver.js.map +1 -1
  19. package/dist/conductor/conductor-session.d.ts +5 -2
  20. package/dist/conductor/conductor-session.d.ts.map +1 -1
  21. package/dist/conductor/conductor-session.js +539 -74
  22. package/dist/conductor/conductor-session.js.map +1 -1
  23. package/dist/conductor/index.d.ts +3 -2
  24. package/dist/conductor/index.d.ts.map +1 -1
  25. package/dist/conductor/index.js +2 -1
  26. package/dist/conductor/index.js.map +1 -1
  27. package/dist/conductor/operator-command.d.ts +5 -0
  28. package/dist/conductor/operator-command.d.ts.map +1 -1
  29. package/dist/conductor/operator-command.js +6 -0
  30. package/dist/conductor/operator-command.js.map +1 -1
  31. package/dist/conductor/pi-conductor-agent.d.ts +2 -5
  32. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  33. package/dist/conductor/pi-conductor-agent.js +16 -19
  34. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  35. package/dist/conductor/pi-resource-loader.d.ts +2 -0
  36. package/dist/conductor/pi-resource-loader.d.ts.map +1 -1
  37. package/dist/conductor/pi-resource-loader.js +16 -8
  38. package/dist/conductor/pi-resource-loader.js.map +1 -1
  39. package/dist/conductor/session/dispatch-hold.d.ts +1 -1
  40. package/dist/conductor/session/dispatch-hold.d.ts.map +1 -1
  41. package/dist/conductor/session/dispatch-hold.js +4 -3
  42. package/dist/conductor/session/dispatch-hold.js.map +1 -1
  43. package/dist/conductor/session/dispatch-mode.d.ts +1 -1
  44. package/dist/conductor/session/events/index.d.ts +2 -1
  45. package/dist/conductor/session/events/index.d.ts.map +1 -1
  46. package/dist/conductor/session/events/index.js.map +1 -1
  47. package/dist/conductor/session/events/session-event.d.ts +7 -1
  48. package/dist/conductor/session/events/session-event.d.ts.map +1 -1
  49. package/dist/conductor/session/events/session-log-event-groups.d.ts +1 -1
  50. package/dist/conductor/session/events/session-log-event-groups.d.ts.map +1 -1
  51. package/dist/conductor/session/events/session-log-event-groups.js +1 -0
  52. package/dist/conductor/session/events/session-log-event-groups.js.map +1 -1
  53. package/dist/conductor/session/events/shared/permission-pending.d.ts +5 -0
  54. package/dist/conductor/session/events/shared/permission-pending.d.ts.map +1 -1
  55. package/dist/conductor/session/format-session-event.d.ts.map +1 -1
  56. package/dist/conductor/session/format-session-event.js +32 -2
  57. package/dist/conductor/session/format-session-event.js.map +1 -1
  58. package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
  59. package/dist/conductor/session/select-dispatch-batch.js +21 -4
  60. package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
  61. package/dist/conductor/session/session-event.d.ts +1 -1
  62. package/dist/conductor/session/session-event.d.ts.map +1 -1
  63. package/dist/conductor/session/session-logger.d.ts +1 -0
  64. package/dist/conductor/session/session-logger.d.ts.map +1 -1
  65. package/dist/conductor/session/session-logger.js +4 -0
  66. package/dist/conductor/session/session-logger.js.map +1 -1
  67. package/dist/conductor/submit-operator-input.d.ts +4 -1
  68. package/dist/conductor/submit-operator-input.d.ts.map +1 -1
  69. package/dist/conductor/submit-operator-input.js +4 -1
  70. package/dist/conductor/submit-operator-input.js.map +1 -1
  71. package/dist/dispatch/get-tool-call-tool.d.ts +8 -0
  72. package/dist/dispatch/get-tool-call-tool.d.ts.map +1 -0
  73. package/dist/dispatch/get-tool-call-tool.js +46 -0
  74. package/dist/dispatch/get-tool-call-tool.js.map +1 -0
  75. package/dist/dispatch/index.d.ts +3 -0
  76. package/dist/dispatch/index.d.ts.map +1 -1
  77. package/dist/dispatch/index.js +2 -0
  78. package/dist/dispatch/index.js.map +1 -1
  79. package/dist/dispatch/report-session-state-tool.d.ts +6 -0
  80. package/dist/dispatch/report-session-state-tool.d.ts.map +1 -0
  81. package/dist/dispatch/report-session-state-tool.js +39 -0
  82. package/dist/dispatch/report-session-state-tool.js.map +1 -0
  83. package/dist/dispatch/session-usage-tool.js +1 -1
  84. package/dist/dispatch/session-usage-tool.js.map +1 -1
  85. package/dist/escalation/apply-operator-message.d.ts +2 -0
  86. package/dist/escalation/apply-operator-message.d.ts.map +1 -1
  87. package/dist/escalation/apply-operator-message.js +2 -0
  88. package/dist/escalation/apply-operator-message.js.map +1 -1
  89. package/dist/escalation/ask-human-tool.d.ts +0 -2
  90. package/dist/escalation/ask-human-tool.d.ts.map +1 -1
  91. package/dist/escalation/ask-human-tool.js +1 -7
  92. package/dist/escalation/ask-human-tool.js.map +1 -1
  93. package/dist/escalation/open-question.d.ts +1 -0
  94. package/dist/escalation/open-question.d.ts.map +1 -1
  95. package/dist/escalation/open-question.js +1 -1
  96. package/dist/escalation/open-question.js.map +1 -1
  97. package/dist/escalation/record-open-question-answer.d.ts +1 -0
  98. package/dist/escalation/record-open-question-answer.d.ts.map +1 -1
  99. package/dist/escalation/record-open-question-answer.js +1 -0
  100. package/dist/escalation/record-open-question-answer.js.map +1 -1
  101. package/dist/github/github-monitor.d.ts +1 -1
  102. package/dist/github/github-monitor.d.ts.map +1 -1
  103. package/dist/github/github-monitor.js +1 -1
  104. package/dist/github/github-monitor.js.map +1 -1
  105. package/dist/github/register-github-watch-tool.d.ts +4 -1
  106. package/dist/github/register-github-watch-tool.d.ts.map +1 -1
  107. package/dist/github/register-github-watch-tool.js +17 -1
  108. package/dist/github/register-github-watch-tool.js.map +1 -1
  109. package/dist/github/unregister-github-watch-tool.d.ts +4 -1
  110. package/dist/github/unregister-github-watch-tool.d.ts.map +1 -1
  111. package/dist/github/unregister-github-watch-tool.js +14 -3
  112. package/dist/github/unregister-github-watch-tool.js.map +1 -1
  113. package/dist/operator-runtime.d.ts +24 -0
  114. package/dist/operator-runtime.d.ts.map +1 -0
  115. package/dist/operator-runtime.js +17 -0
  116. package/dist/operator-runtime.js.map +1 -0
  117. package/dist/permission/index.d.ts +2 -0
  118. package/dist/permission/index.d.ts.map +1 -1
  119. package/dist/permission/index.js +1 -0
  120. package/dist/permission/index.js.map +1 -1
  121. package/dist/permission/permission-brief.d.ts +22 -0
  122. package/dist/permission/permission-brief.d.ts.map +1 -0
  123. package/dist/permission/permission-brief.js +245 -0
  124. package/dist/permission/permission-brief.js.map +1 -0
  125. package/dist/prompt/index.d.ts +1 -0
  126. package/dist/prompt/index.d.ts.map +1 -1
  127. package/dist/prompt/index.js +1 -0
  128. package/dist/prompt/index.js.map +1 -1
  129. package/dist/prompt/modules/ensemble/conductor-base-module.d.ts.map +1 -1
  130. package/dist/prompt/modules/ensemble/conductor-base-module.js +11 -1
  131. package/dist/prompt/modules/ensemble/conductor-base-module.js.map +1 -1
  132. package/dist/runtime/index.d.ts +3 -0
  133. package/dist/runtime/index.d.ts.map +1 -1
  134. package/dist/runtime/index.js +2 -0
  135. package/dist/runtime/index.js.map +1 -1
  136. package/dist/runtime/tool-call-snapshot-registry.d.ts +69 -0
  137. package/dist/runtime/tool-call-snapshot-registry.d.ts.map +1 -0
  138. package/dist/runtime/tool-call-snapshot-registry.js +275 -0
  139. package/dist/runtime/tool-call-snapshot-registry.js.map +1 -0
  140. package/dist/runtime/types.d.ts +2 -0
  141. package/dist/runtime/types.d.ts.map +1 -1
  142. package/dist/runtime/worker-runtime.d.ts.map +1 -1
  143. package/dist/runtime/worker-runtime.js +6 -2
  144. package/dist/runtime/worker-runtime.js.map +1 -1
  145. package/dist/session/index.d.ts +7 -1
  146. package/dist/session/index.d.ts.map +1 -1
  147. package/dist/session/index.js +3 -0
  148. package/dist/session/index.js.map +1 -1
  149. package/dist/session/open-question-control.d.ts +53 -0
  150. package/dist/session/open-question-control.d.ts.map +1 -0
  151. package/dist/session/open-question-control.js +410 -0
  152. package/dist/session/open-question-control.js.map +1 -0
  153. package/dist/session/open-questions-store.d.ts +12 -0
  154. package/dist/session/open-questions-store.d.ts.map +1 -0
  155. package/dist/session/open-questions-store.js +164 -0
  156. package/dist/session/open-questions-store.js.map +1 -0
  157. package/dist/session/session-sidecar.d.ts +5 -0
  158. package/dist/session/session-sidecar.d.ts.map +1 -1
  159. package/dist/session/session-sidecar.js +24 -3
  160. package/dist/session/session-sidecar.js.map +1 -1
  161. package/dist/session/session-state-store.d.ts +21 -0
  162. package/dist/session/session-state-store.d.ts.map +1 -0
  163. package/dist/session/session-state-store.js +131 -0
  164. package/dist/session/session-state-store.js.map +1 -0
  165. package/dist/session/tool-call-snapshot-history.d.ts +32 -0
  166. package/dist/session/tool-call-snapshot-history.d.ts.map +1 -0
  167. package/dist/session/tool-call-snapshot-history.js +203 -0
  168. package/dist/session/tool-call-snapshot-history.js.map +1 -0
  169. package/dist/usage/enrich-session-usage-cost.d.ts +4 -1
  170. package/dist/usage/enrich-session-usage-cost.d.ts.map +1 -1
  171. package/dist/usage/enrich-session-usage-cost.js +8 -2
  172. package/dist/usage/enrich-session-usage-cost.js.map +1 -1
  173. package/dist/usage/index.d.ts +1 -1
  174. package/dist/usage/index.d.ts.map +1 -1
  175. package/dist/usage/session-usage-tracker.d.ts +7 -1
  176. package/dist/usage/session-usage-tracker.d.ts.map +1 -1
  177. package/dist/usage/session-usage-tracker.js +51 -0
  178. package/dist/usage/session-usage-tracker.js.map +1 -1
  179. package/dist/usage/types.d.ts +30 -2
  180. package/dist/usage/types.d.ts.map +1 -1
  181. package/package.json +5 -1
@@ -4,25 +4,28 @@ 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';
11
11
  import { createResolvePermissionTool } from '../permission/resolve-permission-tool.js';
12
+ import { buildPermissionBrief, getPermissionToolCallId, } from '../permission/permission-brief.js';
12
13
  import { acpSpawnFingerprint, resolveWorkerAcpSpawn, resolvedAcpSpawnToOptions, } from '../acp/resolve-acp-spawn.js';
13
14
  import { finalizeResolvedAcpSpawn, validateWorkerAcpPrerequisites, } from '../acp/validate-acp-preset-prerequisites.js';
14
15
  import { profileWorkersToSessionSpecs, sessionStateFromProfile, resolveAgentPromptModule, } from '../profile/types.js';
15
16
  import { WorkerSession } from '../runtime/worker-session.js';
17
+ import { ToolCallSnapshotRegistry, } from '../runtime/tool-call-snapshot-registry.js';
16
18
  import { createPromptWorkerTool } from '../dispatch/prompt-worker-tool.js';
17
19
  import { createWorkerStatusTools } from '../dispatch/worker-status-tool.js';
20
+ import { createGetToolCallTool } from '../dispatch/get-tool-call-tool.js';
18
21
  import { createSessionUsageTools } from '../dispatch/session-usage-tool.js';
22
+ import { createReportSessionStateTool } from '../dispatch/report-session-state-tool.js';
19
23
  import { createSetDispatchHoldTool } from '../dispatch/set-dispatch-hold-tool.js';
20
24
  import { parseIssueUrl } from '../issue/issue-ref.js';
21
25
  import { resolveWorkerWorkspace, removeWorkerWorktree, } from '../worktree/worktree.js';
22
26
  import { WorkerOutboundQueue } from '../runtime/worker-outbound-queue.js';
23
27
  import { ConductorToolRegistry } from './conductor-tool.js';
24
- import { createCursorSdkConductorAgentFactory } from './cursor-sdk-conductor-agent.js';
25
- import { createPiConductorAgentFactory } from './pi-conductor-agent.js';
28
+ import { createConductorAgentFactory } from './conductor-agent-factory.js';
26
29
  import { formatConductorAuthRecoveryHint, isConductorSendAuthError, } from './conductor-auth.js';
27
30
  import { resolvePiConductorProvider } from './conductor-pi-auth.js';
28
31
  import { SessionLogger } from './session/session-logger.js';
@@ -34,6 +37,10 @@ import { isOperatorExitCommand } from './operator-exit.js';
34
37
  import { isOperatorCloseCommand } from './operator-close.js';
35
38
  import { isOperatorReconnectCommand } from './operator-reconnect.js';
36
39
  import { assertSessionSidecarMatches, requireSessionSidecarForResume, saveSessionSidecar, SESSION_SIDECAR_VERSION, sessionSidecarPath, } from '../session/session-sidecar.js';
40
+ import { saveSessionStatusLine, sessionStateStorePath, } from '../session/session-state-store.js';
41
+ import { loadToolCallSnapshotHistory, pruneExpiredToolCallSnapshotHistoryFiles, saveToolCallSnapshotHistory, toolCallSnapshotHistoryPath, } from '../session/tool-call-snapshot-history.js';
42
+ import { loadOpenQuestionsStore, openQuestionsStorePath, saveOpenQuestionsStore, } from '../session/open-questions-store.js';
43
+ import { OpenQuestionControlOperationError, startOpenQuestionControlServer, } from '../session/open-question-control.js';
37
44
  import { formatPermissionSummaryForOperator } from '../permission/format-permission-summary-for-operator.js';
38
45
  import { enrichSessionUsageWithCost } from '../usage/enrich-session-usage-cost.js';
39
46
  import { SessionUsageTracker } from '../usage/session-usage-tracker.js';
@@ -49,6 +56,12 @@ import { createRegisterGitHubWatchTool } from '../github/register-github-watch-t
49
56
  import { createUnregisterGitHubWatchTool } from '../github/unregister-github-watch-tool.js';
50
57
  import { compileConductorSystemPrompt } from '../prompt/compile-system-prompt.js';
51
58
  import { loadUserInstructions } from '../prompt/load-user-instructions.js';
59
+ const OPEN_QUESTION_CONTROL_DRAIN_TIMEOUT_MS = 5_000;
60
+ const RECORDED_ANSWER_UNKNOWN_DISPATCH = {
61
+ outcome: 'unknown',
62
+ answerStatus: 'recorded',
63
+ dispatchStatus: 'unknown',
64
+ };
52
65
  export async function runConductorSession(options) {
53
66
  const userInstructions = await loadUserInstructions({
54
67
  userEnsembleRoot: options.mcpConfigOptions?.userEnsembleRoot,
@@ -65,9 +78,20 @@ export async function runConductorSession(options) {
65
78
  repoRoot: options.repoRoot,
66
79
  });
67
80
  attachLegacySessionCallbacks(sessionLogger, options);
81
+ const expiredHistory = await pruneExpiredToolCallSnapshotHistoryFiles(options.repoRoot);
82
+ if (expiredHistory.failedCount > 0) {
83
+ sessionLogger.emit({
84
+ type: 'harness.warning',
85
+ message: 'Some expired tool-call snapshot history files could not be removed.',
86
+ });
87
+ }
68
88
  const escalations = [];
69
89
  const openQuestions = new OpenQuestionRegistry();
90
+ let harnessSocketPath;
91
+ let openQuestionControlServer;
70
92
  const eventQueue = new SessionEventQueue();
93
+ const openQuestionDispatchReceipts = new WeakMap();
94
+ const pendingOpenQuestionDispatchReceipts = new Set();
71
95
  const dispatchHoldState = createDispatchHoldState();
72
96
  const onDispatchHoldChanged = (change) => {
73
97
  sessionLogger.emit({
@@ -85,16 +109,19 @@ export async function runConductorSession(options) {
85
109
  const workerSessions = new Map();
86
110
  let githubMonitorCursor;
87
111
  let githubMonitor;
112
+ const pendingGitHubRecoveries = [];
88
113
  const conductorCwd = options.conductorCwd ?? process.cwd();
89
114
  const mcpServers = await resolveMcpServers(options.repoRoot, options.mcpConfigOptions);
90
115
  const mcpConfigDigest = computeMcpConfigDigest(mcpServers);
91
116
  const mcpConfigConfigured = Object.keys(mcpServers).length > 0;
117
+ let resumeToolCallSnapshots = [];
118
+ let toolCallHistoryNeedsRewrite = false;
92
119
  if (options.resumeAgentId) {
93
- const sidecar = await requireSessionSidecarForResume({
120
+ const loadedSidecar = await requireSessionSidecarForResume({
94
121
  repoRoot: options.repoRoot,
95
122
  conductorAgentId: options.resumeAgentId,
96
123
  });
97
- assertSessionSidecarMatches(sidecar, {
124
+ assertSessionSidecarMatches(loadedSidecar, {
98
125
  conductorAgentId: options.resumeAgentId,
99
126
  issueUrl: options.issueUrl,
100
127
  repoRoot: options.repoRoot,
@@ -102,10 +129,57 @@ export async function runConductorSession(options) {
102
129
  mcpConfigDigest,
103
130
  mcpConfigConfigured,
104
131
  });
105
- openQuestions.restore({
106
- sequence: sidecar.sequence,
107
- openQuestions: sidecar.openQuestions,
132
+ // A previously completed lifecycle no longer describes this instance once
133
+ // resume begins. Persist unknown before restoring or attaching anything so
134
+ // a crash before the first regular sidecar flush cannot leave stale
135
+ // `closed` / `exited` data behind.
136
+ const sidecar = {
137
+ ...loadedSidecar,
138
+ lifecycle: 'unknown',
139
+ harnessSocketPath: undefined,
140
+ updatedAt: Date.now(),
141
+ };
142
+ await saveSessionSidecar(sessionSidecarPath({
143
+ repoRoot: options.repoRoot,
144
+ conductorAgentId: options.resumeAgentId,
145
+ }), sidecar);
146
+ const history = await loadToolCallSnapshotHistory({
147
+ repoRoot: options.repoRoot,
148
+ conductorAgentId: options.resumeAgentId,
108
149
  });
150
+ resumeToolCallSnapshots = history.entries;
151
+ toolCallHistoryNeedsRewrite = history.needsRewrite;
152
+ if (history.warning) {
153
+ sessionLogger.emit({
154
+ type: 'harness.warning',
155
+ message: `${history.warning}; the session will continue without affected history`,
156
+ });
157
+ }
158
+ const openQuestionsPath = openQuestionsStorePath({
159
+ repoRoot: options.repoRoot,
160
+ conductorAgentId: options.resumeAgentId,
161
+ });
162
+ const openQuestionsSnapshot = await loadOpenQuestionsStore(openQuestionsPath);
163
+ if (openQuestionsSnapshot) {
164
+ openQuestions.restore(openQuestionsSnapshot);
165
+ }
166
+ else {
167
+ const legacySnapshot = {
168
+ sequence: sidecar.sequence,
169
+ openQuestions: sidecar.openQuestions,
170
+ };
171
+ openQuestions.restore(legacySnapshot);
172
+ try {
173
+ await saveOpenQuestionsStore(openQuestionsPath, legacySnapshot);
174
+ }
175
+ catch (error) {
176
+ const detail = error instanceof Error ? error.message : String(error);
177
+ sessionLogger.emit({
178
+ type: 'harness.warning',
179
+ message: `旧 sidecar から復元した open question を YAML にバックフィルできませんでした。今回のセッションは継続します。 error=${detail}`,
180
+ });
181
+ }
182
+ }
109
183
  activeProfile = sidecar.profile;
110
184
  // Sidecars written before this setting always started every worker eagerly.
111
185
  activeLazyWorkers = sidecar.lazyWorkers ?? false;
@@ -147,12 +221,62 @@ export async function runConductorSession(options) {
147
221
  ? new AbortController()
148
222
  : undefined;
149
223
  const shutdownSignal = options.shutdownSignal ?? shutdownController.signal;
224
+ const shutdownIntentController = new AbortController();
150
225
  const operatorTerminationController = new AbortController();
226
+ const drainedShutdownController = new AbortController();
151
227
  const driverShutdownSignal = AbortSignal.any([
152
- shutdownSignal,
228
+ drainedShutdownController.signal,
153
229
  operatorTerminationController.signal,
154
230
  ]);
231
+ const abortAfterOpenQuestionDrain = (controller) => {
232
+ const activeControlServer = openQuestionControlServer;
233
+ if (!activeControlServer) {
234
+ controller.abort();
235
+ return;
236
+ }
237
+ const reportDrainFailure = (error) => {
238
+ const detail = error instanceof Error ? error.message : String(error);
239
+ sessionLogger.emit({
240
+ type: 'harness.warning',
241
+ message: `Open Question control drain failed: ${detail}`,
242
+ });
243
+ };
244
+ const quiescing = activeControlServer.quiesce();
245
+ if (!activeControlServer.hasInFlightRequests()) {
246
+ void quiescing.catch(reportDrainFailure);
247
+ controller.abort();
248
+ return;
249
+ }
250
+ let drainTimeout;
251
+ const drainDeadline = new Promise((resolve) => {
252
+ drainTimeout = setTimeout(() => resolve('timeout'), OPEN_QUESTION_CONTROL_DRAIN_TIMEOUT_MS);
253
+ });
254
+ const drainResult = quiescing.then(() => 'drained', (error) => {
255
+ reportDrainFailure(error);
256
+ return 'failed';
257
+ });
258
+ void Promise.race([drainResult, drainDeadline]).then((result) => {
259
+ if (drainTimeout)
260
+ clearTimeout(drainTimeout);
261
+ if (result === 'timeout') {
262
+ sessionLogger.emit({
263
+ type: 'harness.warning',
264
+ message: `Open Question control drain exceeded ${OPEN_QUESTION_CONTROL_DRAIN_TIMEOUT_MS}ms; aborting conductor dispatch. Accepted answers may have unknown notification outcomes.`,
265
+ });
266
+ }
267
+ controller.abort();
268
+ });
269
+ };
270
+ const onShutdownSignal = () => {
271
+ shutdownIntentController.abort();
272
+ abortAfterOpenQuestionDrain(drainedShutdownController);
273
+ };
274
+ shutdownSignal.addEventListener('abort', onShutdownSignal, { once: true });
275
+ if (shutdownSignal.aborted)
276
+ onShutdownSignal();
155
277
  let unregisterProcessSignalHandlers = () => { };
278
+ const removeShutdownDrainListener = () => shutdownSignal.removeEventListener('abort', onShutdownSignal);
279
+ unregisterProcessSignalHandlers = removeShutdownDrainListener;
156
280
  if (ownsShutdownController &&
157
281
  options.registerProcessSignalHandlers !== false) {
158
282
  const onSignal = () => shutdownController.abort();
@@ -161,11 +285,16 @@ export async function runConductorSession(options) {
161
285
  unregisterProcessSignalHandlers = () => {
162
286
  process.off('SIGINT', onSignal);
163
287
  process.off('SIGTERM', onSignal);
288
+ removeShutdownDrainListener();
164
289
  };
165
290
  }
166
291
  const permissionPipeline = options.permissionPipeline ??
167
292
  new PermissionPipeline({ policy: options.permissionPolicy });
168
293
  let scheduleSidecarFlush = () => { };
294
+ let flushSidecar = async () => { };
295
+ let scheduleToolCallHistoryFlush = () => { };
296
+ let flushToolCallHistory = async () => { };
297
+ let toolCallHistoryDirty = toolCallHistoryNeedsRewrite;
169
298
  const sessionState = sessionStateFromProfile(activeProfile);
170
299
  const issue = parseIssueUrl(options.issueUrl);
171
300
  const spawnBase = {
@@ -216,7 +345,11 @@ export async function runConductorSession(options) {
216
345
  }
217
346
  const sessionUsageTracker = new SessionUsageTracker({
218
347
  contextLimitTokens: options.contextLimitTokens,
348
+ trackCompactions: conductorBackend === 'pi' && options.conductorAgentFactory == null,
219
349
  });
350
+ const toolCallSnapshots = new ToolCallSnapshotRegistry();
351
+ let toolCallHistoryLimitWarningEmitted = false;
352
+ let toolCallHistoryOversizeWarningEmitted = false;
220
353
  const workerSession = new WorkerSession({
221
354
  issueUrl: options.issueUrl,
222
355
  repoRoot: options.repoRoot,
@@ -249,6 +382,13 @@ export async function runConductorSession(options) {
249
382
  const operatorSummary = formatPermissionSummaryForOperator(pending, {
250
383
  workerLabel,
251
384
  });
385
+ const toolCallId = getPermissionToolCallId(pending.request);
386
+ const toolCallSnapshot = toolCallId && pending.request.sessionId
387
+ ? toolCallSnapshots.get(toolCallId, {
388
+ workerId,
389
+ sessionId: pending.request.sessionId,
390
+ })
391
+ : undefined;
252
392
  sessionLogger.emit({
253
393
  type: 'permission.pending',
254
394
  permission: pending,
@@ -257,6 +397,8 @@ export async function runConductorSession(options) {
257
397
  eventQueue.enqueue({
258
398
  type: 'permission.pending',
259
399
  permission: pending,
400
+ worker: workerLabel,
401
+ brief: buildPermissionBrief(pending.request, toolCallSnapshot),
260
402
  operatorSummary,
261
403
  });
262
404
  }
@@ -331,6 +473,36 @@ export async function runConductorSession(options) {
331
473
  }
332
474
  },
333
475
  onAcpUpdate: (event) => {
476
+ if (event.toolCallUpdate) {
477
+ const result = toolCallSnapshots.applyWithResult({
478
+ workerId: event.workerId,
479
+ workerName: event.name,
480
+ sessionId: event.sessionId ?? '',
481
+ }, event.toolCallUpdate);
482
+ if (result.status === 'applied') {
483
+ toolCallHistoryDirty = true;
484
+ scheduleToolCallHistoryFlush();
485
+ if (result.evictedCount > 0 &&
486
+ !toolCallHistoryLimitWarningEmitted) {
487
+ toolCallHistoryLimitWarningEmitted = true;
488
+ sessionLogger.emit({
489
+ type: 'harness.warning',
490
+ message: 'Tool-call snapshot history reached its retention limit; oldest snapshots were removed.',
491
+ });
492
+ }
493
+ }
494
+ else if (result.reason === 'too-large') {
495
+ toolCallHistoryDirty = true;
496
+ scheduleToolCallHistoryFlush();
497
+ if (!toolCallHistoryOversizeWarningEmitted) {
498
+ toolCallHistoryOversizeWarningEmitted = true;
499
+ sessionLogger.emit({
500
+ type: 'harness.warning',
501
+ message: 'A tool-call snapshot exceeds the history size limit and was not retained.',
502
+ });
503
+ }
504
+ }
505
+ }
334
506
  sessionLogger.emit({
335
507
  type: 'harness.worker.acp.update',
336
508
  name: event.name,
@@ -369,6 +541,22 @@ export async function runConductorSession(options) {
369
541
  });
370
542
  }
371
543
  workerSession.startWorkers();
544
+ if (resumeToolCallSnapshots.length > 0) {
545
+ const workerIdsByName = new Map();
546
+ workers.forEach((worker, index) => {
547
+ const workerId = workerSession.startedWorkerIds[index];
548
+ if (workerId)
549
+ workerIdsByName.set(worker.name, workerId);
550
+ });
551
+ const restoreResult = toolCallSnapshots.restore(resumeToolCallSnapshots, workerIdsByName);
552
+ if (restoreResult.skippedCount > 0) {
553
+ toolCallHistoryDirty = true;
554
+ sessionLogger.emit({
555
+ type: 'harness.warning',
556
+ message: `${restoreResult.skippedCount} tool-call snapshot entr${restoreResult.skippedCount === 1 ? 'y was' : 'ies were'} skipped because its worker is no longer available or its data is invalid.`,
557
+ });
558
+ }
559
+ }
372
560
  const recordAnsweredQuestion = (answered) => {
373
561
  const record = openQuestionToEscalationRecord(answered);
374
562
  if (record) {
@@ -380,15 +568,6 @@ export async function runConductorSession(options) {
380
568
  };
381
569
  const askHumanTools = createAskHumanTool({
382
570
  registry: openQuestions,
383
- getPermissionSummary: (permissionId) => {
384
- const pending = permissionPipeline.pending.get(permissionId);
385
- if (!pending) {
386
- return undefined;
387
- }
388
- const workerLabel = workerSession.runtime.resolveWorkerLabel(pending.workerId) ??
389
- pending.workerId;
390
- return formatPermissionSummaryForOperator(pending, { workerLabel });
391
- },
392
571
  onEnqueued: (question) => {
393
572
  sessionLogger.emit({ type: 'open.question.enqueued', question });
394
573
  options.onOpenQuestionEnqueued?.(question);
@@ -406,6 +585,9 @@ export async function runConductorSession(options) {
406
585
  pipeline: permissionPipeline,
407
586
  inbox: workerSession.inbox,
408
587
  });
588
+ const getToolCallTools = createGetToolCallTool({
589
+ snapshots: toolCallSnapshots,
590
+ });
409
591
  let outboundDispatchesThisSend = 0;
410
592
  const outboundWorkersThisSend = new Set();
411
593
  const workerOutboundQueue = new WorkerOutboundQueue((worker, instruction, options) => {
@@ -431,9 +613,12 @@ export async function runConductorSession(options) {
431
613
  githubMonitorCursor ??= emptyGitHubMonitorCursor();
432
614
  return githubMonitorCursor;
433
615
  },
434
- onRegistered: (registration) => {
616
+ onRegistered: async (registration) => {
435
617
  githubMonitor?.registerPullRequest?.(registration);
436
- scheduleSidecarFlush();
618
+ // Do not report success to the conductor until the explicit watch is
619
+ // durable. This closes the crash window between the tool result and a
620
+ // later best-effort flush.
621
+ await flushSidecar();
437
622
  },
438
623
  });
439
624
  const unregisterGitHubWatchTools = createUnregisterGitHubWatchTool({
@@ -442,9 +627,9 @@ export async function runConductorSession(options) {
442
627
  githubMonitorCursor ??= emptyGitHubMonitorCursor();
443
628
  return githubMonitorCursor;
444
629
  },
445
- onUnregistered: (prNumber) => {
630
+ onUnregistered: async (prNumber) => {
446
631
  githubMonitor?.unregisterPullRequest?.(prNumber);
447
- scheduleSidecarFlush();
632
+ await flushSidecar();
448
633
  },
449
634
  });
450
635
  let conductorAgent;
@@ -456,6 +641,20 @@ export async function runConductorSession(options) {
456
641
  return usage.cost;
457
642
  },
458
643
  });
644
+ let sessionStateWriteQueue = Promise.resolve();
645
+ const reportSessionStateTool = createReportSessionStateTool({
646
+ onReport: (statusLine) => {
647
+ if (teardownStarted) {
648
+ throw new Error('report_session_state is unavailable during teardown');
649
+ }
650
+ const write = sessionStateWriteQueue.then(() => saveSessionStatusLine(sessionStateStorePath({
651
+ repoRoot: options.repoRoot,
652
+ conductorAgentId: conductorAgent.agentId,
653
+ }), statusLine));
654
+ sessionStateWriteQueue = write.catch(() => { });
655
+ return write;
656
+ },
657
+ });
459
658
  const dispatchHoldTools = createSetDispatchHoldTool({
460
659
  state: dispatchHoldState,
461
660
  onChanged: onDispatchHoldChanged,
@@ -479,11 +678,13 @@ export async function runConductorSession(options) {
479
678
  .registerAll(answerOpenQuestionTools)
480
679
  .registerAll(openQuestionListTools)
481
680
  .registerAll(resolvePermissionTools)
681
+ .registerAll(getToolCallTools)
482
682
  .registerAll(promptWorkerTools)
483
683
  .registerAll(workerStatusTools)
484
684
  .registerAll(registerGitHubWatchTools)
485
685
  .registerAll(unregisterGitHubWatchTools)
486
686
  .registerAll(sessionUsageTools)
687
+ .registerAll(reportSessionStateTool)
487
688
  .registerAll(dispatchHoldTools);
488
689
  const conductorOptions = {
489
690
  cwd: conductorCwd,
@@ -504,6 +705,11 @@ export async function runConductorSession(options) {
504
705
  : {}),
505
706
  ...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
506
707
  customTools: conductorToolRegistry.toRecord(),
708
+ ...(conductorBackend === 'pi' && options.conductorAgentFactory == null
709
+ ? {
710
+ onContextCompaction: () => sessionUsageTracker.recordContextCompaction(),
711
+ }
712
+ : {}),
507
713
  };
508
714
  const conductorProvider = conductorBackend === 'pi'
509
715
  ? (await resolvePiConductorProvider({
@@ -520,6 +726,7 @@ export async function runConductorSession(options) {
520
726
  const sendReconnect = {
521
727
  conductorAgentFactory,
522
728
  conductorOptions,
729
+ retrySuppressionSignal: shutdownIntentController.signal,
523
730
  onAuthReconnectAttempt: ({ agentId }) => {
524
731
  sessionLogger.emit({
525
732
  type: 'conductor.auth.reconnect',
@@ -543,47 +750,128 @@ export async function runConductorSession(options) {
543
750
  },
544
751
  };
545
752
  let sidecarFlushQueue = Promise.resolve();
546
- let flushSidecar = async () => { };
547
- flushSidecar = () => {
548
- const nextFlush = sidecarFlushQueue.then(async () => {
549
- const workers = {};
550
- for (const [name, worker] of workerSessions) {
551
- workers[name] = {
552
- acpSessionId: worker.acpSessionId,
553
- ...(worker.acpCwd ? { acpCwd: worker.acpCwd } : {}),
554
- ...(worker.acpSpawn ? { acpSpawn: worker.acpSpawn } : {}),
555
- };
556
- }
557
- const snapshot = openQuestions.snapshot();
558
- const sidecar = {
559
- version: SESSION_SIDECAR_VERSION,
560
- conductorAgentId: conductorHandle.conductor.agentId,
561
- conductorBackend,
562
- lazyWorkers: activeLazyWorkers,
563
- issueUrl: options.issueUrl,
564
- repoRoot: options.repoRoot,
565
- profile: structuredClone(activeProfile),
566
- ...(options.profilePath ? { profilePath: options.profilePath } : {}),
567
- openQuestions: snapshot.openQuestions,
568
- sequence: snapshot.sequence,
569
- workers,
570
- mcpConfigDigest,
571
- ...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
572
- updatedAt: Date.now(),
753
+ let terminalLifecycle;
754
+ const enqueueSidecarWrite = (write) => {
755
+ const nextWrite = sidecarFlushQueue.then(write);
756
+ sidecarFlushQueue = nextWrite.catch(() => { });
757
+ return nextWrite;
758
+ };
759
+ const createSessionSidecar = (snapshot, lifecycle) => {
760
+ const workers = {};
761
+ for (const [name, worker] of workerSessions) {
762
+ workers[name] = {
763
+ acpSessionId: worker.acpSessionId,
764
+ ...(worker.acpCwd ? { acpCwd: worker.acpCwd } : {}),
765
+ ...(worker.acpSpawn ? { acpSpawn: worker.acpSpawn } : {}),
573
766
  };
574
- await saveSessionSidecar(sessionSidecarPath({
575
- repoRoot: options.repoRoot,
576
- conductorAgentId: conductorHandle.conductor.agentId,
577
- }), sidecar);
767
+ }
768
+ return {
769
+ version: SESSION_SIDECAR_VERSION,
770
+ conductorAgentId: conductorHandle.conductor.agentId,
771
+ conductorBackend,
772
+ lazyWorkers: activeLazyWorkers,
773
+ ...(harnessSocketPath ? { harnessSocketPath } : {}),
774
+ issueUrl: options.issueUrl,
775
+ repoRoot: options.repoRoot,
776
+ profile: structuredClone(activeProfile),
777
+ ...(options.profilePath ? { profilePath: options.profilePath } : {}),
778
+ openQuestions: snapshot.openQuestions,
779
+ sequence: snapshot.sequence,
780
+ workers,
781
+ mcpConfigDigest,
782
+ ...((terminalLifecycle ?? lifecycle)
783
+ ? { lifecycle: terminalLifecycle ?? lifecycle }
784
+ : {}),
785
+ ...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
786
+ updatedAt: Date.now(),
787
+ };
788
+ };
789
+ const persistOpenQuestionsSnapshot = async (snapshot) => {
790
+ await saveOpenQuestionsStore(openQuestionsStorePath({
791
+ repoRoot: options.repoRoot,
792
+ conductorAgentId: conductorHandle.conductor.agentId,
793
+ }), snapshot);
794
+ };
795
+ const persistSessionSidecar = async (snapshot, lifecycle) => {
796
+ await saveSessionSidecar(sessionSidecarPath({
797
+ repoRoot: options.repoRoot,
798
+ conductorAgentId: conductorHandle.conductor.agentId,
799
+ }), createSessionSidecar(snapshot, lifecycle));
800
+ };
801
+ flushSidecar = (lifecycle) => enqueueSidecarWrite(async () => {
802
+ const snapshot = openQuestions.snapshot();
803
+ await persistOpenQuestionsSnapshot(snapshot);
804
+ await persistSessionSidecar(snapshot, lifecycle);
805
+ });
806
+ const reportSidecarFlushFailure = (error) => {
807
+ const detail = error instanceof Error ? error.message : String(error);
808
+ sessionLogger.emit({
809
+ type: 'harness.warning',
810
+ message: `Open Question YAML / session sidecar を保存できませんでした。最新状態から再開できず、直前に保存された状態からの再開になる場合があります。 error=${detail}`,
811
+ });
812
+ };
813
+ const reportSessionSidecarWriteFailure = (error) => {
814
+ const detail = error instanceof Error ? error.message : String(error);
815
+ sessionLogger.emit({
816
+ type: 'harness.warning',
817
+ message: `Session sidecar の更新に失敗しました。Open Question YAML の保存は完了しており、sidecar は後続 flush で同期します。 error=${detail}`,
578
818
  });
579
- sidecarFlushQueue = nextFlush.catch(() => { });
580
- return nextFlush;
581
819
  };
582
820
  scheduleSidecarFlush = () => {
583
- void flushSidecar().catch(() => {
584
- // best-effort persistence
821
+ void flushSidecar().catch(reportSidecarFlushFailure);
822
+ };
823
+ const toolCallHistoryPath = toolCallSnapshotHistoryPath({
824
+ repoRoot: options.repoRoot,
825
+ conductorAgentId: conductorHandle.conductor.agentId,
826
+ });
827
+ let toolCallHistoryFlushTimer;
828
+ let toolCallHistoryFlushQueue = Promise.resolve();
829
+ let toolCallHistoryFlushWarningEmitted = false;
830
+ const emitToolCallHistoryFlushWarning = () => {
831
+ if (toolCallHistoryFlushWarningEmitted)
832
+ return;
833
+ toolCallHistoryFlushWarningEmitted = true;
834
+ sessionLogger.emit({
835
+ type: 'harness.warning',
836
+ message: 'Tool-call snapshot history could not be saved; the latest history may be unavailable after resume.',
585
837
  });
586
838
  };
839
+ flushToolCallHistory = () => {
840
+ if (toolCallHistoryFlushTimer) {
841
+ clearTimeout(toolCallHistoryFlushTimer);
842
+ toolCallHistoryFlushTimer = undefined;
843
+ }
844
+ if (!toolCallHistoryDirty)
845
+ return toolCallHistoryFlushQueue;
846
+ toolCallHistoryDirty = false;
847
+ const nextFlush = toolCallHistoryFlushQueue.then(() => saveToolCallSnapshotHistory(toolCallHistoryPath, toolCallSnapshots.snapshot()));
848
+ toolCallHistoryFlushQueue = nextFlush.catch(() => { });
849
+ return nextFlush;
850
+ };
851
+ scheduleToolCallHistoryFlush = () => {
852
+ toolCallHistoryDirty = true;
853
+ if (toolCallHistoryFlushTimer)
854
+ return;
855
+ toolCallHistoryFlushTimer = setTimeout(() => {
856
+ toolCallHistoryFlushTimer = undefined;
857
+ void flushToolCallHistory().catch(() => {
858
+ toolCallHistoryDirty = true;
859
+ emitToolCallHistoryFlushWarning();
860
+ });
861
+ }, 100);
862
+ toolCallHistoryFlushTimer.unref?.();
863
+ };
864
+ const flushToolCallHistoryBestEffort = async () => {
865
+ try {
866
+ await flushToolCallHistory();
867
+ }
868
+ catch {
869
+ toolCallHistoryDirty = true;
870
+ emitToolCallHistoryFlushWarning();
871
+ }
872
+ };
873
+ if (toolCallHistoryDirty)
874
+ scheduleToolCallHistoryFlush();
587
875
  let sendCount = 0;
588
876
  let autonomousTurns = 0;
589
877
  const monitorDefaults = ensembleConfig.github.monitor;
@@ -610,6 +898,14 @@ export async function runConductorSession(options) {
610
898
  type: 'harness.github.update',
611
899
  itemCount: payload.items.length,
612
900
  });
901
+ for (const suppressedErrorCount of pendingGitHubRecoveries.splice(0)) {
902
+ eventQueue.enqueue({
903
+ type: 'harness.connectivity.recovered',
904
+ source: 'github_monitor',
905
+ suppressedErrorCount,
906
+ dispatchMode: 'inform',
907
+ });
908
+ }
613
909
  eventQueue.enqueue({
614
910
  type: 'github.update',
615
911
  items: payload.items,
@@ -635,11 +931,21 @@ export async function runConductorSession(options) {
635
931
  message,
636
932
  });
637
933
  },
638
- onPollRecovered: (suppressedErrorCount) => {
934
+ onPollRecovered: (suppressedErrorCount, hasUpdates) => {
639
935
  sessionLogger.emit({
640
936
  type: 'harness.github.monitor_recovered',
641
937
  suppressedErrorCount,
642
938
  });
939
+ if (hasUpdates) {
940
+ pendingGitHubRecoveries.push(suppressedErrorCount);
941
+ }
942
+ else {
943
+ eventQueue.enqueue({
944
+ type: 'harness.connectivity.recovered',
945
+ source: 'github_monitor',
946
+ suppressedErrorCount,
947
+ });
948
+ }
643
949
  },
644
950
  });
645
951
  githubMonitor.start();
@@ -687,14 +993,16 @@ export async function runConductorSession(options) {
687
993
  sessionLogger.emit({ type: 'session.operator_exit' });
688
994
  workerSession.runtime.cancelAllActivePrompts();
689
995
  operatorRequestedExit = true;
690
- operatorTerminationController.abort();
996
+ shutdownIntentController.abort();
997
+ abortAfterOpenQuestionDrain(operatorTerminationController);
691
998
  return false;
692
999
  }
693
1000
  if (isOperatorCloseCommand(message)) {
694
1001
  sessionLogger.emit({ type: 'session.operator_close' });
695
1002
  workerSession.runtime.cancelAllActivePrompts();
696
1003
  operatorRequestedClose = true;
697
- operatorTerminationController.abort();
1004
+ shutdownIntentController.abort();
1005
+ abortAfterOpenQuestionDrain(operatorTerminationController);
698
1006
  return false;
699
1007
  }
700
1008
  if (isOperatorReconnectCommand(message)) {
@@ -728,6 +1036,114 @@ export async function runConductorSession(options) {
728
1036
  }
729
1037
  }
730
1038
  try {
1039
+ openQuestionControlServer = await startOpenQuestionControlServer({
1040
+ listOpenQuestions: () => openQuestions.listOpen(),
1041
+ answerOpenQuestion: async ({ questionId, answer }) => {
1042
+ if (teardownStarted) {
1043
+ throw new OpenQuestionControlOperationError(-32000, 'The session is shutting down.');
1044
+ }
1045
+ const current = openQuestions.get(questionId);
1046
+ if (!current || current.status !== 'open') {
1047
+ throw new OpenQuestionControlOperationError(-32004, 'Open Question is not available.');
1048
+ }
1049
+ let accepted = false;
1050
+ let sidecarWriteError;
1051
+ let dispatchReceipt;
1052
+ let answered;
1053
+ const escalatedRecords = [];
1054
+ await enqueueSidecarWrite(async () => {
1055
+ const stagedOpenQuestions = new OpenQuestionRegistry();
1056
+ stagedOpenQuestions.restore(openQuestions.snapshot());
1057
+ const stagedAccepted = submitOperatorInput({
1058
+ message: answer,
1059
+ targetOpenQuestionId: questionId,
1060
+ conductorTurn: sendCount + 1,
1061
+ openQuestions: stagedOpenQuestions,
1062
+ escalations: [],
1063
+ eventQueue: new SessionEventQueue(),
1064
+ sessionLogger: { emit: () => { } },
1065
+ });
1066
+ const stagedAnswer = stagedOpenQuestions.get(questionId);
1067
+ if (!stagedAccepted ||
1068
+ !stagedAnswer ||
1069
+ stagedAnswer.status !== 'answered') {
1070
+ return;
1071
+ }
1072
+ // The Open Questions YAML is the durable commit point. Do not expose
1073
+ // the answer in the live Registry or event queue until this succeeds.
1074
+ await persistOpenQuestionsSnapshot(stagedOpenQuestions.snapshot());
1075
+ // Another answer path may have won while the atomic YAML write was
1076
+ // in flight. Restore the current Registry snapshot before rejecting.
1077
+ if (openQuestions.get(questionId)?.status !== 'open') {
1078
+ await persistOpenQuestionsSnapshot(openQuestions.snapshot());
1079
+ return;
1080
+ }
1081
+ accepted = submitOperatorInput({
1082
+ message: answer,
1083
+ targetOpenQuestionId: questionId,
1084
+ answeredAt: stagedAnswer.answeredAt,
1085
+ conductorTurn: sendCount + 1,
1086
+ openQuestions,
1087
+ escalations,
1088
+ eventQueue,
1089
+ sessionLogger,
1090
+ onEscalated: (record) => escalatedRecords.push(record),
1091
+ onOperatorMessageQueued: (event) => {
1092
+ let receipt;
1093
+ dispatchReceipt = new Promise((resolve, reject) => {
1094
+ receipt = {
1095
+ resolve: () => resolve(),
1096
+ reject: (error) => reject(error),
1097
+ };
1098
+ openQuestionDispatchReceipts.set(event, receipt);
1099
+ pendingOpenQuestionDispatchReceipts.add(receipt);
1100
+ });
1101
+ void dispatchReceipt.catch(() => { });
1102
+ },
1103
+ });
1104
+ answered = openQuestions.get(questionId);
1105
+ if (!accepted || !answered || answered.status !== 'answered') {
1106
+ await persistOpenQuestionsSnapshot(openQuestions.snapshot());
1107
+ accepted = false;
1108
+ return;
1109
+ }
1110
+ try {
1111
+ await persistSessionSidecar(openQuestions.snapshot());
1112
+ }
1113
+ catch (error) {
1114
+ // YAML is authoritative and is already durable. Keep the accepted
1115
+ // answer/notification, report the mirror failure, then repair it.
1116
+ sidecarWriteError = error;
1117
+ }
1118
+ });
1119
+ if (!accepted || !answered || answered.status !== 'answered') {
1120
+ throw new OpenQuestionControlOperationError(-32004, 'Open Question could not be answered.');
1121
+ }
1122
+ for (const record of escalatedRecords) {
1123
+ try {
1124
+ options.onEscalated?.(record);
1125
+ }
1126
+ catch (error) {
1127
+ const detail = error instanceof Error ? error.message : String(error);
1128
+ sessionLogger.emit({
1129
+ type: 'harness.warning',
1130
+ message: `Open Question observer callback failed: ${detail}`,
1131
+ });
1132
+ }
1133
+ }
1134
+ if (sidecarWriteError !== undefined) {
1135
+ reportSessionSidecarWriteFailure(sidecarWriteError);
1136
+ scheduleSidecarFlush();
1137
+ }
1138
+ if (!dispatchReceipt) {
1139
+ 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);
1140
+ }
1141
+ await dispatchReceipt;
1142
+ return answered;
1143
+ },
1144
+ });
1145
+ harnessSocketPath = openQuestionControlServer.socketPath;
1146
+ await flushSidecar().catch(reportSidecarFlushFailure);
731
1147
  const driverResult = await runConductorSessionDriver({
732
1148
  issueUrl: options.issueUrl,
733
1149
  initialPrompt: conductorBackend === 'pi'
@@ -788,6 +1204,23 @@ export async function runConductorSession(options) {
788
1204
  });
789
1205
  },
790
1206
  onSendComplete: recordSendComplete,
1207
+ onDispatchBatchSettled: ({ events, result, error }) => {
1208
+ for (const event of events) {
1209
+ if (event.type !== 'operator.message')
1210
+ continue;
1211
+ const receipt = openQuestionDispatchReceipts.get(event);
1212
+ if (!receipt)
1213
+ continue;
1214
+ openQuestionDispatchReceipts.delete(event);
1215
+ pendingOpenQuestionDispatchReceipts.delete(receipt);
1216
+ if (error || result?.status !== 'finished') {
1217
+ 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));
1218
+ }
1219
+ else {
1220
+ receipt.resolve();
1221
+ }
1222
+ }
1223
+ },
791
1224
  onOpenQuestionEnqueued: (question) => {
792
1225
  sessionLogger.emit({ type: 'open.question.enqueued', question });
793
1226
  options.onOpenQuestionEnqueued?.(question);
@@ -869,14 +1302,28 @@ export async function runConductorSession(options) {
869
1302
  agentId: conductorHandle.conductor.agentId,
870
1303
  escalations,
871
1304
  openQuestions: openQuestions.list(),
872
- sessionUsage: sessionUsage.totals.rounds > 0 || sessionUsage.cost != null
1305
+ sessionUsage: sessionUsage.totals.rounds > 0 ||
1306
+ sessionUsage.cost != null ||
1307
+ sessionUsage.compactionCount != null
873
1308
  ? sessionUsage
874
1309
  : undefined,
875
1310
  });
876
1311
  }
877
1312
  }
878
1313
  finally {
1314
+ const lifecycle = operatorRequestedClose
1315
+ ? 'closed'
1316
+ : operatorRequestedExit
1317
+ ? 'exited'
1318
+ : shutdownSignal.aborted
1319
+ ? 'interrupted'
1320
+ : 'unknown';
1321
+ terminalLifecycle = lifecycle;
879
1322
  teardownStarted = true;
1323
+ for (const receipt of pendingOpenQuestionDispatchReceipts) {
1324
+ 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));
1325
+ }
1326
+ pendingOpenQuestionDispatchReceipts.clear();
880
1327
  const forceShutdown = operatorRequestedExit ||
881
1328
  operatorRequestedClose ||
882
1329
  shutdownSignal.aborted;
@@ -902,6 +1349,24 @@ export async function runConductorSession(options) {
902
1349
  sessionLogger.emit({ type: 'harness.teardown.phase', phase });
903
1350
  };
904
1351
  try {
1352
+ const activeControlServer = openQuestionControlServer;
1353
+ openQuestionControlServer = undefined;
1354
+ harnessSocketPath = undefined;
1355
+ if (activeControlServer) {
1356
+ emitTeardownPhase('openQuestionControl');
1357
+ const phaseStart = Date.now();
1358
+ try {
1359
+ await activeControlServer.close();
1360
+ }
1361
+ catch (error) {
1362
+ const detail = error instanceof Error ? error.message : String(error);
1363
+ sessionLogger.emit({
1364
+ type: 'harness.warning',
1365
+ message: `Open Question control socket cleanup failed: ${detail}`,
1366
+ });
1367
+ }
1368
+ teardownPhases.openQuestionControl = Date.now() - phaseStart;
1369
+ }
905
1370
  disposeOperatorInput?.();
906
1371
  if (permissionDeadlockMonitor) {
907
1372
  emitTeardownPhase('permissionDeadlockMonitor');
@@ -955,14 +1420,9 @@ export async function runConductorSession(options) {
955
1420
  teardownPhases.flushSidecar = Date.now() - phaseStart;
956
1421
  }
957
1422
  catch (error) {
958
- if (operatorRequestedClose) {
959
- const detail = error instanceof Error ? error.message : String(error);
960
- sessionLogger.emit({
961
- type: 'harness.warning',
962
- message: `/close 時に sidecar を保存できませんでした。最新状態から再開できず、直前に保存された状態からの再開になる場合があります。 error=${detail}`,
963
- });
964
- }
1423
+ reportSidecarFlushFailure(error);
965
1424
  }
1425
+ await flushToolCallHistoryBestEffort();
966
1426
  const runGithubMonitorStop = async () => {
967
1427
  if (!githubMonitor || githubMonitorStopped) {
968
1428
  return;
@@ -1006,6 +1466,9 @@ export async function runConductorSession(options) {
1006
1466
  await runWorkerStop();
1007
1467
  await runConductorClose();
1008
1468
  }
1469
+ // ACP updates can arrive while cancellation and bridge shutdown settle.
1470
+ // Flush once more after all workers stop to capture their final snapshot.
1471
+ await flushToolCallHistoryBestEffort();
1009
1472
  sessionLogger.emit({
1010
1473
  type: 'harness.teardown',
1011
1474
  force: forceShutdown,
@@ -1021,17 +1484,19 @@ export async function runConductorSession(options) {
1021
1484
  }
1022
1485
  }
1023
1486
  finally {
1487
+ try {
1488
+ emitTeardownPhase('flushSidecar');
1489
+ await sessionStateWriteQueue;
1490
+ await flushSidecar(lifecycle);
1491
+ }
1492
+ catch (error) {
1493
+ reportSidecarFlushFailure(error);
1494
+ }
1024
1495
  process.off('SIGINT', onTeardownSignal);
1025
1496
  process.off('SIGTERM', onTeardownSignal);
1026
1497
  }
1027
1498
  }
1028
1499
  }
1029
- export function createConductorAgentFactory(backend) {
1030
- if (backend === 'cursor') {
1031
- return createCursorSdkConductorAgentFactory();
1032
- }
1033
- return createPiConductorAgentFactory();
1034
- }
1035
1500
  function createPiKickoffPrompt(issueUrl) {
1036
1501
  return (`Start the conductor workflow for ${issueUrl}. ` +
1037
1502
  'Review the Issue context in your system instructions and dispatch the first required worker with prompt_worker.');