@agents-ensemble/core 0.14.2 → 0.15.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 (76) hide show
  1. package/dist/acp/resolve-acp-spawn.d.ts +1 -1
  2. package/dist/acp/resolve-acp-spawn.d.ts.map +1 -1
  3. package/dist/acp/resolve-acp-spawn.js +2 -2
  4. package/dist/acp/resolve-acp-spawn.js.map +1 -1
  5. package/dist/conductor/conductor-session-driver.d.ts +5 -2
  6. package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
  7. package/dist/conductor/conductor-session-driver.js +6 -4
  8. package/dist/conductor/conductor-session-driver.js.map +1 -1
  9. package/dist/conductor/conductor-session.d.ts +2 -0
  10. package/dist/conductor/conductor-session.d.ts.map +1 -1
  11. package/dist/conductor/conductor-session.js +186 -67
  12. package/dist/conductor/conductor-session.js.map +1 -1
  13. package/dist/conductor/index.d.ts +2 -0
  14. package/dist/conductor/index.d.ts.map +1 -1
  15. package/dist/conductor/index.js +1 -0
  16. package/dist/conductor/index.js.map +1 -1
  17. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  18. package/dist/conductor/pi-conductor-agent.js +18 -1
  19. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  20. package/dist/conductor/session/dispatch-hold-reminder-monitor.d.ts +28 -0
  21. package/dist/conductor/session/dispatch-hold-reminder-monitor.d.ts.map +1 -0
  22. package/dist/conductor/session/dispatch-hold-reminder-monitor.js +88 -0
  23. package/dist/conductor/session/dispatch-hold-reminder-monitor.js.map +1 -0
  24. package/dist/conductor/session/dispatch-hold.d.ts +1 -1
  25. package/dist/conductor/session/dispatch-hold.d.ts.map +1 -1
  26. package/dist/config/defaults.d.ts.map +1 -1
  27. package/dist/config/defaults.js +3 -0
  28. package/dist/config/defaults.js.map +1 -1
  29. package/dist/config/index.d.ts +1 -1
  30. package/dist/config/index.d.ts.map +1 -1
  31. package/dist/config/load-ensemble-config.d.ts +2 -0
  32. package/dist/config/load-ensemble-config.d.ts.map +1 -1
  33. package/dist/config/load-ensemble-config.js +18 -5
  34. package/dist/config/load-ensemble-config.js.map +1 -1
  35. package/dist/config/parse-config.d.ts.map +1 -1
  36. package/dist/config/parse-config.js +12 -0
  37. package/dist/config/parse-config.js.map +1 -1
  38. package/dist/config/types.d.ts +5 -0
  39. package/dist/config/types.d.ts.map +1 -1
  40. package/dist/dispatch/worker-acp-session.d.ts +2 -2
  41. package/dist/dispatch/worker-acp-session.d.ts.map +1 -1
  42. package/dist/dispatch/worker-acp-session.js +1 -1
  43. package/dist/dispatch/worker-acp-session.js.map +1 -1
  44. package/dist/github/github-monitor-cursor.d.ts +10 -10
  45. package/dist/github/github-monitor-cursor.d.ts.map +1 -1
  46. package/dist/github/github-monitor-cursor.js +1 -1
  47. package/dist/github/github-monitor-cursor.js.map +1 -1
  48. package/dist/mcp/load-mcp-config.d.ts +1 -1
  49. package/dist/mcp/load-mcp-config.js +1 -1
  50. package/dist/operator-runtime.d.ts +8 -4
  51. package/dist/operator-runtime.d.ts.map +1 -1
  52. package/dist/operator-runtime.js +4 -2
  53. package/dist/operator-runtime.js.map +1 -1
  54. package/dist/prompt/modules/ensemble/worker-base-module.d.ts.map +1 -1
  55. package/dist/prompt/modules/ensemble/worker-base-module.js +1 -0
  56. package/dist/prompt/modules/ensemble/worker-base-module.js.map +1 -1
  57. package/dist/session/index.d.ts +8 -4
  58. package/dist/session/index.d.ts.map +1 -1
  59. package/dist/session/index.js +4 -2
  60. package/dist/session/index.js.map +1 -1
  61. package/dist/session/open-question-control.d.ts +26 -2
  62. package/dist/session/open-question-control.d.ts.map +1 -1
  63. package/dist/session/open-question-control.js +96 -10
  64. package/dist/session/open-question-control.js.map +1 -1
  65. package/dist/session/session-resume-store.d.ts +119 -0
  66. package/dist/session/session-resume-store.d.ts.map +1 -0
  67. package/dist/session/session-resume-store.js +354 -0
  68. package/dist/session/session-resume-store.js.map +1 -0
  69. package/dist/session/tool-call-snapshot-history.d.ts +1 -1
  70. package/dist/session/tool-call-snapshot-history.js +4 -4
  71. package/dist/session/tool-call-snapshot-history.js.map +1 -1
  72. package/package.json +1 -1
  73. package/dist/session/session-sidecar.d.ts +0 -76
  74. package/dist/session/session-sidecar.d.ts.map +0 -1
  75. package/dist/session/session-sidecar.js +0 -261
  76. package/dist/session/session-sidecar.js.map +0 -1
@@ -31,17 +31,18 @@ import { resolvePiConductorProvider } from './conductor-pi-auth.js';
31
31
  import { SessionLogger } from './session/session-logger.js';
32
32
  import { SessionEventQueue } from './session/session-event-queue.js';
33
33
  import { bufferDispatchHoldEvents, createDispatchHoldState, pruneStalePermissionEvents, } from './session/dispatch-hold.js';
34
+ import { createDispatchHoldReminderMonitor, formatDispatchHoldReminderMessage, } from './session/dispatch-hold-reminder-monitor.js';
34
35
  import { operatorInputMaxTurns, resolveMaxTurns, } from './session-policy.js';
35
36
  import { runConductorSessionDriver } from './conductor-session-driver.js';
36
37
  import { isOperatorExitCommand } from './operator-exit.js';
37
38
  import { isOperatorCloseCommand } from './operator-close.js';
38
39
  import { isOperatorReconnectCommand } from './operator-reconnect.js';
39
40
  import { parseOperatorMessageDelivery } from './operator-command.js';
40
- import { assertSessionSidecarMatches, requireSessionSidecarForResume, saveSessionSidecar, SESSION_SIDECAR_VERSION, sessionSidecarPath, } from '../session/session-sidecar.js';
41
+ import { assertSessionResumeStoreMatches, requireSessionResumeStoreForResume, saveSessionResumeStore, SESSION_RESUME_STORE_VERSION, sessionResumeStorePath, } from '../session/session-resume-store.js';
41
42
  import { saveSessionStatusLine, sessionStateStorePath, loadSessionStateStore, } from '../session/session-state-store.js';
42
43
  import { loadToolCallSnapshotHistory, pruneExpiredToolCallSnapshotHistoryFiles, saveToolCallSnapshotHistory, toolCallSnapshotHistoryPath, } from '../session/tool-call-snapshot-history.js';
43
44
  import { loadOpenQuestionsStore, openQuestionsStorePath, saveOpenQuestionsStore, } from '../session/open-questions-store.js';
44
- import { OpenQuestionControlOperationError, startOpenQuestionControlServer, } from '../session/open-question-control.js';
45
+ import { CONDUCTOR_MESSAGE_HISTORY_MAX_ENTRIES, OpenQuestionControlOperationError, startOpenQuestionControlServer, } from '../session/open-question-control.js';
45
46
  import { formatPermissionSummaryForOperator } from '../permission/format-permission-summary-for-operator.js';
46
47
  import { enrichSessionUsageWithCost } from '../usage/enrich-session-usage-cost.js';
47
48
  import { SessionUsageTracker } from '../usage/session-usage-tracker.js';
@@ -89,12 +90,17 @@ export async function runConductorSession(options) {
89
90
  const escalations = [];
90
91
  const openQuestions = new OpenQuestionRegistry();
91
92
  let harnessSocketPath;
93
+ let harnessPhase;
94
+ let harnessModifiedAt;
95
+ let reportHarnessActivity = () => { };
92
96
  let openQuestionControlServer;
93
97
  let lastConductorMessage = null;
98
+ const conductorMessages = [];
94
99
  const eventQueue = new SessionEventQueue();
95
100
  const openQuestionDispatchReceipts = new WeakMap();
96
101
  const pendingOpenQuestionDispatchReceipts = new Set();
97
102
  const dispatchHoldState = createDispatchHoldState();
103
+ let dispatchHoldReminderMonitor;
98
104
  const onDispatchHoldChanged = (change) => {
99
105
  sessionLogger.emit({
100
106
  type: 'conductor.dispatch_hold',
@@ -105,6 +111,7 @@ export async function runConductorSession(options) {
105
111
  ? { flushedEventCount: change.flushedEventCount }
106
112
  : {}),
107
113
  });
114
+ dispatchHoldReminderMonitor?.onHoldChanged(change.hold);
108
115
  };
109
116
  let activeProfile = options.profile;
110
117
  let activeLazyWorkers = ensembleConfig.session.lazyWorkers;
@@ -119,11 +126,11 @@ export async function runConductorSession(options) {
119
126
  let resumeToolCallSnapshots = [];
120
127
  let toolCallHistoryNeedsRewrite = false;
121
128
  if (options.resumeAgentId) {
122
- const loadedSidecar = await requireSessionSidecarForResume({
129
+ const loadedResumeStore = await requireSessionResumeStoreForResume({
123
130
  repoRoot: options.repoRoot,
124
131
  conductorAgentId: options.resumeAgentId,
125
132
  });
126
- assertSessionSidecarMatches(loadedSidecar, {
133
+ assertSessionResumeStoreMatches(loadedResumeStore, {
127
134
  conductorAgentId: options.resumeAgentId,
128
135
  issueUrl: options.issueUrl,
129
136
  repoRoot: options.repoRoot,
@@ -133,18 +140,22 @@ export async function runConductorSession(options) {
133
140
  });
134
141
  // A previously completed lifecycle no longer describes this instance once
135
142
  // resume begins. Persist unknown before restoring or attaching anything so
136
- // a crash before the first regular sidecar flush cannot leave stale
143
+ // a crash before the first regular SessionResumeStore flush cannot leave stale
137
144
  // `closed` / `exited` data behind.
138
- const sidecar = {
139
- ...loadedSidecar,
145
+ const resumeStore = {
146
+ ...loadedResumeStore,
140
147
  lifecycle: 'unknown',
148
+ harnessPhase: 'running',
149
+ modifiedAt: Date.now(),
141
150
  harnessSocketPath: undefined,
142
151
  updatedAt: Date.now(),
143
152
  };
144
- await saveSessionSidecar(sessionSidecarPath({
153
+ harnessPhase = 'running';
154
+ harnessModifiedAt = resumeStore.modifiedAt;
155
+ await saveSessionResumeStore(sessionResumeStorePath({
145
156
  repoRoot: options.repoRoot,
146
157
  conductorAgentId: options.resumeAgentId,
147
- }), sidecar);
158
+ }), resumeStore);
148
159
  const history = await loadToolCallSnapshotHistory({
149
160
  repoRoot: options.repoRoot,
150
161
  conductorAgentId: options.resumeAgentId,
@@ -167,8 +178,8 @@ export async function runConductorSession(options) {
167
178
  }
168
179
  else {
169
180
  const legacySnapshot = {
170
- sequence: sidecar.sequence,
171
- openQuestions: sidecar.openQuestions,
181
+ sequence: resumeStore.sequence,
182
+ openQuestions: resumeStore.openQuestions,
172
183
  };
173
184
  openQuestions.restore(legacySnapshot);
174
185
  try {
@@ -178,15 +189,15 @@ export async function runConductorSession(options) {
178
189
  const detail = error instanceof Error ? error.message : String(error);
179
190
  sessionLogger.emit({
180
191
  type: 'harness.warning',
181
- message: `旧 sidecar から復元した open question を YAML にバックフィルできませんでした。今回のセッションは継続します。 error=${detail}`,
192
+ message: `旧形式の SessionResumeStore から復元した open question を YAML にバックフィルできませんでした。今回のセッションは継続します。 error=${detail}`,
182
193
  });
183
194
  }
184
195
  }
185
- activeProfile = sidecar.profile;
186
- // Sidecars written before this setting always started every worker eagerly.
187
- activeLazyWorkers = sidecar.lazyWorkers ?? false;
188
- githubMonitorCursor = sidecar.githubMonitor;
189
- for (const [name, worker] of Object.entries(sidecar.workers)) {
196
+ activeProfile = resumeStore.profile;
197
+ // ResumeStores written before this setting always started every worker eagerly.
198
+ activeLazyWorkers = resumeStore.lazyWorkers ?? false;
199
+ githubMonitorCursor = resumeStore.githubMonitor;
200
+ for (const [name, worker] of Object.entries(resumeStore.workers)) {
190
201
  workerSessions.set(name, {
191
202
  acpSessionId: worker.acpSessionId,
192
203
  ...(worker.acpCwd ? { acpCwd: worker.acpCwd } : {}),
@@ -292,8 +303,8 @@ export async function runConductorSession(options) {
292
303
  }
293
304
  const permissionPipeline = options.permissionPipeline ??
294
305
  new PermissionPipeline({ policy: options.permissionPolicy });
295
- let scheduleSidecarFlush = () => { };
296
- let flushSidecar = async () => { };
306
+ let scheduleSessionResumeStoreFlush = () => { };
307
+ let flushSessionResumeStore = async () => { };
297
308
  let scheduleToolCallHistoryFlush = () => { };
298
309
  let flushToolCallHistory = async () => { };
299
310
  let toolCallHistoryDirty = toolCallHistoryNeedsRewrite;
@@ -436,7 +447,7 @@ export async function runConductorSession(options) {
436
447
  : {}),
437
448
  });
438
449
  eventQueue.enqueue({ type: 'worker.completed', result });
439
- scheduleSidecarFlush();
450
+ scheduleSessionResumeStoreFlush();
440
451
  },
441
452
  onWorkerFailed: (failure) => {
442
453
  sessionLogger.emit({ type: 'worker.failed', failure });
@@ -445,6 +456,7 @@ export async function runConductorSession(options) {
445
456
  onPromptTelemetry: (event) => {
446
457
  switch (event.phase) {
447
458
  case 'started':
459
+ reportHarnessActivity('worker', true);
448
460
  sessionLogger.emit({
449
461
  type: 'harness.worker.prompt.started',
450
462
  name: event.name,
@@ -454,6 +466,7 @@ export async function runConductorSession(options) {
454
466
  });
455
467
  break;
456
468
  case 'completed':
469
+ reportHarnessActivity('worker', false);
457
470
  sessionLogger.emit({
458
471
  type: 'harness.worker.prompt.completed',
459
472
  name: event.name,
@@ -464,6 +477,7 @@ export async function runConductorSession(options) {
464
477
  });
465
478
  break;
466
479
  case 'failed':
480
+ reportHarnessActivity('worker', false);
467
481
  sessionLogger.emit({
468
482
  type: 'harness.worker.prompt.failed',
469
483
  name: event.name,
@@ -567,14 +581,14 @@ export async function runConductorSession(options) {
567
581
  sessionLogger.emit({ type: 'escalation.recorded', record });
568
582
  options.onEscalated?.(record);
569
583
  }
570
- scheduleSidecarFlush();
584
+ scheduleSessionResumeStoreFlush();
571
585
  };
572
586
  const askHumanTools = createAskHumanTool({
573
587
  registry: openQuestions,
574
588
  onEnqueued: (question) => {
575
589
  sessionLogger.emit({ type: 'open.question.enqueued', question });
576
590
  options.onOpenQuestionEnqueued?.(question);
577
- scheduleSidecarFlush();
591
+ scheduleSessionResumeStoreFlush();
578
592
  },
579
593
  });
580
594
  const answerOpenQuestionTools = createAnswerOpenQuestionTool({
@@ -621,7 +635,7 @@ export async function runConductorSession(options) {
621
635
  // Do not report success to the conductor until the explicit watch is
622
636
  // durable. This closes the crash window between the tool result and a
623
637
  // later best-effort flush.
624
- await flushSidecar();
638
+ await flushSessionResumeStore();
625
639
  },
626
640
  });
627
641
  const unregisterGitHubWatchTools = createUnregisterGitHubWatchTool({
@@ -632,7 +646,7 @@ export async function runConductorSession(options) {
632
646
  },
633
647
  onUnregistered: async (prNumber) => {
634
648
  githubMonitor?.unregisterPullRequest?.(prNumber);
635
- await flushSidecar();
649
+ await flushSessionResumeStore();
636
650
  },
637
651
  });
638
652
  let conductorAgent;
@@ -752,14 +766,15 @@ export async function runConductorSession(options) {
752
766
  });
753
767
  },
754
768
  };
755
- let sidecarFlushQueue = Promise.resolve();
769
+ let sessionResumeStoreWriteQueue = Promise.resolve();
756
770
  let terminalLifecycle;
757
- const enqueueSidecarWrite = (write) => {
758
- const nextWrite = sidecarFlushQueue.then(write);
759
- sidecarFlushQueue = nextWrite.catch(() => { });
771
+ let terminalResumeStoreFinalFlushQueued = false;
772
+ const enqueueSessionResumeStoreWrite = (write) => {
773
+ const nextWrite = sessionResumeStoreWriteQueue.then(write);
774
+ sessionResumeStoreWriteQueue = nextWrite.catch(() => { });
760
775
  return nextWrite;
761
776
  };
762
- const createSessionSidecar = (snapshot, lifecycle) => {
777
+ const createSessionResumeStore = (snapshot, lifecycle) => {
763
778
  const workers = {};
764
779
  for (const [name, worker] of workerSessions) {
765
780
  workers[name] = {
@@ -768,8 +783,13 @@ export async function runConductorSession(options) {
768
783
  ...(worker.acpSpawn ? { acpSpawn: worker.acpSpawn } : {}),
769
784
  };
770
785
  }
786
+ const persistedLifecycle = terminalLifecycle ??
787
+ lifecycle ??
788
+ (harnessPhase === 'running' || harnessPhase === 'idle'
789
+ ? 'unknown'
790
+ : undefined);
771
791
  return {
772
- version: SESSION_SIDECAR_VERSION,
792
+ version: SESSION_RESUME_STORE_VERSION,
773
793
  conductorAgentId: conductorHandle.conductor.agentId,
774
794
  conductorBackend,
775
795
  lazyWorkers: activeLazyWorkers,
@@ -778,13 +798,15 @@ export async function runConductorSession(options) {
778
798
  repoRoot: options.repoRoot,
779
799
  profile: structuredClone(activeProfile),
780
800
  ...(options.profilePath ? { profilePath: options.profilePath } : {}),
801
+ ...(harnessPhase ? { harnessPhase } : {}),
802
+ ...(harnessModifiedAt !== undefined
803
+ ? { modifiedAt: harnessModifiedAt }
804
+ : {}),
781
805
  openQuestions: snapshot.openQuestions,
782
806
  sequence: snapshot.sequence,
783
807
  workers,
784
808
  mcpConfigDigest,
785
- ...((terminalLifecycle ?? lifecycle)
786
- ? { lifecycle: terminalLifecycle ?? lifecycle }
787
- : {}),
809
+ ...(persistedLifecycle ? { lifecycle: persistedLifecycle } : {}),
788
810
  ...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
789
811
  updatedAt: Date.now(),
790
812
  };
@@ -795,33 +817,90 @@ export async function runConductorSession(options) {
795
817
  conductorAgentId: conductorHandle.conductor.agentId,
796
818
  }), snapshot);
797
819
  };
798
- const persistSessionSidecar = async (snapshot, lifecycle) => {
799
- await saveSessionSidecar(sessionSidecarPath({
820
+ const persistSessionResumeStore = async (snapshot, lifecycle) => {
821
+ await saveSessionResumeStore(sessionResumeStorePath({
800
822
  repoRoot: options.repoRoot,
801
823
  conductorAgentId: conductorHandle.conductor.agentId,
802
- }), createSessionSidecar(snapshot, lifecycle));
824
+ }), createSessionResumeStore(snapshot, lifecycle));
803
825
  };
804
- flushSidecar = (lifecycle) => enqueueSidecarWrite(async () => {
805
- const snapshot = openQuestions.snapshot();
806
- await persistOpenQuestionsSnapshot(snapshot);
807
- await persistSessionSidecar(snapshot, lifecycle);
808
- });
809
- const reportSidecarFlushFailure = (error) => {
826
+ flushSessionResumeStore = (lifecycle) => {
827
+ if (terminalResumeStoreFinalFlushQueued && lifecycle === undefined) {
828
+ return sessionResumeStoreWriteQueue;
829
+ }
830
+ if (lifecycle !== undefined && lifecycle !== 'unknown') {
831
+ terminalResumeStoreFinalFlushQueued = true;
832
+ }
833
+ return enqueueSessionResumeStoreWrite(async () => {
834
+ if (lifecycle !== undefined) {
835
+ terminalLifecycle = lifecycle;
836
+ harnessPhase =
837
+ lifecycle === 'closed'
838
+ ? 'close'
839
+ : lifecycle === 'exited'
840
+ ? 'exit'
841
+ : lifecycle === 'interrupted'
842
+ ? 'abort'
843
+ : undefined;
844
+ harnessModifiedAt = harnessPhase ? Date.now() : undefined;
845
+ }
846
+ const snapshot = openQuestions.snapshot();
847
+ await persistOpenQuestionsSnapshot(snapshot);
848
+ await persistSessionResumeStore(snapshot, lifecycle);
849
+ });
850
+ };
851
+ const reportResumeStoreFlushFailure = (error) => {
810
852
  const detail = error instanceof Error ? error.message : String(error);
811
853
  sessionLogger.emit({
812
854
  type: 'harness.warning',
813
- message: `Open Question YAML / session sidecar を保存できませんでした。最新状態から再開できず、直前に保存された状態からの再開になる場合があります。 error=${detail}`,
855
+ message: `Open Question YAML / SessionResumeStore を保存できませんでした。最新状態から再開できず、直前に保存された状態からの再開になる場合があります。 error=${detail}`,
814
856
  });
815
857
  };
816
- const reportSessionSidecarWriteFailure = (error) => {
858
+ const reportSessionResumeStoreWriteFailure = (error) => {
817
859
  const detail = error instanceof Error ? error.message : String(error);
818
860
  sessionLogger.emit({
819
861
  type: 'harness.warning',
820
- message: `Session sidecar の更新に失敗しました。Open Question YAML の保存は完了しており、sidecar は後続 flush で同期します。 error=${detail}`,
862
+ message: `SessionResumeStore の更新に失敗しました。Open Question YAML の保存は完了しており、保存状態は後続 flush で同期します。 error=${detail}`,
821
863
  });
822
864
  };
823
- scheduleSidecarFlush = () => {
824
- void flushSidecar().catch(reportSidecarFlushFailure);
865
+ scheduleSessionResumeStoreFlush = () => {
866
+ void flushSessionResumeStore().catch(reportResumeStoreFlushFailure);
867
+ };
868
+ let conductorSendCount = 0;
869
+ let workerPromptCount = 0;
870
+ let idlePhaseTimer;
871
+ const persistHarnessPhase = (phase) => {
872
+ if (teardownStarted || harnessPhase === phase)
873
+ return;
874
+ if (idlePhaseTimer) {
875
+ clearTimeout(idlePhaseTimer);
876
+ idlePhaseTimer = undefined;
877
+ }
878
+ harnessPhase = phase;
879
+ harnessModifiedAt = Date.now();
880
+ void flushSessionResumeStore().catch(reportResumeStoreFlushFailure);
881
+ };
882
+ reportHarnessActivity = (kind, active) => {
883
+ if (teardownStarted)
884
+ return;
885
+ if (kind === 'conductor') {
886
+ conductorSendCount = Math.max(0, conductorSendCount + (active ? 1 : -1));
887
+ }
888
+ else {
889
+ workerPromptCount = Math.max(0, workerPromptCount + (active ? 1 : -1));
890
+ }
891
+ if (conductorSendCount + workerPromptCount > 0) {
892
+ persistHarnessPhase('running');
893
+ return;
894
+ }
895
+ if (idlePhaseTimer || harnessPhase === 'idle')
896
+ return;
897
+ idlePhaseTimer = setTimeout(() => {
898
+ idlePhaseTimer = undefined;
899
+ if (conductorSendCount + workerPromptCount === 0) {
900
+ persistHarnessPhase('idle');
901
+ }
902
+ }, 250);
903
+ idlePhaseTimer.unref?.();
825
904
  };
826
905
  const toolCallHistoryPath = toolCallSnapshotHistoryPath({
827
906
  repoRoot: options.repoRoot,
@@ -894,7 +973,7 @@ export async function runConductorSession(options) {
894
973
  shutdownSignal,
895
974
  onCursorChange: (cursor) => {
896
975
  githubMonitorCursor = cursor;
897
- scheduleSidecarFlush();
976
+ scheduleSessionResumeStoreFlush();
898
977
  },
899
978
  onUpdate: (payload) => {
900
979
  sessionLogger.emit({
@@ -953,6 +1032,21 @@ export async function runConductorSession(options) {
953
1032
  });
954
1033
  githubMonitor.start();
955
1034
  }
1035
+ dispatchHoldReminderMonitor = createDispatchHoldReminderMonitor({
1036
+ getSnapshot: () => ({
1037
+ hold: dispatchHoldState.dispatchHold,
1038
+ heldEventCount: dispatchHoldState.heldEvents.length,
1039
+ }),
1040
+ onReminder: ({ heldEventCount }) => {
1041
+ sessionLogger.emit({
1042
+ type: 'harness.warning',
1043
+ message: formatDispatchHoldReminderMessage(heldEventCount, options.dispatchHoldReminderMs),
1044
+ });
1045
+ },
1046
+ reminderIntervalMs: options.dispatchHoldReminderMs,
1047
+ shutdownSignal,
1048
+ });
1049
+ dispatchHoldReminderMonitor.start();
956
1050
  let permissionDeadlockMonitor;
957
1051
  if (!options.disablePermissionDeadlockMonitor) {
958
1052
  permissionDeadlockMonitor = createPermissionDeadlockMonitor({
@@ -990,7 +1084,7 @@ export async function runConductorSession(options) {
990
1084
  let operatorRequestedClose = false;
991
1085
  let disposeOperatorInput;
992
1086
  const onOperatorMessageQueued = (delivery) => {
993
- scheduleSidecarFlush();
1087
+ scheduleSessionResumeStoreFlush();
994
1088
  if (delivery !== 'interrupt')
995
1089
  return;
996
1090
  const cancel = conductorHandle.conductor.cancel;
@@ -1089,6 +1183,7 @@ export async function runConductorSession(options) {
1089
1183
  openQuestionControlServer = await startOpenQuestionControlServer({
1090
1184
  listOpenQuestions: () => openQuestions.listOpen(),
1091
1185
  getLastConductorMessage: () => lastConductorMessage,
1186
+ listConductorMessages: () => conductorMessages,
1092
1187
  observeSession: async () => {
1093
1188
  await sessionStateWriteQueue;
1094
1189
  const sessionState = await loadSessionStateStore(sessionStateStorePath({
@@ -1115,11 +1210,11 @@ export async function runConductorSession(options) {
1115
1210
  throw new OpenQuestionControlOperationError(-32004, 'Open Question is not available.');
1116
1211
  }
1117
1212
  let accepted = false;
1118
- let sidecarWriteError;
1213
+ let resumeStoreWriteError;
1119
1214
  let dispatchReceipt;
1120
1215
  let answered;
1121
1216
  const escalatedRecords = [];
1122
- await enqueueSidecarWrite(async () => {
1217
+ await enqueueSessionResumeStoreWrite(async () => {
1123
1218
  const stagedOpenQuestions = new OpenQuestionRegistry();
1124
1219
  stagedOpenQuestions.restore(openQuestions.snapshot());
1125
1220
  const stagedAccepted = submitOperatorInput({
@@ -1172,12 +1267,12 @@ export async function runConductorSession(options) {
1172
1267
  return;
1173
1268
  }
1174
1269
  try {
1175
- await persistSessionSidecar(openQuestions.snapshot());
1270
+ await persistSessionResumeStore(openQuestions.snapshot());
1176
1271
  }
1177
1272
  catch (error) {
1178
1273
  // YAML is authoritative and is already durable. Keep the accepted
1179
1274
  // answer/notification, report the mirror failure, then repair it.
1180
- sidecarWriteError = error;
1275
+ resumeStoreWriteError = error;
1181
1276
  }
1182
1277
  });
1183
1278
  if (!accepted || !answered || answered.status !== 'answered') {
@@ -1195,9 +1290,9 @@ export async function runConductorSession(options) {
1195
1290
  });
1196
1291
  }
1197
1292
  }
1198
- if (sidecarWriteError !== undefined) {
1199
- reportSessionSidecarWriteFailure(sidecarWriteError);
1200
- scheduleSidecarFlush();
1293
+ if (resumeStoreWriteError !== undefined) {
1294
+ reportSessionResumeStoreWriteFailure(resumeStoreWriteError);
1295
+ scheduleSessionResumeStoreFlush();
1201
1296
  }
1202
1297
  if (!dispatchReceipt) {
1203
1298
  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);
@@ -1207,7 +1302,7 @@ export async function runConductorSession(options) {
1207
1302
  },
1208
1303
  });
1209
1304
  harnessSocketPath = openQuestionControlServer.socketPath;
1210
- await flushSidecar().catch(reportSidecarFlushFailure);
1305
+ await flushSessionResumeStore().catch(reportResumeStoreFlushFailure);
1211
1306
  const driverResult = await runConductorSessionDriver({
1212
1307
  issueUrl: options.issueUrl,
1213
1308
  initialPrompt: conductorBackend === 'pi'
@@ -1253,6 +1348,7 @@ export async function runConductorSession(options) {
1253
1348
  // session briefing.
1254
1349
  skipInitialSend: Boolean(options.resumeAgentId),
1255
1350
  onSendStarted: (info) => {
1351
+ reportHarnessActivity('conductor', true);
1256
1352
  sessionLogger.emit({
1257
1353
  type: 'conductor.send.started',
1258
1354
  sendCount: info.sendCount,
@@ -1303,11 +1399,26 @@ export async function runConductorSession(options) {
1303
1399
  autonomousTurns = info.autonomousTurns;
1304
1400
  const responseText = info.result?.trim();
1305
1401
  if (info.status === 'finished' && responseText) {
1402
+ const at = Date.now();
1306
1403
  lastConductorMessage = {
1307
1404
  text: responseText,
1308
- at: Date.now(),
1405
+ at,
1309
1406
  sendCount: info.sendCount,
1310
1407
  };
1408
+ if (info.dispatchSource !== 'initial') {
1409
+ conductorMessages.push({
1410
+ text: responseText,
1411
+ inbound: info.inbound,
1412
+ at,
1413
+ sendCount: info.sendCount,
1414
+ ...(info.dispatchSource
1415
+ ? { dispatchSource: info.dispatchSource }
1416
+ : {}),
1417
+ });
1418
+ if (conductorMessages.length > CONDUCTOR_MESSAGE_HISTORY_MAX_ENTRIES) {
1419
+ conductorMessages.shift();
1420
+ }
1421
+ }
1311
1422
  }
1312
1423
  const githubUpdateIds = info.githubUpdateIds ?? [];
1313
1424
  if (info.status === 'finished' &&
@@ -1338,6 +1449,7 @@ export async function runConductorSession(options) {
1338
1449
  workerDispatches: info.workerDispatches,
1339
1450
  workerFailures: info.workerFailures,
1340
1451
  });
1452
+ reportHarnessActivity('conductor', false);
1341
1453
  outboundWorkersThisSend.clear();
1342
1454
  if (isConductorSendAuthError({
1343
1455
  runId: info.runId,
@@ -1357,7 +1469,7 @@ export async function runConductorSession(options) {
1357
1469
  : { backend: 'cursor' }),
1358
1470
  });
1359
1471
  }
1360
- scheduleSidecarFlush();
1472
+ scheduleSessionResumeStoreFlush();
1361
1473
  }
1362
1474
  async function buildResult() {
1363
1475
  sessionLogger.finish(stopReason);
@@ -1390,8 +1502,11 @@ export async function runConductorSession(options) {
1390
1502
  : shutdownSignal.aborted
1391
1503
  ? 'interrupted'
1392
1504
  : 'unknown';
1393
- terminalLifecycle = lifecycle;
1394
1505
  teardownStarted = true;
1506
+ if (idlePhaseTimer) {
1507
+ clearTimeout(idlePhaseTimer);
1508
+ idlePhaseTimer = undefined;
1509
+ }
1395
1510
  for (const receipt of pendingOpenQuestionDispatchReceipts) {
1396
1511
  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
1512
  }
@@ -1440,6 +1555,10 @@ export async function runConductorSession(options) {
1440
1555
  teardownPhases.openQuestionControl = Date.now() - phaseStart;
1441
1556
  }
1442
1557
  disposeOperatorInput?.();
1558
+ if (dispatchHoldReminderMonitor) {
1559
+ emitTeardownPhase('dispatchHoldReminderMonitor');
1560
+ dispatchHoldReminderMonitor.stop();
1561
+ }
1443
1562
  if (permissionDeadlockMonitor) {
1444
1563
  emitTeardownPhase('permissionDeadlockMonitor');
1445
1564
  permissionDeadlockMonitor.stop();
@@ -1486,13 +1605,13 @@ export async function runConductorSession(options) {
1486
1605
  githubMonitorStopped = true;
1487
1606
  }
1488
1607
  try {
1489
- emitTeardownPhase('flushSidecar');
1608
+ emitTeardownPhase('flushSessionResumeStore');
1490
1609
  const phaseStart = Date.now();
1491
- await flushSidecar();
1492
- teardownPhases.flushSidecar = Date.now() - phaseStart;
1610
+ await flushSessionResumeStore();
1611
+ teardownPhases.flushSessionResumeStore = Date.now() - phaseStart;
1493
1612
  }
1494
1613
  catch (error) {
1495
- reportSidecarFlushFailure(error);
1614
+ reportResumeStoreFlushFailure(error);
1496
1615
  }
1497
1616
  await flushToolCallHistoryBestEffort();
1498
1617
  const runGithubMonitorStop = async () => {
@@ -1557,12 +1676,12 @@ export async function runConductorSession(options) {
1557
1676
  }
1558
1677
  finally {
1559
1678
  try {
1560
- emitTeardownPhase('flushSidecar');
1679
+ emitTeardownPhase('flushSessionResumeStore');
1561
1680
  await sessionStateWriteQueue;
1562
- await flushSidecar(lifecycle);
1681
+ await flushSessionResumeStore(lifecycle);
1563
1682
  }
1564
1683
  catch (error) {
1565
- reportSidecarFlushFailure(error);
1684
+ reportResumeStoreFlushFailure(error);
1566
1685
  }
1567
1686
  process.off('SIGINT', onTeardownSignal);
1568
1687
  process.off('SIGTERM', onTeardownSignal);