@agents-ensemble/core 0.17.2 → 0.19.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 (107) hide show
  1. package/dist/acp/acp-bridge.d.ts +5 -1
  2. package/dist/acp/acp-bridge.d.ts.map +1 -1
  3. package/dist/acp/acp-bridge.js +4 -2
  4. package/dist/acp/acp-bridge.js.map +1 -1
  5. package/dist/acp/acp-client.d.ts +12 -0
  6. package/dist/acp/acp-client.d.ts.map +1 -1
  7. package/dist/acp/acp-client.js +88 -2
  8. package/dist/acp/acp-client.js.map +1 -1
  9. package/dist/acp/resolve-acp-spawn.d.ts +20 -2
  10. package/dist/acp/resolve-acp-spawn.d.ts.map +1 -1
  11. package/dist/acp/resolve-acp-spawn.js +105 -27
  12. package/dist/acp/resolve-acp-spawn.js.map +1 -1
  13. package/dist/acp/testing/fake-acp-server.d.ts +11 -0
  14. package/dist/acp/testing/fake-acp-server.d.ts.map +1 -1
  15. package/dist/acp/testing/fake-acp-server.js +31 -2
  16. package/dist/acp/testing/fake-acp-server.js.map +1 -1
  17. package/dist/conductor/conductor-agent-factory-cleanup.d.ts +7 -0
  18. package/dist/conductor/conductor-agent-factory-cleanup.d.ts.map +1 -0
  19. package/dist/conductor/conductor-agent-factory-cleanup.js +14 -0
  20. package/dist/conductor/conductor-agent-factory-cleanup.js.map +1 -0
  21. package/dist/conductor/conductor-agent.d.ts +6 -0
  22. package/dist/conductor/conductor-agent.d.ts.map +1 -1
  23. package/dist/conductor/conductor-session-driver.js +5 -7
  24. package/dist/conductor/conductor-session-driver.js.map +1 -1
  25. package/dist/conductor/conductor-session.d.ts +8 -0
  26. package/dist/conductor/conductor-session.d.ts.map +1 -1
  27. package/dist/conductor/conductor-session.js +228 -54
  28. package/dist/conductor/conductor-session.js.map +1 -1
  29. package/dist/conductor/cursor-sdk-conductor-agent.d.ts.map +1 -1
  30. package/dist/conductor/cursor-sdk-conductor-agent.js +77 -5
  31. package/dist/conductor/cursor-sdk-conductor-agent.js.map +1 -1
  32. package/dist/conductor/index.d.ts +1 -0
  33. package/dist/conductor/index.d.ts.map +1 -1
  34. package/dist/conductor/index.js +1 -0
  35. package/dist/conductor/index.js.map +1 -1
  36. package/dist/conductor/pi-conductor-agent.d.ts +1 -1
  37. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  38. package/dist/conductor/pi-conductor-agent.js +36 -1
  39. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  40. package/dist/conductor/resolve-conductor-reasoning.d.ts +13 -0
  41. package/dist/conductor/resolve-conductor-reasoning.d.ts.map +1 -0
  42. package/dist/conductor/resolve-conductor-reasoning.js +16 -0
  43. package/dist/conductor/resolve-conductor-reasoning.js.map +1 -0
  44. package/dist/conductor/session/dispatch-hold.d.ts +1 -1
  45. package/dist/conductor/session/dispatch-hold.d.ts.map +1 -1
  46. package/dist/conductor/session/dispatch-hold.js +2 -1
  47. package/dist/conductor/session/dispatch-hold.js.map +1 -1
  48. package/dist/conductor/session/session-logger.d.ts +1 -0
  49. package/dist/conductor/session/session-logger.d.ts.map +1 -1
  50. package/dist/conductor/session/session-logger.js +1 -0
  51. package/dist/conductor/session/session-logger.js.map +1 -1
  52. package/dist/config/ensemble-config-digest.d.ts +47 -1
  53. package/dist/config/ensemble-config-digest.d.ts.map +1 -1
  54. package/dist/config/ensemble-config-digest.js +68 -2
  55. package/dist/config/ensemble-config-digest.js.map +1 -1
  56. package/dist/config/index.d.ts +2 -2
  57. package/dist/config/index.d.ts.map +1 -1
  58. package/dist/config/index.js +1 -1
  59. package/dist/config/index.js.map +1 -1
  60. package/dist/config/load-ensemble-config.d.ts.map +1 -1
  61. package/dist/config/load-ensemble-config.js +22 -0
  62. package/dist/config/load-ensemble-config.js.map +1 -1
  63. package/dist/config/parse-config.d.ts +1 -1
  64. package/dist/config/parse-config.d.ts.map +1 -1
  65. package/dist/config/parse-config.js +34 -2
  66. package/dist/config/parse-config.js.map +1 -1
  67. package/dist/config/resolve-settings.d.ts +8 -0
  68. package/dist/config/resolve-settings.d.ts.map +1 -1
  69. package/dist/config/resolve-settings.js +10 -0
  70. package/dist/config/resolve-settings.js.map +1 -1
  71. package/dist/config/types.d.ts +21 -3
  72. package/dist/config/types.d.ts.map +1 -1
  73. package/dist/dispatch/set-dispatch-hold-tool.d.ts.map +1 -1
  74. package/dist/dispatch/set-dispatch-hold-tool.js +5 -4
  75. package/dist/dispatch/set-dispatch-hold-tool.js.map +1 -1
  76. package/dist/dispatch/worker-acp-session.d.ts +1 -1
  77. package/dist/dispatch/worker-acp-session.d.ts.map +1 -1
  78. package/dist/dispatch/worker-acp-session.js +23 -7
  79. package/dist/dispatch/worker-acp-session.js.map +1 -1
  80. package/dist/operator-runtime.d.ts +1 -0
  81. package/dist/operator-runtime.d.ts.map +1 -1
  82. package/dist/operator-runtime.js +1 -0
  83. package/dist/operator-runtime.js.map +1 -1
  84. package/dist/permission/permission-deadlock-monitor.d.ts +7 -2
  85. package/dist/permission/permission-deadlock-monitor.d.ts.map +1 -1
  86. package/dist/permission/permission-deadlock-monitor.js +16 -9
  87. package/dist/permission/permission-deadlock-monitor.js.map +1 -1
  88. package/dist/profiles/implementer-and-reviewer-v2/profile.yaml +1 -1
  89. package/dist/profiles/implementer-and-reviewer-v2/team-conductor.md +2 -0
  90. package/dist/prompt/modules/ensemble/conductor-base-module.js +3 -3
  91. package/dist/prompt/modules/ensemble/conductor-base-module.js.map +1 -1
  92. package/dist/prompt/modules/ensemble/worker-base-module.d.ts.map +1 -1
  93. package/dist/prompt/modules/ensemble/worker-base-module.js +1 -0
  94. package/dist/prompt/modules/ensemble/worker-base-module.js.map +1 -1
  95. package/dist/session/build-session-models-summary.d.ts +39 -0
  96. package/dist/session/build-session-models-summary.d.ts.map +1 -0
  97. package/dist/session/build-session-models-summary.js +40 -0
  98. package/dist/session/build-session-models-summary.js.map +1 -0
  99. package/dist/session/index.d.ts +3 -1
  100. package/dist/session/index.d.ts.map +1 -1
  101. package/dist/session/index.js +1 -0
  102. package/dist/session/index.js.map +1 -1
  103. package/dist/session/session-resume-store.d.ts +7 -2
  104. package/dist/session/session-resume-store.d.ts.map +1 -1
  105. package/dist/session/session-resume-store.js +48 -0
  106. package/dist/session/session-resume-store.js.map +1 -1
  107. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { computeEnsembleConfigDigest } from '../config/ensemble-config-digest.js';
1
+ import { computeAcpModelConfigDigest, computeEnsembleConfigDigest, computeResumeFeasibilityConfigDigest, } from '../config/ensemble-config-digest.js';
2
2
  import { loadEnsembleConfig } from '../config/load-ensemble-config.js';
3
3
  import { computeMcpConfigDigest, resolveMcpServers, } from '../mcp/load-mcp-config.js';
4
4
  import { createAnswerOpenQuestionTool } from '../escalation/answer-open-question-tool.js';
@@ -25,8 +25,10 @@ import { createSetDispatchHoldTool } from '../dispatch/set-dispatch-hold-tool.js
25
25
  import { parseIssueUrl } from '../issue/issue-ref.js';
26
26
  import { resolveWorkerWorkspace, removeWorkerWorktree, } from '../worktree/worktree.js';
27
27
  import { WorkerOutboundQueue } from '../runtime/worker-outbound-queue.js';
28
+ import { isConductorAgentFactoryCleanupIncomplete } from './conductor-agent-factory-cleanup.js';
28
29
  import { ConductorToolRegistry } from './conductor-tool.js';
29
30
  import { createConductorAgentFactory } from './conductor-agent-factory.js';
31
+ import { resolveConductorReasoning } from './resolve-conductor-reasoning.js';
30
32
  import { formatConductorAuthRecoveryHint, isConductorSendAuthError, } from './conductor-auth.js';
31
33
  import { resolvePiConductorProvider } from './conductor-pi-auth.js';
32
34
  import { SessionLogger } from './session/session-logger.js';
@@ -40,6 +42,7 @@ import { isOperatorCloseCommand } from './operator-close.js';
40
42
  import { isOperatorReconnectCommand } from './operator-reconnect.js';
41
43
  import { parseOperatorMessageDelivery } from './operator-command.js';
42
44
  import { assertSessionResumeStoreMatches, requireLocatedSessionResumeStoreForResume, saveSessionResumeStore, SESSION_RESUME_STORE_VERSION, sessionResumeStorePath, } from '../session/session-resume-store.js';
45
+ import { buildSessionModelsSummary, } from '../session/build-session-models-summary.js';
43
46
  import { saveSessionStatusLine, sessionStateStorePath, loadSessionStateStore, legacySessionStateStorePath, } from '../session/session-state-store.js';
44
47
  import { loadToolCallSnapshotHistory, pruneExpiredToolCallSnapshotHistoryFiles, saveToolCallSnapshotHistory, toolCallSnapshotHistoryPath, } from '../session/tool-call-snapshot-history.js';
45
48
  import { loadOpenQuestionsStore, legacyOpenQuestionsStorePath, openQuestionsStorePath, saveOpenQuestionsStore, } from '../session/open-questions-store.js';
@@ -50,6 +53,7 @@ import { SessionUsageTracker } from '../usage/session-usage-tracker.js';
50
53
  import { enqueueOperatorMessage as enqueueOperatorMessageEvent, submitOperatorInput, } from './submit-operator-input.js';
51
54
  import { createGitHubMonitor, } from '../github/github-monitor.js';
52
55
  import { resolveConductorBackendSetting, resolveConductorPiResourcePaths, resolveGitHubMonitorEnabled, } from '../config/resolve-settings.js';
56
+ import { CONDUCTOR_MODEL_ID_ENV, DEFAULT_CONDUCTOR_MODEL_ID, resolveConductorModelId, } from './resolve-conductor-model-id.js';
53
57
  import { GitHubMonitorError } from '../github/github-monitor-error.js';
54
58
  import { formatGitHubErrorMessage, GITHUB_AUTH_HINT, } from '../github/github-auth.js';
55
59
  import { fetchIssueContext } from '../github/issue-context.js';
@@ -66,12 +70,86 @@ const RECORDED_ANSWER_UNKNOWN_DISPATCH = {
66
70
  answerStatus: 'recorded',
67
71
  dispatchStatus: 'unknown',
68
72
  };
73
+ /** Acquired resources are released here until the session teardown takes ownership. */
74
+ class StartupCleanupGuard {
75
+ actions = [];
76
+ handedOff = false;
77
+ cleanupFailed = false;
78
+ reportCleanupError = () => { };
79
+ get hasFailures() {
80
+ return this.cleanupFailed;
81
+ }
82
+ add(action) {
83
+ if (this.handedOff) {
84
+ throw new Error('Cannot register startup cleanup after session handoff');
85
+ }
86
+ this.actions.push(action);
87
+ }
88
+ setErrorReporter(report) {
89
+ this.reportCleanupError = report;
90
+ }
91
+ recordCleanupFailure(error) {
92
+ this.cleanupFailed = true;
93
+ try {
94
+ this.reportCleanupError(error);
95
+ }
96
+ catch {
97
+ // Keep the startup error as the rejection reason if reporting fails.
98
+ }
99
+ }
100
+ handoff() {
101
+ this.handedOff = true;
102
+ this.actions.length = 0;
103
+ }
104
+ async cleanup() {
105
+ if (this.handedOff)
106
+ return;
107
+ this.handedOff = true;
108
+ for (const action of this.actions.reverse()) {
109
+ try {
110
+ await action();
111
+ }
112
+ catch (error) {
113
+ this.recordCleanupFailure(error);
114
+ }
115
+ }
116
+ }
117
+ }
69
118
  export async function runConductorSession(options) {
119
+ const startupCleanup = new StartupCleanupGuard();
120
+ try {
121
+ return await runConductorSessionWithStartupGuard(options, startupCleanup);
122
+ }
123
+ catch (error) {
124
+ await startupCleanup.cleanup();
125
+ throw error;
126
+ }
127
+ }
128
+ async function runConductorSessionWithStartupGuard(options, startupCleanup) {
70
129
  const userInstructions = await loadUserInstructions({
71
130
  userEnsembleRoot: options.mcpConfigOptions?.userEnsembleRoot,
72
131
  });
73
132
  const ensembleConfig = options.ensembleConfig ?? (await loadEnsembleConfig(options.repoRoot));
74
- const ensembleConfigDigest = computeEnsembleConfigDigest(ensembleConfig);
133
+ let ensembleConfigDigestScheme = 'resume-feasibility';
134
+ let ensembleConfigDigest = computeResumeFeasibilityConfigDigest(ensembleConfig);
135
+ let acpModelConfigDigest = computeAcpModelConfigDigest(ensembleConfig);
136
+ const resolvedConductorModelId = resolveConductorModelId(options.modelId, {
137
+ config: ensembleConfig,
138
+ });
139
+ const hasInvocationModelOverride = Boolean(options.modelId?.trim() ||
140
+ process.env[CONDUCTOR_MODEL_ID_ENV]?.trim());
141
+ // Built-in factories understand `default` as their backend default. For a
142
+ // custom factory, preserve the optional contract when the default comes
143
+ // from config/code defaults. Keep an invocation-level `default` sentinel so
144
+ // the factory can honor it over its own lower-priority defaults.
145
+ const conductorFactoryModelId = options.conductorAgentFactory &&
146
+ resolvedConductorModelId === DEFAULT_CONDUCTOR_MODEL_ID &&
147
+ !hasInvocationModelOverride
148
+ ? undefined
149
+ : resolvedConductorModelId;
150
+ const conductorReasoning = resolveConductorReasoning(options.reasoning, {
151
+ config: ensembleConfig,
152
+ });
75
153
  const conductorBackend = resolveConductorBackendSetting({
76
154
  profile: options.profile,
77
155
  config: ensembleConfig,
@@ -82,6 +160,13 @@ export async function runConductorSession(options) {
82
160
  issueUrl: options.issueUrl,
83
161
  repoRoot: options.repoRoot,
84
162
  });
163
+ startupCleanup.setErrorReporter((error) => {
164
+ const detail = error instanceof Error ? error.message : String(error);
165
+ sessionLogger.emit({
166
+ type: 'harness.warning',
167
+ message: `起動失敗後の後始末に失敗しました: ${detail}`,
168
+ });
169
+ });
85
170
  attachLegacySessionCallbacks(sessionLogger, options);
86
171
  const expiredHistory = await pruneExpiredToolCallSnapshotHistoryFiles(options.repoRoot);
87
172
  if (expiredHistory.failedCount > 0) {
@@ -123,6 +208,7 @@ export async function runConductorSession(options) {
123
208
  let githubMonitor;
124
209
  let activeResumeStorePath;
125
210
  let activeStoreLocation = 'user-path-hash';
211
+ let drainStartupResumeStoreWrites = async () => { };
126
212
  const pendingGitHubRecoveries = [];
127
213
  const conductorCwd = options.conductorCwd ?? process.cwd();
128
214
  let cursorBootstrapRunId;
@@ -139,6 +225,11 @@ export async function runConductorSession(options) {
139
225
  const loadedResumeStore = locatedResumeStore.resumeStore;
140
226
  activeResumeStorePath = locatedResumeStore.path;
141
227
  activeStoreLocation = locatedResumeStore.location;
228
+ const savedConfigDigestScheme = loadedResumeStore.ensembleConfigDigestScheme;
229
+ const effectiveConfigDigestScheme = savedConfigDigestScheme ?? 'full';
230
+ const currentEnsembleConfigDigest = effectiveConfigDigestScheme === 'resume-feasibility'
231
+ ? computeResumeFeasibilityConfigDigest(ensembleConfig)
232
+ : computeEnsembleConfigDigest(ensembleConfig);
142
233
  assertSessionResumeStoreMatches(loadedResumeStore, {
143
234
  conductorAgentId: options.resumeAgentId,
144
235
  issueUrl: options.issueUrl,
@@ -146,7 +237,7 @@ export async function runConductorSession(options) {
146
237
  conductorBackend,
147
238
  mcpConfigDigest,
148
239
  mcpConfigConfigured,
149
- ensembleConfigDigest,
240
+ ensembleConfigDigest: currentEnsembleConfigDigest,
150
241
  });
151
242
  if (loadedResumeStore.ensembleConfigDigest === undefined) {
152
243
  sessionLogger.emit({
@@ -154,10 +245,31 @@ export async function runConductorSession(options) {
154
245
  message: 'SessionResumeStore has no ensemble config digest; config consistency cannot be verified for this legacy session.',
155
246
  });
156
247
  }
248
+ const hasComparableLegacyFullDigest = effectiveConfigDigestScheme === 'full' &&
249
+ loadedResumeStore.ensembleConfigDigest !== undefined;
250
+ if (loadedResumeStore.acpModelConfigDigest === undefined &&
251
+ !hasComparableLegacyFullDigest) {
252
+ sessionLogger.emit({
253
+ type: 'harness.warning',
254
+ message: 'SessionResumeStore has no ACP model config digest; this diagnostic metadata is unavailable and will not be backfilled. Resume is not blocked by its absence.',
255
+ });
256
+ }
257
+ // Keep the persisted scheme and digest paired across resume and flush.
258
+ // Legacy stores without a digest remain unverifiable until a new session
259
+ // is started; a resume must not silently relabel their existing state.
260
+ ensembleConfigDigestScheme = savedConfigDigestScheme;
261
+ ensembleConfigDigest = loadedResumeStore.ensembleConfigDigest;
262
+ // This digest describes the config used when the session was first
263
+ // recorded. Keep it as diagnostic metadata; model changes do not gate
264
+ // resume, and a missing legacy value must not be backfilled.
265
+ acpModelConfigDigest = loadedResumeStore.acpModelConfigDigest;
157
266
  // A previously completed lifecycle no longer describes this instance once
158
267
  // resume begins. Persist unknown before restoring or attaching anything so
159
- // a crash before the first regular SessionResumeStore flush cannot leave stale
160
- // `closed` / `exited` data behind.
268
+ // a crash during backend resume cannot leave stale `closed` / `exited` data
269
+ // behind. Startup cleanup restores the previous record only if every
270
+ // acquired resource is released successfully; otherwise keep this record
271
+ // marked unknown/running so it does not claim a possibly active session is
272
+ // closed.
161
273
  const resumeStore = {
162
274
  ...loadedResumeStore,
163
275
  lifecycle: 'unknown',
@@ -169,7 +281,22 @@ export async function runConductorSession(options) {
169
281
  cursorBootstrapRunId = loadedResumeStore.cursorBootstrapRunId;
170
282
  harnessPhase = 'running';
171
283
  harnessModifiedAt = resumeStore.modifiedAt;
284
+ let resumeStoreActivationCommitted = false;
285
+ startupCleanup.add(async () => {
286
+ if (!resumeStoreActivationCommitted)
287
+ return;
288
+ await drainStartupResumeStoreWrites();
289
+ if (startupCleanup.hasFailures) {
290
+ sessionLogger.emit({
291
+ type: 'harness.warning',
292
+ message: '起動失敗後の後始末が完了しなかったため、再開記録を unknown/running のまま保ちます。',
293
+ });
294
+ return;
295
+ }
296
+ await saveSessionResumeStore(locatedResumeStore.path, loadedResumeStore);
297
+ });
172
298
  await saveSessionResumeStore(activeResumeStorePath, resumeStore);
299
+ resumeStoreActivationCommitted = true;
173
300
  const history = await loadToolCallSnapshotHistory({
174
301
  repoRoot: options.repoRoot,
175
302
  conductorAgentId: options.resumeAgentId,
@@ -334,22 +461,22 @@ export async function runConductorSession(options) {
334
461
  shutdownIntentController.abort();
335
462
  abortAfterOpenQuestionDrain(drainedShutdownController);
336
463
  };
464
+ let unregisterProcessSignalHandlers = () => { };
465
+ const removeShutdownDrainListener = () => shutdownSignal.removeEventListener('abort', onShutdownSignal);
466
+ const onSignal = () => shutdownController.abort();
467
+ unregisterProcessSignalHandlers = () => {
468
+ process.off('SIGINT', onSignal);
469
+ process.off('SIGTERM', onSignal);
470
+ removeShutdownDrainListener();
471
+ };
472
+ startupCleanup.add(() => unregisterProcessSignalHandlers());
337
473
  shutdownSignal.addEventListener('abort', onShutdownSignal, { once: true });
338
474
  if (shutdownSignal.aborted)
339
475
  onShutdownSignal();
340
- let unregisterProcessSignalHandlers = () => { };
341
- const removeShutdownDrainListener = () => shutdownSignal.removeEventListener('abort', onShutdownSignal);
342
- unregisterProcessSignalHandlers = removeShutdownDrainListener;
343
476
  if (ownsShutdownController &&
344
477
  options.registerProcessSignalHandlers !== false) {
345
- const onSignal = () => shutdownController.abort();
346
478
  process.once('SIGINT', onSignal);
347
479
  process.once('SIGTERM', onSignal);
348
- unregisterProcessSignalHandlers = () => {
349
- process.off('SIGINT', onSignal);
350
- process.off('SIGTERM', onSignal);
351
- removeShutdownDrainListener();
352
- };
353
480
  }
354
481
  const permissionPipeline = options.permissionPipeline ??
355
482
  new PermissionPipeline({ policy: options.permissionPolicy });
@@ -598,33 +725,7 @@ export async function runConductorSession(options) {
598
725
  });
599
726
  },
600
727
  });
601
- if (workers.length > 0 && !activeLazyWorkers) {
602
- sessionLogger.emit({
603
- type: 'harness.worker.bootstrap',
604
- workers: workers.map((worker) => ({
605
- name: worker.name,
606
- kind: worker.kind,
607
- })),
608
- mode: 'init',
609
- });
610
- }
611
- workerSession.startWorkers();
612
- if (resumeToolCallSnapshots.length > 0) {
613
- const workerIdsByName = new Map();
614
- workers.forEach((worker, index) => {
615
- const workerId = workerSession.startedWorkerIds[index];
616
- if (workerId)
617
- workerIdsByName.set(worker.name, workerId);
618
- });
619
- const restoreResult = toolCallSnapshots.restore(resumeToolCallSnapshots, workerIdsByName);
620
- if (restoreResult.skippedCount > 0) {
621
- toolCallHistoryDirty = true;
622
- sessionLogger.emit({
623
- type: 'harness.warning',
624
- 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.`,
625
- });
626
- }
627
- }
728
+ startupCleanup.add(() => workerSession.stop({ force: true }));
628
729
  const recordAnsweredQuestion = (answered) => {
629
730
  const record = openQuestionToEscalationRecord(answered);
630
731
  if (record) {
@@ -778,7 +879,10 @@ export async function runConductorSession(options) {
778
879
  ? { piSkills: activeProfile.conductor?.piSkills !== false }
779
880
  : {}),
780
881
  apiKey: options.apiKey,
781
- modelId: options.modelId,
882
+ modelId: conductorFactoryModelId,
883
+ ...(conductorReasoning !== undefined
884
+ ? { reasoning: conductorReasoning }
885
+ : {}),
782
886
  ...(conductorBackend === 'pi'
783
887
  ? {
784
888
  pi: resolveConductorPiResourcePaths({
@@ -808,9 +912,29 @@ export async function runConductorSession(options) {
808
912
  pi: conductorOptions.pi,
809
913
  })) || undefined
810
914
  : undefined;
811
- conductorAgent = options.resumeAgentId
812
- ? await conductorAgentFactory.resume(options.resumeAgentId, conductorOptions)
813
- : await conductorAgentFactory.create(conductorOptions);
915
+ shutdownSignal.throwIfAborted();
916
+ try {
917
+ conductorAgent = options.resumeAgentId
918
+ ? await conductorAgentFactory.resume(options.resumeAgentId, conductorOptions)
919
+ : await conductorAgentFactory.create(conductorOptions);
920
+ }
921
+ catch (error) {
922
+ if (isConductorAgentFactoryCleanupIncomplete(error)) {
923
+ startupCleanup.recordCleanupFailure(error);
924
+ }
925
+ throw error;
926
+ }
927
+ startupCleanup.add(async () => {
928
+ if (options.resumeAgentId && conductorAgent.closeForResume) {
929
+ await conductorAgent.closeForResume();
930
+ }
931
+ else {
932
+ await conductorAgent.close();
933
+ }
934
+ });
935
+ // Factories may not be abortable while pending. Register the returned agent
936
+ // before checking shutdown so a late success is still closed by the guard.
937
+ shutdownSignal.throwIfAborted();
814
938
  const conductorHandle = { conductor: conductorAgent };
815
939
  activeResumeStorePath ??= sessionResumeStorePath({
816
940
  repoRoot: options.repoRoot,
@@ -844,6 +968,9 @@ export async function runConductorSession(options) {
844
968
  },
845
969
  };
846
970
  let sessionResumeStoreWriteQueue = Promise.resolve();
971
+ drainStartupResumeStoreWrites = async () => {
972
+ await sessionResumeStoreWriteQueue;
973
+ };
847
974
  let terminalLifecycle;
848
975
  let terminalResumeStoreFinalFlushQueued = false;
849
976
  const enqueueSessionResumeStoreWrite = (write) => {
@@ -887,7 +1014,15 @@ export async function runConductorSession(options) {
887
1014
  sequence: snapshot.sequence,
888
1015
  workers,
889
1016
  mcpConfigDigest,
890
- ensembleConfigDigest,
1017
+ ...(ensembleConfigDigestScheme !== undefined
1018
+ ? { ensembleConfigDigestScheme }
1019
+ : {}),
1020
+ ...(ensembleConfigDigest !== undefined
1021
+ ? { ensembleConfigDigest }
1022
+ : {}),
1023
+ ...(acpModelConfigDigest !== undefined
1024
+ ? { acpModelConfigDigest }
1025
+ : {}),
891
1026
  ...(persistedLifecycle ? { lifecycle: persistedLifecycle } : {}),
892
1027
  ...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
893
1028
  updatedAt: Date.now(),
@@ -1033,8 +1168,6 @@ export async function runConductorSession(options) {
1033
1168
  emitToolCallHistoryFlushWarning();
1034
1169
  }
1035
1170
  };
1036
- if (toolCallHistoryDirty)
1037
- scheduleToolCallHistoryFlush();
1038
1171
  let sendCount = 0;
1039
1172
  let autonomousTurns = 0;
1040
1173
  const monitorDefaults = ensembleConfig.github.monitor;
@@ -1111,6 +1244,9 @@ export async function runConductorSession(options) {
1111
1244
  }
1112
1245
  },
1113
1246
  });
1247
+ startupCleanup.add(async () => {
1248
+ await githubMonitor?.stop();
1249
+ });
1114
1250
  githubMonitor.start();
1115
1251
  }
1116
1252
  dispatchHoldReminderMonitor = createDispatchHoldReminderMonitor({
@@ -1127,17 +1263,12 @@ export async function runConductorSession(options) {
1127
1263
  reminderIntervalMs: options.dispatchHoldReminderMs,
1128
1264
  shutdownSignal,
1129
1265
  });
1266
+ startupCleanup.add(() => dispatchHoldReminderMonitor.stop());
1130
1267
  dispatchHoldReminderMonitor.start();
1131
1268
  let permissionDeadlockMonitor;
1132
1269
  if (!options.disablePermissionDeadlockMonitor) {
1133
1270
  permissionDeadlockMonitor = createPermissionDeadlockMonitor({
1134
1271
  pipeline: permissionPipeline,
1135
- getActivitySnapshot: () => ({
1136
- attachInFlight: workerSession.runtime.attachInFlightCount,
1137
- hasProcessingWorker: workerSession.runtime
1138
- .listWorkerStatuses()
1139
- .some((worker) => worker.state === 'processing'),
1140
- }),
1141
1272
  onWarning: (message) => {
1142
1273
  sessionLogger.emit({ type: 'harness.warning', message });
1143
1274
  },
@@ -1156,6 +1287,7 @@ export async function runConductorSession(options) {
1156
1287
  pollIntervalMs: options.permissionDeadlockPollMs,
1157
1288
  shutdownSignal,
1158
1289
  });
1290
+ startupCleanup.add(() => permissionDeadlockMonitor.stop());
1159
1291
  permissionDeadlockMonitor.start();
1160
1292
  }
1161
1293
  let stopReason = 'completed';
@@ -1266,9 +1398,43 @@ export async function runConductorSession(options) {
1266
1398
  });
1267
1399
  if (typeof bindingDispose === 'function') {
1268
1400
  disposeOperatorInput = bindingDispose;
1401
+ startupCleanup.add(() => disposeOperatorInput?.());
1269
1402
  }
1270
1403
  }
1271
1404
  try {
1405
+ if (workers.length > 0 && !activeLazyWorkers) {
1406
+ sessionLogger.emit({
1407
+ type: 'harness.worker.bootstrap',
1408
+ workers: workers.map((worker) => ({
1409
+ name: worker.name,
1410
+ kind: worker.kind,
1411
+ })),
1412
+ mode: 'init',
1413
+ });
1414
+ }
1415
+ // Keep startup cleanup active through startup logging and the final
1416
+ // shutdown check. No await follows before workers start.
1417
+ shutdownSignal.throwIfAborted();
1418
+ startupCleanup.handoff();
1419
+ workerSession.startWorkers();
1420
+ if (resumeToolCallSnapshots.length > 0) {
1421
+ const workerIdsByName = new Map();
1422
+ workers.forEach((worker, index) => {
1423
+ const workerId = workerSession.startedWorkerIds[index];
1424
+ if (workerId)
1425
+ workerIdsByName.set(worker.name, workerId);
1426
+ });
1427
+ const restoreResult = toolCallSnapshots.restore(resumeToolCallSnapshots, workerIdsByName);
1428
+ if (restoreResult.skippedCount > 0) {
1429
+ toolCallHistoryDirty = true;
1430
+ sessionLogger.emit({
1431
+ type: 'harness.warning',
1432
+ 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.`,
1433
+ });
1434
+ }
1435
+ }
1436
+ if (toolCallHistoryDirty)
1437
+ scheduleToolCallHistoryFlush();
1272
1438
  openQuestionControlServer = await startOpenQuestionControlServer({
1273
1439
  listOpenQuestions: () => openQuestions.listOpen(),
1274
1440
  getLastConductorMessage: () => lastConductorMessage,
@@ -1615,6 +1781,14 @@ export async function runConductorSession(options) {
1615
1781
  agentId: conductorHandle.conductor.agentId,
1616
1782
  escalations,
1617
1783
  openQuestions: openQuestions.list(),
1784
+ sessionModels: buildSessionModelsSummary({
1785
+ backend: conductorBackend,
1786
+ explicitConductorModelId: resolvedConductorModelId,
1787
+ explicitConductorReasoning: conductorReasoning,
1788
+ config: ensembleConfig,
1789
+ workers,
1790
+ usageTracker: sessionUsageTracker,
1791
+ }),
1618
1792
  sessionUsage: sessionUsage.totals.rounds > 0 ||
1619
1793
  sessionUsage.cost != null ||
1620
1794
  sessionUsage.compactionCount != null