@agents-ensemble/core 0.13.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.
- package/dist/conductor/conductor-send-reconnect.d.ts +3 -0
- package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
- package/dist/conductor/conductor-send-reconnect.js +7 -1
- package/dist/conductor/conductor-send-reconnect.js.map +1 -1
- package/dist/conductor/conductor-session-driver.d.ts +6 -0
- package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
- package/dist/conductor/conductor-session-driver.js +17 -1
- package/dist/conductor/conductor-session-driver.js.map +1 -1
- package/dist/conductor/conductor-session.d.ts +4 -0
- package/dist/conductor/conductor-session.d.ts.map +1 -1
- package/dist/conductor/conductor-session.js +329 -49
- package/dist/conductor/conductor-session.js.map +1 -1
- package/dist/conductor/session/session-logger.d.ts +1 -0
- package/dist/conductor/session/session-logger.d.ts.map +1 -1
- package/dist/conductor/session/session-logger.js +4 -0
- package/dist/conductor/session/session-logger.js.map +1 -1
- package/dist/conductor/submit-operator-input.d.ts +4 -1
- package/dist/conductor/submit-operator-input.d.ts.map +1 -1
- package/dist/conductor/submit-operator-input.js +4 -1
- package/dist/conductor/submit-operator-input.js.map +1 -1
- package/dist/dispatch/report-session-state-tool.d.ts +3 -1
- package/dist/dispatch/report-session-state-tool.d.ts.map +1 -1
- package/dist/dispatch/report-session-state-tool.js +2 -1
- package/dist/dispatch/report-session-state-tool.js.map +1 -1
- package/dist/escalation/apply-operator-message.d.ts +2 -0
- package/dist/escalation/apply-operator-message.d.ts.map +1 -1
- package/dist/escalation/apply-operator-message.js +2 -0
- package/dist/escalation/apply-operator-message.js.map +1 -1
- package/dist/escalation/open-question.d.ts +1 -0
- package/dist/escalation/open-question.d.ts.map +1 -1
- package/dist/escalation/open-question.js +1 -1
- package/dist/escalation/open-question.js.map +1 -1
- package/dist/escalation/record-open-question-answer.d.ts +1 -0
- package/dist/escalation/record-open-question-answer.d.ts.map +1 -1
- package/dist/escalation/record-open-question-answer.js +1 -0
- package/dist/escalation/record-open-question-answer.js.map +1 -1
- package/dist/github/register-github-watch-tool.d.ts +4 -1
- package/dist/github/register-github-watch-tool.d.ts.map +1 -1
- package/dist/github/register-github-watch-tool.js +17 -1
- package/dist/github/register-github-watch-tool.js.map +1 -1
- package/dist/github/unregister-github-watch-tool.d.ts +4 -1
- package/dist/github/unregister-github-watch-tool.d.ts.map +1 -1
- package/dist/github/unregister-github-watch-tool.js +14 -3
- package/dist/github/unregister-github-watch-tool.js.map +1 -1
- package/dist/operator-runtime.d.ts +6 -2
- package/dist/operator-runtime.d.ts.map +1 -1
- package/dist/operator-runtime.js +4 -1
- package/dist/operator-runtime.js.map +1 -1
- package/dist/session/index.d.ts +5 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -1
- package/dist/session/open-question-control.d.ts +53 -0
- package/dist/session/open-question-control.d.ts.map +1 -0
- package/dist/session/open-question-control.js +410 -0
- package/dist/session/open-question-control.js.map +1 -0
- package/dist/session/open-questions-store.d.ts.map +1 -1
- package/dist/session/open-questions-store.js +6 -3
- package/dist/session/open-questions-store.js.map +1 -1
- package/dist/session/session-sidecar.d.ts +5 -0
- package/dist/session/session-sidecar.d.ts.map +1 -1
- package/dist/session/session-sidecar.js +24 -3
- package/dist/session/session-sidecar.js.map +1 -1
- package/dist/session/session-state-store.d.ts +21 -0
- package/dist/session/session-state-store.d.ts.map +1 -0
- package/dist/session/session-state-store.js +131 -0
- package/dist/session/session-state-store.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { createAnswerOpenQuestionTool } from '../escalation/answer-open-question
|
|
|
4
4
|
import { createAskHumanTool } from '../escalation/ask-human-tool.js';
|
|
5
5
|
import { createOpenQuestionListTools } from '../escalation/open-question-list-tools.js';
|
|
6
6
|
import { openQuestionToEscalationRecord } from '../escalation/open-question-to-escalation.js';
|
|
7
|
-
import { OpenQuestionRegistry } from '../escalation/open-question.js';
|
|
7
|
+
import { OpenQuestionRegistry, } from '../escalation/open-question.js';
|
|
8
8
|
import { deny } from '../permission/permission-broker.js';
|
|
9
9
|
import { PermissionPipeline } from '../permission/permission-pipeline.js';
|
|
10
10
|
import { createPermissionDeadlockMonitor, } from '../permission/permission-deadlock-monitor.js';
|
|
@@ -37,8 +37,10 @@ import { isOperatorExitCommand } from './operator-exit.js';
|
|
|
37
37
|
import { isOperatorCloseCommand } from './operator-close.js';
|
|
38
38
|
import { isOperatorReconnectCommand } from './operator-reconnect.js';
|
|
39
39
|
import { assertSessionSidecarMatches, requireSessionSidecarForResume, saveSessionSidecar, SESSION_SIDECAR_VERSION, sessionSidecarPath, } from '../session/session-sidecar.js';
|
|
40
|
+
import { saveSessionStatusLine, sessionStateStorePath, } from '../session/session-state-store.js';
|
|
40
41
|
import { loadToolCallSnapshotHistory, pruneExpiredToolCallSnapshotHistoryFiles, saveToolCallSnapshotHistory, toolCallSnapshotHistoryPath, } from '../session/tool-call-snapshot-history.js';
|
|
41
42
|
import { loadOpenQuestionsStore, openQuestionsStorePath, saveOpenQuestionsStore, } from '../session/open-questions-store.js';
|
|
43
|
+
import { OpenQuestionControlOperationError, startOpenQuestionControlServer, } from '../session/open-question-control.js';
|
|
42
44
|
import { formatPermissionSummaryForOperator } from '../permission/format-permission-summary-for-operator.js';
|
|
43
45
|
import { enrichSessionUsageWithCost } from '../usage/enrich-session-usage-cost.js';
|
|
44
46
|
import { SessionUsageTracker } from '../usage/session-usage-tracker.js';
|
|
@@ -54,6 +56,12 @@ import { createRegisterGitHubWatchTool } from '../github/register-github-watch-t
|
|
|
54
56
|
import { createUnregisterGitHubWatchTool } from '../github/unregister-github-watch-tool.js';
|
|
55
57
|
import { compileConductorSystemPrompt } from '../prompt/compile-system-prompt.js';
|
|
56
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
|
+
};
|
|
57
65
|
export async function runConductorSession(options) {
|
|
58
66
|
const userInstructions = await loadUserInstructions({
|
|
59
67
|
userEnsembleRoot: options.mcpConfigOptions?.userEnsembleRoot,
|
|
@@ -79,7 +87,11 @@ export async function runConductorSession(options) {
|
|
|
79
87
|
}
|
|
80
88
|
const escalations = [];
|
|
81
89
|
const openQuestions = new OpenQuestionRegistry();
|
|
90
|
+
let harnessSocketPath;
|
|
91
|
+
let openQuestionControlServer;
|
|
82
92
|
const eventQueue = new SessionEventQueue();
|
|
93
|
+
const openQuestionDispatchReceipts = new WeakMap();
|
|
94
|
+
const pendingOpenQuestionDispatchReceipts = new Set();
|
|
83
95
|
const dispatchHoldState = createDispatchHoldState();
|
|
84
96
|
const onDispatchHoldChanged = (change) => {
|
|
85
97
|
sessionLogger.emit({
|
|
@@ -105,11 +117,11 @@ export async function runConductorSession(options) {
|
|
|
105
117
|
let resumeToolCallSnapshots = [];
|
|
106
118
|
let toolCallHistoryNeedsRewrite = false;
|
|
107
119
|
if (options.resumeAgentId) {
|
|
108
|
-
const
|
|
120
|
+
const loadedSidecar = await requireSessionSidecarForResume({
|
|
109
121
|
repoRoot: options.repoRoot,
|
|
110
122
|
conductorAgentId: options.resumeAgentId,
|
|
111
123
|
});
|
|
112
|
-
assertSessionSidecarMatches(
|
|
124
|
+
assertSessionSidecarMatches(loadedSidecar, {
|
|
113
125
|
conductorAgentId: options.resumeAgentId,
|
|
114
126
|
issueUrl: options.issueUrl,
|
|
115
127
|
repoRoot: options.repoRoot,
|
|
@@ -117,6 +129,20 @@ export async function runConductorSession(options) {
|
|
|
117
129
|
mcpConfigDigest,
|
|
118
130
|
mcpConfigConfigured,
|
|
119
131
|
});
|
|
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);
|
|
120
146
|
const history = await loadToolCallSnapshotHistory({
|
|
121
147
|
repoRoot: options.repoRoot,
|
|
122
148
|
conductorAgentId: options.resumeAgentId,
|
|
@@ -195,12 +221,62 @@ export async function runConductorSession(options) {
|
|
|
195
221
|
? new AbortController()
|
|
196
222
|
: undefined;
|
|
197
223
|
const shutdownSignal = options.shutdownSignal ?? shutdownController.signal;
|
|
224
|
+
const shutdownIntentController = new AbortController();
|
|
198
225
|
const operatorTerminationController = new AbortController();
|
|
226
|
+
const drainedShutdownController = new AbortController();
|
|
199
227
|
const driverShutdownSignal = AbortSignal.any([
|
|
200
|
-
|
|
228
|
+
drainedShutdownController.signal,
|
|
201
229
|
operatorTerminationController.signal,
|
|
202
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();
|
|
203
277
|
let unregisterProcessSignalHandlers = () => { };
|
|
278
|
+
const removeShutdownDrainListener = () => shutdownSignal.removeEventListener('abort', onShutdownSignal);
|
|
279
|
+
unregisterProcessSignalHandlers = removeShutdownDrainListener;
|
|
204
280
|
if (ownsShutdownController &&
|
|
205
281
|
options.registerProcessSignalHandlers !== false) {
|
|
206
282
|
const onSignal = () => shutdownController.abort();
|
|
@@ -209,11 +285,13 @@ export async function runConductorSession(options) {
|
|
|
209
285
|
unregisterProcessSignalHandlers = () => {
|
|
210
286
|
process.off('SIGINT', onSignal);
|
|
211
287
|
process.off('SIGTERM', onSignal);
|
|
288
|
+
removeShutdownDrainListener();
|
|
212
289
|
};
|
|
213
290
|
}
|
|
214
291
|
const permissionPipeline = options.permissionPipeline ??
|
|
215
292
|
new PermissionPipeline({ policy: options.permissionPolicy });
|
|
216
293
|
let scheduleSidecarFlush = () => { };
|
|
294
|
+
let flushSidecar = async () => { };
|
|
217
295
|
let scheduleToolCallHistoryFlush = () => { };
|
|
218
296
|
let flushToolCallHistory = async () => { };
|
|
219
297
|
let toolCallHistoryDirty = toolCallHistoryNeedsRewrite;
|
|
@@ -535,9 +613,12 @@ export async function runConductorSession(options) {
|
|
|
535
613
|
githubMonitorCursor ??= emptyGitHubMonitorCursor();
|
|
536
614
|
return githubMonitorCursor;
|
|
537
615
|
},
|
|
538
|
-
onRegistered: (registration) => {
|
|
616
|
+
onRegistered: async (registration) => {
|
|
539
617
|
githubMonitor?.registerPullRequest?.(registration);
|
|
540
|
-
|
|
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();
|
|
541
622
|
},
|
|
542
623
|
});
|
|
543
624
|
const unregisterGitHubWatchTools = createUnregisterGitHubWatchTool({
|
|
@@ -546,9 +627,9 @@ export async function runConductorSession(options) {
|
|
|
546
627
|
githubMonitorCursor ??= emptyGitHubMonitorCursor();
|
|
547
628
|
return githubMonitorCursor;
|
|
548
629
|
},
|
|
549
|
-
onUnregistered: (prNumber) => {
|
|
630
|
+
onUnregistered: async (prNumber) => {
|
|
550
631
|
githubMonitor?.unregisterPullRequest?.(prNumber);
|
|
551
|
-
|
|
632
|
+
await flushSidecar();
|
|
552
633
|
},
|
|
553
634
|
});
|
|
554
635
|
let conductorAgent;
|
|
@@ -560,7 +641,20 @@ export async function runConductorSession(options) {
|
|
|
560
641
|
return usage.cost;
|
|
561
642
|
},
|
|
562
643
|
});
|
|
563
|
-
|
|
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
|
+
});
|
|
564
658
|
const dispatchHoldTools = createSetDispatchHoldTool({
|
|
565
659
|
state: dispatchHoldState,
|
|
566
660
|
onChanged: onDispatchHoldChanged,
|
|
@@ -632,6 +726,7 @@ export async function runConductorSession(options) {
|
|
|
632
726
|
const sendReconnect = {
|
|
633
727
|
conductorAgentFactory,
|
|
634
728
|
conductorOptions,
|
|
729
|
+
retrySuppressionSignal: shutdownIntentController.signal,
|
|
635
730
|
onAuthReconnectAttempt: ({ agentId }) => {
|
|
636
731
|
sessionLogger.emit({
|
|
637
732
|
type: 'conductor.auth.reconnect',
|
|
@@ -655,46 +750,59 @@ export async function runConductorSession(options) {
|
|
|
655
750
|
},
|
|
656
751
|
};
|
|
657
752
|
let sidecarFlushQueue = Promise.resolve();
|
|
658
|
-
let
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
version: SESSION_SIDECAR_VERSION,
|
|
672
|
-
conductorAgentId: conductorHandle.conductor.agentId,
|
|
673
|
-
conductorBackend,
|
|
674
|
-
lazyWorkers: activeLazyWorkers,
|
|
675
|
-
issueUrl: options.issueUrl,
|
|
676
|
-
repoRoot: options.repoRoot,
|
|
677
|
-
profile: structuredClone(activeProfile),
|
|
678
|
-
...(options.profilePath ? { profilePath: options.profilePath } : {}),
|
|
679
|
-
openQuestions: snapshot.openQuestions,
|
|
680
|
-
sequence: snapshot.sequence,
|
|
681
|
-
workers,
|
|
682
|
-
mcpConfigDigest,
|
|
683
|
-
...(githubMonitorCursor ? { githubMonitor: githubMonitorCursor } : {}),
|
|
684
|
-
updatedAt: Date.now(),
|
|
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 } : {}),
|
|
685
766
|
};
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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);
|
|
697
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
|
+
});
|
|
698
806
|
const reportSidecarFlushFailure = (error) => {
|
|
699
807
|
const detail = error instanceof Error ? error.message : String(error);
|
|
700
808
|
sessionLogger.emit({
|
|
@@ -702,6 +810,13 @@ export async function runConductorSession(options) {
|
|
|
702
810
|
message: `Open Question YAML / session sidecar を保存できませんでした。最新状態から再開できず、直前に保存された状態からの再開になる場合があります。 error=${detail}`,
|
|
703
811
|
});
|
|
704
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}`,
|
|
818
|
+
});
|
|
819
|
+
};
|
|
705
820
|
scheduleSidecarFlush = () => {
|
|
706
821
|
void flushSidecar().catch(reportSidecarFlushFailure);
|
|
707
822
|
};
|
|
@@ -878,14 +993,16 @@ export async function runConductorSession(options) {
|
|
|
878
993
|
sessionLogger.emit({ type: 'session.operator_exit' });
|
|
879
994
|
workerSession.runtime.cancelAllActivePrompts();
|
|
880
995
|
operatorRequestedExit = true;
|
|
881
|
-
|
|
996
|
+
shutdownIntentController.abort();
|
|
997
|
+
abortAfterOpenQuestionDrain(operatorTerminationController);
|
|
882
998
|
return false;
|
|
883
999
|
}
|
|
884
1000
|
if (isOperatorCloseCommand(message)) {
|
|
885
1001
|
sessionLogger.emit({ type: 'session.operator_close' });
|
|
886
1002
|
workerSession.runtime.cancelAllActivePrompts();
|
|
887
1003
|
operatorRequestedClose = true;
|
|
888
|
-
|
|
1004
|
+
shutdownIntentController.abort();
|
|
1005
|
+
abortAfterOpenQuestionDrain(operatorTerminationController);
|
|
889
1006
|
return false;
|
|
890
1007
|
}
|
|
891
1008
|
if (isOperatorReconnectCommand(message)) {
|
|
@@ -919,6 +1036,114 @@ export async function runConductorSession(options) {
|
|
|
919
1036
|
}
|
|
920
1037
|
}
|
|
921
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);
|
|
922
1147
|
const driverResult = await runConductorSessionDriver({
|
|
923
1148
|
issueUrl: options.issueUrl,
|
|
924
1149
|
initialPrompt: conductorBackend === 'pi'
|
|
@@ -979,6 +1204,23 @@ export async function runConductorSession(options) {
|
|
|
979
1204
|
});
|
|
980
1205
|
},
|
|
981
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
|
+
},
|
|
982
1224
|
onOpenQuestionEnqueued: (question) => {
|
|
983
1225
|
sessionLogger.emit({ type: 'open.question.enqueued', question });
|
|
984
1226
|
options.onOpenQuestionEnqueued?.(question);
|
|
@@ -1069,7 +1311,19 @@ export async function runConductorSession(options) {
|
|
|
1069
1311
|
}
|
|
1070
1312
|
}
|
|
1071
1313
|
finally {
|
|
1314
|
+
const lifecycle = operatorRequestedClose
|
|
1315
|
+
? 'closed'
|
|
1316
|
+
: operatorRequestedExit
|
|
1317
|
+
? 'exited'
|
|
1318
|
+
: shutdownSignal.aborted
|
|
1319
|
+
? 'interrupted'
|
|
1320
|
+
: 'unknown';
|
|
1321
|
+
terminalLifecycle = lifecycle;
|
|
1072
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();
|
|
1073
1327
|
const forceShutdown = operatorRequestedExit ||
|
|
1074
1328
|
operatorRequestedClose ||
|
|
1075
1329
|
shutdownSignal.aborted;
|
|
@@ -1095,6 +1349,24 @@ export async function runConductorSession(options) {
|
|
|
1095
1349
|
sessionLogger.emit({ type: 'harness.teardown.phase', phase });
|
|
1096
1350
|
};
|
|
1097
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
|
+
}
|
|
1098
1370
|
disposeOperatorInput?.();
|
|
1099
1371
|
if (permissionDeadlockMonitor) {
|
|
1100
1372
|
emitTeardownPhase('permissionDeadlockMonitor');
|
|
@@ -1212,6 +1484,14 @@ export async function runConductorSession(options) {
|
|
|
1212
1484
|
}
|
|
1213
1485
|
}
|
|
1214
1486
|
finally {
|
|
1487
|
+
try {
|
|
1488
|
+
emitTeardownPhase('flushSidecar');
|
|
1489
|
+
await sessionStateWriteQueue;
|
|
1490
|
+
await flushSidecar(lifecycle);
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
reportSidecarFlushFailure(error);
|
|
1494
|
+
}
|
|
1215
1495
|
process.off('SIGINT', onTeardownSignal);
|
|
1216
1496
|
process.off('SIGTERM', onTeardownSignal);
|
|
1217
1497
|
}
|