@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.
- package/dist/acp/testing/fake-acp-server.d.ts +2 -0
- package/dist/acp/testing/fake-acp-server.d.ts.map +1 -1
- package/dist/acp/testing/fake-acp-server.js +3 -0
- package/dist/acp/testing/fake-acp-server.js.map +1 -1
- package/dist/conductor/conductor-agent-factory.d.ts +5 -0
- package/dist/conductor/conductor-agent-factory.d.ts.map +1 -0
- package/dist/conductor/conductor-agent-factory.js +10 -0
- package/dist/conductor/conductor-agent-factory.js.map +1 -0
- package/dist/conductor/conductor-agent.d.ts +7 -0
- package/dist/conductor/conductor-agent.d.ts.map +1 -1
- 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 +19 -2
- package/dist/conductor/conductor-session-driver.js.map +1 -1
- package/dist/conductor/conductor-session.d.ts +5 -2
- package/dist/conductor/conductor-session.d.ts.map +1 -1
- package/dist/conductor/conductor-session.js +539 -74
- package/dist/conductor/conductor-session.js.map +1 -1
- package/dist/conductor/index.d.ts +3 -2
- package/dist/conductor/index.d.ts.map +1 -1
- package/dist/conductor/index.js +2 -1
- package/dist/conductor/index.js.map +1 -1
- package/dist/conductor/operator-command.d.ts +5 -0
- package/dist/conductor/operator-command.d.ts.map +1 -1
- package/dist/conductor/operator-command.js +6 -0
- package/dist/conductor/operator-command.js.map +1 -1
- package/dist/conductor/pi-conductor-agent.d.ts +2 -5
- package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
- package/dist/conductor/pi-conductor-agent.js +16 -19
- package/dist/conductor/pi-conductor-agent.js.map +1 -1
- package/dist/conductor/pi-resource-loader.d.ts +2 -0
- package/dist/conductor/pi-resource-loader.d.ts.map +1 -1
- package/dist/conductor/pi-resource-loader.js +16 -8
- package/dist/conductor/pi-resource-loader.js.map +1 -1
- package/dist/conductor/session/dispatch-hold.d.ts +1 -1
- package/dist/conductor/session/dispatch-hold.d.ts.map +1 -1
- package/dist/conductor/session/dispatch-hold.js +4 -3
- package/dist/conductor/session/dispatch-hold.js.map +1 -1
- package/dist/conductor/session/dispatch-mode.d.ts +1 -1
- package/dist/conductor/session/events/index.d.ts +2 -1
- package/dist/conductor/session/events/index.d.ts.map +1 -1
- package/dist/conductor/session/events/index.js.map +1 -1
- package/dist/conductor/session/events/session-event.d.ts +7 -1
- package/dist/conductor/session/events/session-event.d.ts.map +1 -1
- package/dist/conductor/session/events/session-log-event-groups.d.ts +1 -1
- package/dist/conductor/session/events/session-log-event-groups.d.ts.map +1 -1
- package/dist/conductor/session/events/session-log-event-groups.js +1 -0
- package/dist/conductor/session/events/session-log-event-groups.js.map +1 -1
- package/dist/conductor/session/events/shared/permission-pending.d.ts +5 -0
- package/dist/conductor/session/events/shared/permission-pending.d.ts.map +1 -1
- package/dist/conductor/session/format-session-event.d.ts.map +1 -1
- package/dist/conductor/session/format-session-event.js +32 -2
- package/dist/conductor/session/format-session-event.js.map +1 -1
- package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
- package/dist/conductor/session/select-dispatch-batch.js +21 -4
- package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
- package/dist/conductor/session/session-event.d.ts +1 -1
- package/dist/conductor/session/session-event.d.ts.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/get-tool-call-tool.d.ts +8 -0
- package/dist/dispatch/get-tool-call-tool.d.ts.map +1 -0
- package/dist/dispatch/get-tool-call-tool.js +46 -0
- package/dist/dispatch/get-tool-call-tool.js.map +1 -0
- package/dist/dispatch/index.d.ts +3 -0
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/index.js +2 -0
- package/dist/dispatch/index.js.map +1 -1
- package/dist/dispatch/report-session-state-tool.d.ts +6 -0
- package/dist/dispatch/report-session-state-tool.d.ts.map +1 -0
- package/dist/dispatch/report-session-state-tool.js +39 -0
- package/dist/dispatch/report-session-state-tool.js.map +1 -0
- package/dist/dispatch/session-usage-tool.js +1 -1
- package/dist/dispatch/session-usage-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/ask-human-tool.d.ts +0 -2
- package/dist/escalation/ask-human-tool.d.ts.map +1 -1
- package/dist/escalation/ask-human-tool.js +1 -7
- package/dist/escalation/ask-human-tool.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/github-monitor.d.ts +1 -1
- package/dist/github/github-monitor.d.ts.map +1 -1
- package/dist/github/github-monitor.js +1 -1
- package/dist/github/github-monitor.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 +24 -0
- package/dist/operator-runtime.d.ts.map +1 -0
- package/dist/operator-runtime.js +17 -0
- package/dist/operator-runtime.js.map +1 -0
- package/dist/permission/index.d.ts +2 -0
- package/dist/permission/index.d.ts.map +1 -1
- package/dist/permission/index.js +1 -0
- package/dist/permission/index.js.map +1 -1
- package/dist/permission/permission-brief.d.ts +22 -0
- package/dist/permission/permission-brief.d.ts.map +1 -0
- package/dist/permission/permission-brief.js +245 -0
- package/dist/permission/permission-brief.js.map +1 -0
- package/dist/prompt/index.d.ts +1 -0
- package/dist/prompt/index.d.ts.map +1 -1
- package/dist/prompt/index.js +1 -0
- package/dist/prompt/index.js.map +1 -1
- package/dist/prompt/modules/ensemble/conductor-base-module.d.ts.map +1 -1
- package/dist/prompt/modules/ensemble/conductor-base-module.js +11 -1
- package/dist/prompt/modules/ensemble/conductor-base-module.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/tool-call-snapshot-registry.d.ts +69 -0
- package/dist/runtime/tool-call-snapshot-registry.d.ts.map +1 -0
- package/dist/runtime/tool-call-snapshot-registry.js +275 -0
- package/dist/runtime/tool-call-snapshot-registry.js.map +1 -0
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/worker-runtime.d.ts.map +1 -1
- package/dist/runtime/worker-runtime.js +6 -2
- package/dist/runtime/worker-runtime.js.map +1 -1
- package/dist/session/index.d.ts +7 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +3 -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 +12 -0
- package/dist/session/open-questions-store.d.ts.map +1 -0
- package/dist/session/open-questions-store.js +164 -0
- package/dist/session/open-questions-store.js.map +1 -0
- 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/dist/session/tool-call-snapshot-history.d.ts +32 -0
- package/dist/session/tool-call-snapshot-history.d.ts.map +1 -0
- package/dist/session/tool-call-snapshot-history.js +203 -0
- package/dist/session/tool-call-snapshot-history.js.map +1 -0
- package/dist/usage/enrich-session-usage-cost.d.ts +4 -1
- package/dist/usage/enrich-session-usage-cost.d.ts.map +1 -1
- package/dist/usage/enrich-session-usage-cost.js +8 -2
- package/dist/usage/enrich-session-usage-cost.js.map +1 -1
- package/dist/usage/index.d.ts +1 -1
- package/dist/usage/index.d.ts.map +1 -1
- package/dist/usage/session-usage-tracker.d.ts +7 -1
- package/dist/usage/session-usage-tracker.d.ts.map +1 -1
- package/dist/usage/session-usage-tracker.js +51 -0
- package/dist/usage/session-usage-tracker.js.map +1 -1
- package/dist/usage/types.d.ts +30 -2
- package/dist/usage/types.d.ts.map +1 -1
- 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 {
|
|
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
|
|
120
|
+
const loadedSidecar = await requireSessionSidecarForResume({
|
|
94
121
|
repoRoot: options.repoRoot,
|
|
95
122
|
conductorAgentId: options.resumeAgentId,
|
|
96
123
|
});
|
|
97
|
-
assertSessionSidecarMatches(
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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.');
|