@love-moon/conductor-cli 0.2.38 → 0.2.40
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/bin/conductor-config.js +16 -0
- package/bin/conductor-fire.js +532 -155
- package/bin/conductor-serve-ai.js +145 -0
- package/bin/conductor.js +5 -1
- package/package.json +6 -6
- package/src/ai-manager-handlers.js +51 -47
- package/src/daemon.js +346 -125
- package/src/fire/resume.js +498 -107
- package/src/handoff-log-mask.js +64 -0
- package/src/runtime-backends.js +111 -18
- package/src/serve-ai/adapter.js +383 -0
- package/src/serve-ai/config.js +133 -0
- package/src/serve-ai/errors.js +28 -0
- package/src/serve-ai/image-handler.js +92 -0
- package/src/serve-ai/index.js +529 -0
package/bin/conductor-fire.js
CHANGED
|
@@ -29,7 +29,9 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
filterRuntimeSupportedAllowCliList,
|
|
31
31
|
isBuiltInRuntimeBackend,
|
|
32
|
+
isCommandOptionalBuiltInRuntimeBackend,
|
|
32
33
|
listAdvertisedBackends,
|
|
34
|
+
parseCommandParts,
|
|
33
35
|
resolveConfiguredRuntimeBackend,
|
|
34
36
|
normalizeRuntimeBackendAlias,
|
|
35
37
|
normalizeRuntimeBackendName,
|
|
@@ -69,9 +71,20 @@ const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1
|
|
|
69
71
|
"",
|
|
70
72
|
);
|
|
71
73
|
|
|
72
|
-
export function buildConductorConnectHeaders(
|
|
74
|
+
export function buildConductorConnectHeaders(
|
|
75
|
+
version = pkgJson.version,
|
|
76
|
+
options = {},
|
|
77
|
+
) {
|
|
78
|
+
const backends = Array.isArray(options.backends)
|
|
79
|
+
? options.backends.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
80
|
+
: [];
|
|
81
|
+
const capabilities = Array.isArray(options.capabilities)
|
|
82
|
+
? options.capabilities.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
83
|
+
: [];
|
|
73
84
|
return {
|
|
74
85
|
"x-conductor-version": version,
|
|
86
|
+
...(backends.length > 0 ? { "x-conductor-backends": backends.join(",") } : {}),
|
|
87
|
+
...(capabilities.length > 0 ? { "x-conductor-capabilities": capabilities.join(",") } : {}),
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
|
|
@@ -89,6 +102,13 @@ export function shouldRunReconnectRecovery({
|
|
|
89
102
|
return !runner.shouldSuppressReconnectRecovery();
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
export function shouldFireReportTaskStatus({ launchedByDaemon = false, phase } = {}) {
|
|
106
|
+
if (phase === "final") {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return !launchedByDaemon;
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
// Load allow_cli_list from config file (no defaults - must be configured)
|
|
93
113
|
function loadFireConfigYaml(configFilePath) {
|
|
94
114
|
const home = os.homedir();
|
|
@@ -148,70 +168,8 @@ export function resolveConfiguredPrePrompt({ configFilePath, backend, sessionBac
|
|
|
148
168
|
return undefined;
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
function parseCommandParts(commandLine) {
|
|
152
|
-
const input = String(commandLine || "").trim();
|
|
153
|
-
if (!input) {
|
|
154
|
-
return [];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const parts = [];
|
|
158
|
-
let current = "";
|
|
159
|
-
let quote = "";
|
|
160
|
-
let escaping = false;
|
|
161
|
-
let tokenStarted = false;
|
|
162
|
-
|
|
163
|
-
for (const char of input) {
|
|
164
|
-
if (escaping) {
|
|
165
|
-
current += char;
|
|
166
|
-
tokenStarted = true;
|
|
167
|
-
escaping = false;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (char === "\\") {
|
|
172
|
-
escaping = true;
|
|
173
|
-
tokenStarted = true;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (quote) {
|
|
178
|
-
if (char === quote) {
|
|
179
|
-
quote = "";
|
|
180
|
-
} else {
|
|
181
|
-
current += char;
|
|
182
|
-
}
|
|
183
|
-
tokenStarted = true;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (char === "'" || char === "\"") {
|
|
188
|
-
quote = char;
|
|
189
|
-
tokenStarted = true;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (/\s/.test(char)) {
|
|
194
|
-
if (tokenStarted) {
|
|
195
|
-
parts.push(current);
|
|
196
|
-
current = "";
|
|
197
|
-
tokenStarted = false;
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
current += char;
|
|
203
|
-
tokenStarted = true;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (tokenStarted) {
|
|
207
|
-
parts.push(current);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return parts;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
171
|
function extractModelOptionFromCommandLine(commandLine) {
|
|
214
|
-
const parts = parseCommandParts(commandLine);
|
|
172
|
+
const { parts } = parseCommandParts(commandLine);
|
|
215
173
|
for (let index = 0; index < parts.length; index += 1) {
|
|
216
174
|
const token = String(parts[index] || "").trim();
|
|
217
175
|
if (!token) {
|
|
@@ -577,6 +535,39 @@ export class FireWatchdog {
|
|
|
577
535
|
}
|
|
578
536
|
}
|
|
579
537
|
|
|
538
|
+
export function createPendingRemoteInterruptQueue() {
|
|
539
|
+
const pending = [];
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
enqueue(event) {
|
|
543
|
+
return new Promise((resolve) => {
|
|
544
|
+
pending.push({ event, resolve });
|
|
545
|
+
});
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
async flushWith(dispatch) {
|
|
549
|
+
while (pending.length > 0) {
|
|
550
|
+
const next = pending.shift();
|
|
551
|
+
if (!next) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
next.resolve(await dispatch(next.event));
|
|
556
|
+
} catch {
|
|
557
|
+
next.resolve(false);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
rejectAll() {
|
|
563
|
+
while (pending.length > 0) {
|
|
564
|
+
const next = pending.shift();
|
|
565
|
+
next?.resolve(false);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
580
571
|
async function main() {
|
|
581
572
|
syncPwdEnvWithProcessCwdForDaemonLaunch();
|
|
582
573
|
const cliArgs = await parseCliArgs();
|
|
@@ -624,7 +615,8 @@ async function main() {
|
|
|
624
615
|
let resumeContext = null;
|
|
625
616
|
if (cliArgs.resumeSessionId) {
|
|
626
617
|
const bootstrap = await bootstrapResumeContextForFire({
|
|
627
|
-
backend: cliArgs.
|
|
618
|
+
backend: cliArgs.backend,
|
|
619
|
+
sessionBackend: cliArgs.sessionBackend,
|
|
628
620
|
configFile: cliArgs.configFile,
|
|
629
621
|
resumeSessionId: cliArgs.resumeSessionId,
|
|
630
622
|
});
|
|
@@ -637,6 +629,8 @@ async function main() {
|
|
|
637
629
|
let reconnectRunner = null;
|
|
638
630
|
let reconnectTaskId = null;
|
|
639
631
|
let pendingRemoteStopEvent = null;
|
|
632
|
+
const pendingRemoteInterruptQueue = createPendingRemoteInterruptQueue();
|
|
633
|
+
const completedRefreshSessionRequests = new Map();
|
|
640
634
|
let conductor = null;
|
|
641
635
|
let reconnectResumeInFlight = false;
|
|
642
636
|
let fireShuttingDown = false;
|
|
@@ -676,7 +670,7 @@ async function main() {
|
|
|
676
670
|
source: "conductor-fire",
|
|
677
671
|
metadata: { reconnect: true },
|
|
678
672
|
});
|
|
679
|
-
if (
|
|
673
|
+
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "reconnect_running" })) {
|
|
680
674
|
await conductor.sendTaskStatus(reconnectTaskId, {
|
|
681
675
|
status: "RUNNING",
|
|
682
676
|
summary: "conductor fire reconnected",
|
|
@@ -706,6 +700,56 @@ async function main() {
|
|
|
706
700
|
pendingRemoteStopEvent = event;
|
|
707
701
|
};
|
|
708
702
|
|
|
703
|
+
const handleInterruptTurnCommand = async (event) => {
|
|
704
|
+
fireWatchdog.onInbound();
|
|
705
|
+
if (!event || typeof event !== "object") {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const taskId = typeof event.taskId === "string" ? event.taskId : "";
|
|
709
|
+
if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
if (reconnectRunner && typeof reconnectRunner.requestInterruptFromRemote === "function") {
|
|
713
|
+
return await reconnectRunner.requestInterruptFromRemote(event);
|
|
714
|
+
}
|
|
715
|
+
return await pendingRemoteInterruptQueue.enqueue(event);
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const rememberCompletedRefreshSessionRequest = (requestId, accepted) => {
|
|
719
|
+
if (!requestId) {
|
|
720
|
+
return accepted;
|
|
721
|
+
}
|
|
722
|
+
completedRefreshSessionRequests.set(requestId, accepted);
|
|
723
|
+
while (completedRefreshSessionRequests.size > 20) {
|
|
724
|
+
const oldest = completedRefreshSessionRequests.keys().next();
|
|
725
|
+
if (oldest.done) {
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
completedRefreshSessionRequests.delete(oldest.value);
|
|
729
|
+
}
|
|
730
|
+
return accepted;
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const handleRefreshSessionCommand = async (event) => {
|
|
734
|
+
fireWatchdog.onInbound();
|
|
735
|
+
if (!event || typeof event !== "object") {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
const taskId = typeof event.taskId === "string" ? event.taskId : "";
|
|
739
|
+
const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
|
|
740
|
+
if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
if (requestId && completedRefreshSessionRequests.has(requestId)) {
|
|
744
|
+
return completedRefreshSessionRequests.get(requestId) === true;
|
|
745
|
+
}
|
|
746
|
+
if (reconnectRunner && typeof reconnectRunner.requestRefreshSessionFromRemote === "function") {
|
|
747
|
+
const accepted = await reconnectRunner.requestRefreshSessionFromRemote(event);
|
|
748
|
+
return rememberCompletedRefreshSessionRequest(requestId, accepted !== false);
|
|
749
|
+
}
|
|
750
|
+
return rememberCompletedRefreshSessionRequest(requestId, false);
|
|
751
|
+
};
|
|
752
|
+
|
|
709
753
|
if (cliArgs.configFile) {
|
|
710
754
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
711
755
|
}
|
|
@@ -740,7 +784,10 @@ async function main() {
|
|
|
740
784
|
conductor = await ConductorClient.connect({
|
|
741
785
|
projectPath: runtimeProjectPath,
|
|
742
786
|
extraEnv: env,
|
|
743
|
-
extraHeaders: buildConductorConnectHeaders(
|
|
787
|
+
extraHeaders: buildConductorConnectHeaders(pkgJson.version, {
|
|
788
|
+
backends: [cliArgs.backend],
|
|
789
|
+
capabilities: ["refresh_session_inplace"],
|
|
790
|
+
}),
|
|
744
791
|
configFile: cliArgs.configFile,
|
|
745
792
|
onConnected: (event) => {
|
|
746
793
|
fireWatchdog.onConnected(event);
|
|
@@ -756,6 +803,8 @@ async function main() {
|
|
|
756
803
|
fireWatchdog.onPong(event);
|
|
757
804
|
},
|
|
758
805
|
onStopTask: handleStopTaskCommand,
|
|
806
|
+
onInterruptTurn: handleInterruptTurnCommand,
|
|
807
|
+
onRefreshSession: handleRefreshSessionCommand,
|
|
759
808
|
});
|
|
760
809
|
|
|
761
810
|
const taskContext = await ensureTaskContext(conductor, {
|
|
@@ -795,7 +844,10 @@ async function main() {
|
|
|
795
844
|
}
|
|
796
845
|
}
|
|
797
846
|
|
|
798
|
-
|
|
847
|
+
let nextResumeSessionId = cliArgs.resumeSessionId;
|
|
848
|
+
let nextInitialPrompt =
|
|
849
|
+
taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "";
|
|
850
|
+
let pendingRefreshSessionRequest = null;
|
|
799
851
|
|
|
800
852
|
const sessionCommandLine = resolveAiSessionCommandLine(
|
|
801
853
|
cliArgs.backend,
|
|
@@ -810,19 +862,6 @@ async function main() {
|
|
|
810
862
|
env: process.env,
|
|
811
863
|
});
|
|
812
864
|
|
|
813
|
-
backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
|
|
814
|
-
initialImages: cliArgs.initialImages,
|
|
815
|
-
cwd: runtimeProjectPath,
|
|
816
|
-
resumeSessionId: resolvedResumeSessionId,
|
|
817
|
-
configFile: cliArgs.configFile,
|
|
818
|
-
...(cliArgs.sessionOptions || {}),
|
|
819
|
-
...(sessionCommandLine ? { commandLine: sessionCommandLine } : {}),
|
|
820
|
-
logger: { log },
|
|
821
|
-
...(resolvedPrePrompt ? { prePrompt: resolvedPrePrompt } : {}),
|
|
822
|
-
sessionStoreKey: taskContext.taskId ? `task-${taskContext.taskId}` : undefined,
|
|
823
|
-
resumePersistedSession: Boolean(!resolvedResumeSessionId && taskContext.taskId),
|
|
824
|
-
});
|
|
825
|
-
|
|
826
865
|
log(`Using backend: ${cliArgs.backend}`);
|
|
827
866
|
|
|
828
867
|
try {
|
|
@@ -835,25 +874,6 @@ async function main() {
|
|
|
835
874
|
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
836
875
|
}
|
|
837
876
|
|
|
838
|
-
const runner = new BridgeRunner({
|
|
839
|
-
backendSession,
|
|
840
|
-
conductor,
|
|
841
|
-
taskId: taskContext.taskId,
|
|
842
|
-
pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
|
|
843
|
-
initialPrompt: taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "",
|
|
844
|
-
initialPromptDelivery: taskContext.initialPromptDelivery || "none",
|
|
845
|
-
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
846
|
-
cliArgs: cliArgs.rawBackendArgs,
|
|
847
|
-
backendName: cliArgs.backend,
|
|
848
|
-
resumeSessionId: resolvedResumeSessionId,
|
|
849
|
-
daemonName: resolvedDaemonName,
|
|
850
|
-
});
|
|
851
|
-
reconnectRunner = runner;
|
|
852
|
-
if (pendingRemoteStopEvent) {
|
|
853
|
-
await runner.requestStopFromRemote(pendingRemoteStopEvent);
|
|
854
|
-
pendingRemoteStopEvent = null;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
877
|
const signals = new AbortController();
|
|
858
878
|
let shutdownSignal = null;
|
|
859
879
|
let backendShutdownRequested = false;
|
|
@@ -887,7 +907,7 @@ async function main() {
|
|
|
887
907
|
process.on("SIGINT", onSigint);
|
|
888
908
|
process.on("SIGTERM", onSigterm);
|
|
889
909
|
|
|
890
|
-
if (
|
|
910
|
+
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "running" })) {
|
|
891
911
|
try {
|
|
892
912
|
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
893
913
|
status: "RUNNING",
|
|
@@ -897,68 +917,148 @@ async function main() {
|
|
|
897
917
|
}
|
|
898
918
|
}
|
|
899
919
|
|
|
900
|
-
let runnerError = null;
|
|
901
920
|
try {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
921
|
+
while (true) {
|
|
922
|
+
const currentRefreshSessionRequest = pendingRefreshSessionRequest;
|
|
923
|
+
let runnerError = null;
|
|
924
|
+
|
|
925
|
+
backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
|
|
926
|
+
initialImages: cliArgs.initialImages,
|
|
927
|
+
cwd: runtimeProjectPath,
|
|
928
|
+
resumeSessionId: nextResumeSessionId,
|
|
929
|
+
configFile: cliArgs.configFile,
|
|
930
|
+
...(cliArgs.sessionOptions || {}),
|
|
931
|
+
...(sessionCommandLine ? { commandLine: sessionCommandLine } : {}),
|
|
932
|
+
logger: { log },
|
|
933
|
+
...(resolvedPrePrompt ? { prePrompt: resolvedPrePrompt } : {}),
|
|
934
|
+
sessionStoreKey: taskContext.taskId ? `task-${taskContext.taskId}` : undefined,
|
|
935
|
+
resumePersistedSession: Boolean(!nextResumeSessionId && taskContext.taskId),
|
|
905
936
|
});
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
937
|
+
|
|
938
|
+
const runner = new BridgeRunner({
|
|
939
|
+
backendSession,
|
|
940
|
+
conductor,
|
|
941
|
+
taskId: taskContext.taskId,
|
|
942
|
+
pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
|
|
943
|
+
initialPrompt: nextInitialPrompt,
|
|
944
|
+
initialPromptDelivery: taskContext.initialPromptDelivery || "none",
|
|
945
|
+
includeInitialImages: Boolean(nextInitialPrompt && cliArgs.initialImages.length),
|
|
946
|
+
cliArgs: cliArgs.rawBackendArgs,
|
|
947
|
+
backendName: cliArgs.backend,
|
|
948
|
+
resumeSessionId: nextResumeSessionId,
|
|
949
|
+
daemonName: resolvedDaemonName,
|
|
950
|
+
});
|
|
951
|
+
reconnectRunner = runner;
|
|
952
|
+
if (pendingRemoteStopEvent) {
|
|
953
|
+
await runner.requestStopFromRemote(pendingRemoteStopEvent);
|
|
954
|
+
pendingRemoteStopEvent = null;
|
|
955
|
+
}
|
|
956
|
+
await pendingRemoteInterruptQueue.flushWith((event) => runner.requestInterruptFromRemote(event));
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
if (currentRefreshSessionRequest) {
|
|
960
|
+
await runner.announceBackendSession();
|
|
961
|
+
currentRefreshSessionRequest.resolve(true);
|
|
962
|
+
pendingRefreshSessionRequest = null;
|
|
963
|
+
} else if (
|
|
964
|
+
!nextResumeSessionId &&
|
|
965
|
+
String(cliArgs.sessionBackend || cliArgs.backend).trim().toLowerCase() === "codex"
|
|
966
|
+
) {
|
|
967
|
+
await withFreshSessionBootstrapLock(
|
|
968
|
+
cliArgs.sessionBackend || cliArgs.backend,
|
|
969
|
+
runtimeProjectPath,
|
|
970
|
+
async () => {
|
|
971
|
+
await runner.announceBackendSession();
|
|
972
|
+
},
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
await runner.start(signals.signal);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
runnerError = error;
|
|
978
|
+
if (currentRefreshSessionRequest) {
|
|
979
|
+
currentRefreshSessionRequest.resolve(false);
|
|
980
|
+
pendingRefreshSessionRequest = null;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const refreshSessionRequest =
|
|
985
|
+
typeof runner.getRefreshSessionRequest === "function"
|
|
986
|
+
? runner.getRefreshSessionRequest()
|
|
987
|
+
: null;
|
|
988
|
+
if (!runnerError && !shutdownSignal && refreshSessionRequest) {
|
|
989
|
+
const refreshedSessionId =
|
|
990
|
+
refreshSessionRequest.sessionId ||
|
|
991
|
+
(typeof runner.boundSessionId === "string" ? runner.boundSessionId.trim() : "") ||
|
|
992
|
+
nextResumeSessionId;
|
|
993
|
+
if (refreshedSessionId) {
|
|
994
|
+
nextResumeSessionId = refreshedSessionId;
|
|
995
|
+
nextInitialPrompt = "";
|
|
996
|
+
pendingRefreshSessionRequest = refreshSessionRequest;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
runnerError = new Error("refresh_session requires a bound session id");
|
|
1000
|
+
}
|
|
1001
|
+
if (refreshSessionRequest && refreshSessionRequest !== pendingRefreshSessionRequest) {
|
|
1002
|
+
refreshSessionRequest.resolve(false);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "final" })) {
|
|
1006
|
+
const remoteStopReason = typeof runner.getRemoteStopReason === "function" ? runner.getRemoteStopReason() : null;
|
|
1007
|
+
const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
|
|
1008
|
+
// When the task was deleted by the user, the DB record is already gone —
|
|
1009
|
+
// attempting to send a final status update would fail with 500 and the
|
|
1010
|
+
// SDK durable outbox would retry forever, preventing the process from
|
|
1011
|
+
// exiting.
|
|
1012
|
+
const taskDeletedByUser = remoteStopReason === "deleted_by_user";
|
|
1013
|
+
const finalStatus = shutdownSignal
|
|
928
1014
|
? {
|
|
929
1015
|
status: "KILLED",
|
|
930
|
-
summary: `
|
|
1016
|
+
summary: `terminated by ${shutdownSignal}`,
|
|
931
1017
|
}
|
|
932
|
-
:
|
|
1018
|
+
: runnerError
|
|
933
1019
|
? {
|
|
934
1020
|
status: "KILLED",
|
|
935
|
-
summary:
|
|
1021
|
+
summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
|
|
936
1022
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1023
|
+
: remoteStopSummary
|
|
1024
|
+
? {
|
|
1025
|
+
status: "KILLED",
|
|
1026
|
+
summary: remoteStopSummary,
|
|
1027
|
+
}
|
|
1028
|
+
: {
|
|
1029
|
+
status: "COMPLETED",
|
|
1030
|
+
summary: "conductor fire exited",
|
|
1031
|
+
};
|
|
1032
|
+
if (!taskDeletedByUser) {
|
|
1033
|
+
try {
|
|
1034
|
+
const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
1035
|
+
if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
|
|
1036
|
+
await conductor.flushPendingUpstreamEvents({
|
|
1037
|
+
timeoutMs: 5_000,
|
|
1038
|
+
retryIntervalMs: 250,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
log(`Skipping final status report: task was deleted by user`);
|
|
1046
|
+
// Also clear any pending durable outbox retries (e.g. task_stop_ack)
|
|
1047
|
+
// that would keep failing against the deleted task.
|
|
1048
|
+
if (typeof conductor.clearDurableOutboxTimer === "function") {
|
|
1049
|
+
conductor.clearDurableOutboxTimer();
|
|
949
1050
|
}
|
|
950
|
-
} catch (error) {
|
|
951
|
-
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
952
|
-
}
|
|
953
|
-
} else {
|
|
954
|
-
log(`Skipping final status report: task was deleted by user`);
|
|
955
|
-
// Also clear any pending durable outbox retries (e.g. task_stop_ack)
|
|
956
|
-
// that would keep failing against the deleted task.
|
|
957
|
-
if (typeof conductor.clearDurableOutboxTimer === "function") {
|
|
958
|
-
conductor.clearDurableOutboxTimer();
|
|
959
1051
|
}
|
|
960
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
if (runnerError) {
|
|
1055
|
+
throw runnerError;
|
|
1056
|
+
}
|
|
1057
|
+
break;
|
|
961
1058
|
}
|
|
1059
|
+
} finally {
|
|
1060
|
+
process.off("SIGINT", onSigint);
|
|
1061
|
+
process.off("SIGTERM", onSigterm);
|
|
962
1062
|
if (shutdownSignal === "SIGINT") {
|
|
963
1063
|
process.exitCode = 130;
|
|
964
1064
|
} else if (shutdownSignal === "SIGTERM") {
|
|
@@ -966,6 +1066,7 @@ async function main() {
|
|
|
966
1066
|
}
|
|
967
1067
|
}
|
|
968
1068
|
} finally {
|
|
1069
|
+
pendingRemoteInterruptQueue.rejectAll();
|
|
969
1070
|
fireShuttingDown = true;
|
|
970
1071
|
fireWatchdog.stop();
|
|
971
1072
|
if (backendSession && typeof backendSession.close === "function") {
|
|
@@ -1183,6 +1284,12 @@ Environment:
|
|
|
1183
1284
|
const requestedBackend = conductorArgs.backend
|
|
1184
1285
|
? normalizeRuntimeBackendName(conductorArgs.backend)
|
|
1185
1286
|
: supportedBackends[0] || externalBackends[0];
|
|
1287
|
+
if ((conductorArgs.listBackends || listBackendsWithoutSeparator) && discoveryError) {
|
|
1288
|
+
throw discoveryError;
|
|
1289
|
+
}
|
|
1290
|
+
if (!conductorArgs.backend && discoveryError && isCommandOptionalBuiltInRuntimeBackend(requestedBackend)) {
|
|
1291
|
+
throw discoveryError;
|
|
1292
|
+
}
|
|
1186
1293
|
const configuredBackend = await resolveConfiguredRuntimeBackend(requestedBackend, allowCliList, {
|
|
1187
1294
|
configFilePath: configFileFromArgs,
|
|
1188
1295
|
});
|
|
@@ -1207,7 +1314,9 @@ Environment:
|
|
|
1207
1314
|
const isAllowedExternalBackend =
|
|
1208
1315
|
!isBuiltInRuntimeBackend(sessionBackend) &&
|
|
1209
1316
|
advertisedExternalBackends.has(sessionBackend);
|
|
1210
|
-
|
|
1317
|
+
const isCommandOptionalBuiltInBackend =
|
|
1318
|
+
isCommandOptionalBuiltInRuntimeBackend(sessionBackend || backend);
|
|
1319
|
+
if (backend && shouldRequireBackend && !hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltInBackend) {
|
|
1211
1320
|
throw new Error(
|
|
1212
1321
|
`Unsupported backend "${backend}". Supported backends: ${[...runtimeSupportedBackends].join(", ") || "none configured"}.`,
|
|
1213
1322
|
);
|
|
@@ -1559,6 +1668,7 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
|
1559
1668
|
|
|
1560
1669
|
export async function bootstrapResumeContextForFire({
|
|
1561
1670
|
backend,
|
|
1671
|
+
sessionBackend,
|
|
1562
1672
|
configFile,
|
|
1563
1673
|
resumeSessionId,
|
|
1564
1674
|
env = process.env,
|
|
@@ -1582,7 +1692,8 @@ export async function bootstrapResumeContextForFire({
|
|
|
1582
1692
|
return { resumeContext, runtimeProjectPath };
|
|
1583
1693
|
}
|
|
1584
1694
|
|
|
1585
|
-
|
|
1695
|
+
const resumeLookupBackend = backend || sessionBackend;
|
|
1696
|
+
resumeContext = await resolveResumeContextFn(resumeLookupBackend, resumeSessionId, {
|
|
1586
1697
|
configFilePath: configFile,
|
|
1587
1698
|
});
|
|
1588
1699
|
const sessionLocation = resumeContext.sessionPath ? ` at ${resumeContext.sessionPath}` : "";
|
|
@@ -1731,6 +1842,10 @@ export class BridgeRunner {
|
|
|
1731
1842
|
os.hostname();
|
|
1732
1843
|
this.needsReconnectRecovery = false;
|
|
1733
1844
|
this.remoteStopInfo = null;
|
|
1845
|
+
this.refreshSessionRequest = null;
|
|
1846
|
+
this.remoteInterruptsByReplyTo = new Map();
|
|
1847
|
+
this.pendingInterruptRetryTimers = new Map();
|
|
1848
|
+
this.activeTurnReplyTo = "";
|
|
1734
1849
|
this.sessionAnnouncementSent = false;
|
|
1735
1850
|
this.boundSessionId = "";
|
|
1736
1851
|
this.errorLoop = null;
|
|
@@ -1947,7 +2062,11 @@ export class BridgeRunner {
|
|
|
1947
2062
|
}
|
|
1948
2063
|
|
|
1949
2064
|
shouldSuppressReconnectRecovery() {
|
|
1950
|
-
return this.stopped || Boolean(this.remoteStopInfo);
|
|
2065
|
+
return this.stopped || Boolean(this.remoteStopInfo) || Boolean(this.refreshSessionRequest);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
getRefreshSessionRequest() {
|
|
2069
|
+
return this.refreshSessionRequest;
|
|
1951
2070
|
}
|
|
1952
2071
|
|
|
1953
2072
|
getRemoteStopReason() {
|
|
@@ -1990,6 +2109,248 @@ export class BridgeRunner {
|
|
|
1990
2109
|
}
|
|
1991
2110
|
}
|
|
1992
2111
|
|
|
2112
|
+
async requestRefreshSessionFromRemote(event = {}) {
|
|
2113
|
+
const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
|
|
2114
|
+
if (taskId && taskId !== this.taskId) {
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
|
|
2118
|
+
const sessionId = typeof event.sessionId === "string" ? event.sessionId.trim() : "";
|
|
2119
|
+
const sessionFilePath =
|
|
2120
|
+
typeof event.sessionFilePath === "string" && event.sessionFilePath.trim()
|
|
2121
|
+
? event.sessionFilePath.trim()
|
|
2122
|
+
: "";
|
|
2123
|
+
if (!sessionId) {
|
|
2124
|
+
return false;
|
|
2125
|
+
}
|
|
2126
|
+
if (this.refreshSessionRequest) {
|
|
2127
|
+
if (
|
|
2128
|
+
requestId &&
|
|
2129
|
+
this.refreshSessionRequest.requestId &&
|
|
2130
|
+
this.refreshSessionRequest.requestId === requestId
|
|
2131
|
+
) {
|
|
2132
|
+
return await this.refreshSessionRequest.promise;
|
|
2133
|
+
}
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
let resolveRefresh;
|
|
2138
|
+
const promise = new Promise((resolve) => {
|
|
2139
|
+
resolveRefresh = resolve;
|
|
2140
|
+
});
|
|
2141
|
+
this.refreshSessionRequest = {
|
|
2142
|
+
requestId: requestId || null,
|
|
2143
|
+
sessionId,
|
|
2144
|
+
sessionFilePath: sessionFilePath || null,
|
|
2145
|
+
promise,
|
|
2146
|
+
resolve: resolveRefresh,
|
|
2147
|
+
};
|
|
2148
|
+
log(`Received refresh_session for ${this.taskId}; rebuilding backend session in-process`);
|
|
2149
|
+
this.stopped = true;
|
|
2150
|
+
if (typeof this.backendSession?.close === "function") {
|
|
2151
|
+
try {
|
|
2152
|
+
await this.backendSession.close();
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
log(`Failed to close backend session for refresh ${this.taskId}: ${error?.message || error}`);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
return await promise;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
normalizeReplyTarget(replyTo) {
|
|
2161
|
+
return typeof replyTo === "string" ? replyTo.trim() : "";
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
isTurnInterruptedError(error) {
|
|
2165
|
+
const reason = typeof error?.reason === "string" ? error.reason.trim().toLowerCase() : "";
|
|
2166
|
+
if (reason === "turn_interrupted" || reason === "turn_cancelled") {
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
const turnStatus = typeof error?.turnStatus === "string" ? error.turnStatus.trim().toLowerCase() : "";
|
|
2170
|
+
if (
|
|
2171
|
+
turnStatus === "interrupted" ||
|
|
2172
|
+
turnStatus === "cancelled" ||
|
|
2173
|
+
turnStatus === "canceled" ||
|
|
2174
|
+
turnStatus === "aborted"
|
|
2175
|
+
) {
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
const name = typeof error?.name === "string" ? error.name.trim().toLowerCase() : "";
|
|
2179
|
+
if (name === "aborterror") {
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
const message = String(error?.message || error || "").toLowerCase();
|
|
2183
|
+
return (
|
|
2184
|
+
message.includes(" interrupted") ||
|
|
2185
|
+
message.includes("interrupt ") ||
|
|
2186
|
+
message.includes("turn interrupted") ||
|
|
2187
|
+
message.includes("cancelled") ||
|
|
2188
|
+
message.includes("canceled") ||
|
|
2189
|
+
message.includes("aborted")
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
async requestInterruptFromRemote(event = {}) {
|
|
2194
|
+
const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
|
|
2195
|
+
if (taskId && taskId !== this.taskId) {
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
|
|
2199
|
+
const reason = typeof event.reason === "string" ? event.reason.trim() : "";
|
|
2200
|
+
const targetReplyTo = this.normalizeReplyTarget(event.targetReplyTo);
|
|
2201
|
+
if (!targetReplyTo) {
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
if (this.processedMessageIds.has(targetReplyTo)) {
|
|
2205
|
+
this.copilotLog(`ignore late interrupt_turn for processed replyTo=${targetReplyTo}`);
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
const existing = this.remoteInterruptsByReplyTo.get(targetReplyTo) || {};
|
|
2210
|
+
const interruptInfo = {
|
|
2211
|
+
requestId: requestId || existing.requestId || null,
|
|
2212
|
+
reason: reason || existing.reason || "user_interrupt",
|
|
2213
|
+
issued: Boolean(existing.issued),
|
|
2214
|
+
};
|
|
2215
|
+
this.remoteInterruptsByReplyTo.set(targetReplyTo, interruptInfo);
|
|
2216
|
+
log(
|
|
2217
|
+
`Received interrupt_turn for ${this.taskId} replyTo=${targetReplyTo}${
|
|
2218
|
+
interruptInfo.reason ? ` (${interruptInfo.reason})` : ""
|
|
2219
|
+
}`,
|
|
2220
|
+
);
|
|
2221
|
+
return await this.issueInterruptForReplyTarget(targetReplyTo);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
async issueInterruptForReplyTarget(replyTo) {
|
|
2225
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2226
|
+
if (!normalizedReplyTo) {
|
|
2227
|
+
return false;
|
|
2228
|
+
}
|
|
2229
|
+
const interruptInfo = this.remoteInterruptsByReplyTo.get(normalizedReplyTo);
|
|
2230
|
+
if (!interruptInfo) {
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
if (interruptInfo.issued) {
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
const supportsTurnInterrupt = typeof this.backendSession?.interruptCurrentTurn === "function";
|
|
2237
|
+
const isActiveTarget = this.runningTurn && normalizedReplyTo === this.activeTurnReplyTo;
|
|
2238
|
+
const isInFlightTarget = this.inFlightMessageIds.has(normalizedReplyTo);
|
|
2239
|
+
|
|
2240
|
+
if (!isActiveTarget && isInFlightTarget) {
|
|
2241
|
+
this.copilotLog(`interrupt arrived after replyTo=${normalizedReplyTo} stopped being interruptible`);
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (!isActiveTarget) {
|
|
2246
|
+
if (!supportsTurnInterrupt) {
|
|
2247
|
+
log(`Backend session for ${this.taskId} does not support turn interruption`);
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
2250
|
+
this.copilotLog(`queued interrupt request for future replyTo=${normalizedReplyTo}`);
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
if (!supportsTurnInterrupt) {
|
|
2255
|
+
log(`Backend session for ${this.taskId} does not support turn interruption`);
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
const interrupted = await this.backendSession.interruptCurrentTurn();
|
|
2260
|
+
if (interrupted === false) {
|
|
2261
|
+
interruptInfo.issued = false;
|
|
2262
|
+
this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
|
|
2263
|
+
if (
|
|
2264
|
+
this.runningTurn &&
|
|
2265
|
+
this.activeTurnReplyTo === normalizedReplyTo &&
|
|
2266
|
+
this.inFlightMessageIds.has(normalizedReplyTo)
|
|
2267
|
+
) {
|
|
2268
|
+
this.copilotLog(`backend interrupt not ready replyTo=${normalizedReplyTo}; retrying`);
|
|
2269
|
+
this.scheduleInterruptRetryForReplyTarget(normalizedReplyTo);
|
|
2270
|
+
return true;
|
|
2271
|
+
}
|
|
2272
|
+
return false;
|
|
2273
|
+
}
|
|
2274
|
+
interruptInfo.issued = true;
|
|
2275
|
+
this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
|
|
2276
|
+
this.copilotLog(`requested backend interrupt replyTo=${normalizedReplyTo}`);
|
|
2277
|
+
return true;
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
interruptInfo.issued = false;
|
|
2280
|
+
this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
|
|
2281
|
+
log(`Failed to interrupt replyTo=${normalizedReplyTo} for ${this.taskId}: ${error?.message || error}`);
|
|
2282
|
+
return false;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
scheduleInterruptRetryForReplyTarget(replyTo) {
|
|
2287
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2288
|
+
if (!normalizedReplyTo || this.pendingInterruptRetryTimers.has(normalizedReplyTo)) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const timer = setTimeout(() => {
|
|
2293
|
+
this.pendingInterruptRetryTimers.delete(normalizedReplyTo);
|
|
2294
|
+
const interruptInfo = this.remoteInterruptsByReplyTo.get(normalizedReplyTo);
|
|
2295
|
+
if (
|
|
2296
|
+
!interruptInfo ||
|
|
2297
|
+
interruptInfo.issued ||
|
|
2298
|
+
this.processedMessageIds.has(normalizedReplyTo) ||
|
|
2299
|
+
!this.runningTurn ||
|
|
2300
|
+
this.activeTurnReplyTo !== normalizedReplyTo ||
|
|
2301
|
+
!this.inFlightMessageIds.has(normalizedReplyTo)
|
|
2302
|
+
) {
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
void this.issueInterruptForReplyTarget(normalizedReplyTo);
|
|
2306
|
+
}, 50);
|
|
2307
|
+
if (typeof timer.unref === "function") {
|
|
2308
|
+
timer.unref();
|
|
2309
|
+
}
|
|
2310
|
+
this.pendingInterruptRetryTimers.set(normalizedReplyTo, timer);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
clearInterruptRetryForReplyTarget(replyTo) {
|
|
2314
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2315
|
+
const timer = this.pendingInterruptRetryTimers.get(normalizedReplyTo);
|
|
2316
|
+
if (!timer) {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
clearTimeout(timer);
|
|
2320
|
+
this.pendingInterruptRetryTimers.delete(normalizedReplyTo);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
async handleInterruptedTurn(replyTo, interruptInfo) {
|
|
2324
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2325
|
+
this.clearInterruptRetryForReplyTarget(normalizedReplyTo);
|
|
2326
|
+
this.copilotLog(`turn interrupted replyTo=${normalizedReplyTo || "latest"}`);
|
|
2327
|
+
await this.reportRuntimeStatus(
|
|
2328
|
+
{
|
|
2329
|
+
phase: "interrupted",
|
|
2330
|
+
reply_in_progress: false,
|
|
2331
|
+
status_done_line: "Conversation interrupted",
|
|
2332
|
+
},
|
|
2333
|
+
normalizedReplyTo,
|
|
2334
|
+
);
|
|
2335
|
+
try {
|
|
2336
|
+
await this.conductor.sendMessage(this.taskId, "Conversation interrupted", {
|
|
2337
|
+
backend: this.backendName,
|
|
2338
|
+
reply_to: normalizedReplyTo || undefined,
|
|
2339
|
+
interrupted: true,
|
|
2340
|
+
interruption_request_id: interruptInfo?.requestId || undefined,
|
|
2341
|
+
reason: interruptInfo?.reason || undefined,
|
|
2342
|
+
cli_args: this.cliArgs,
|
|
2343
|
+
});
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
log(`Failed to send interrupt confirmation for ${this.taskId}: ${error?.message || error}`);
|
|
2346
|
+
}
|
|
2347
|
+
if (normalizedReplyTo) {
|
|
2348
|
+
this.processedMessageIds.add(normalizedReplyTo);
|
|
2349
|
+
this.remoteInterruptsByReplyTo.delete(normalizedReplyTo);
|
|
2350
|
+
}
|
|
2351
|
+
this.resetErrorLoop();
|
|
2352
|
+
}
|
|
2353
|
+
|
|
1993
2354
|
async recoverAfterReconnect() {
|
|
1994
2355
|
if (!this.needsReconnectRecovery) {
|
|
1995
2356
|
return;
|
|
@@ -2502,6 +2863,7 @@ export class BridgeRunner {
|
|
|
2502
2863
|
}
|
|
2503
2864
|
this.lastRuntimeStatusSignature = null;
|
|
2504
2865
|
this.runningTurn = true;
|
|
2866
|
+
this.activeTurnReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2505
2867
|
const turnStartedAt = Date.now();
|
|
2506
2868
|
let turnWatchdog = null;
|
|
2507
2869
|
if (this.isCopilotBackend) {
|
|
@@ -2540,12 +2902,15 @@ export class BridgeRunner {
|
|
|
2540
2902
|
);
|
|
2541
2903
|
}
|
|
2542
2904
|
|
|
2543
|
-
const
|
|
2905
|
+
const turnPromise = this.backendSession.runTurn(content, {
|
|
2544
2906
|
useInitialImages,
|
|
2545
2907
|
onProgress: (payload) => {
|
|
2546
2908
|
void this.reportRuntimeStatus(payload, replyTo);
|
|
2547
2909
|
},
|
|
2548
2910
|
});
|
|
2911
|
+
await this.issueInterruptForReplyTarget(replyTo);
|
|
2912
|
+
const result = await turnPromise;
|
|
2913
|
+
this.activeTurnReplyTo = "";
|
|
2549
2914
|
this.copilotLog(
|
|
2550
2915
|
`runTurn completed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} answerLen=${String(
|
|
2551
2916
|
result.text || "",
|
|
@@ -2582,6 +2947,10 @@ export class BridgeRunner {
|
|
|
2582
2947
|
});
|
|
2583
2948
|
}
|
|
2584
2949
|
await this.syncBackendSessionBinding();
|
|
2950
|
+
if (replyTo) {
|
|
2951
|
+
this.clearInterruptRetryForReplyTarget(replyTo);
|
|
2952
|
+
this.remoteInterruptsByReplyTo.delete(replyTo);
|
|
2953
|
+
}
|
|
2585
2954
|
if (replyTo) {
|
|
2586
2955
|
this.processedMessageIds.add(replyTo);
|
|
2587
2956
|
}
|
|
@@ -2600,6 +2969,7 @@ export class BridgeRunner {
|
|
|
2600
2969
|
this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
|
|
2601
2970
|
}
|
|
2602
2971
|
} catch (error) {
|
|
2972
|
+
this.activeTurnReplyTo = "";
|
|
2603
2973
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2604
2974
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2605
2975
|
this.copilotLog(
|
|
@@ -2607,6 +2977,11 @@ export class BridgeRunner {
|
|
|
2607
2977
|
);
|
|
2608
2978
|
return;
|
|
2609
2979
|
}
|
|
2980
|
+
const interruptInfo = replyTo ? this.remoteInterruptsByReplyTo.get(replyTo) : null;
|
|
2981
|
+
if (interruptInfo && this.isTurnInterruptedError(error)) {
|
|
2982
|
+
await this.handleInterruptedTurn(replyTo, interruptInfo);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2610
2985
|
if (await this.settleCodexCheckpointUnavailableAfterStream(replyTo, errorMessage)) {
|
|
2611
2986
|
return;
|
|
2612
2987
|
}
|
|
@@ -2663,7 +3038,9 @@ export class BridgeRunner {
|
|
|
2663
3038
|
}
|
|
2664
3039
|
if (replyTo) {
|
|
2665
3040
|
this.inFlightMessageIds.delete(replyTo);
|
|
3041
|
+
this.clearInterruptRetryForReplyTarget(replyTo);
|
|
2666
3042
|
}
|
|
3043
|
+
this.activeTurnReplyTo = "";
|
|
2667
3044
|
this.copilotLog(
|
|
2668
3045
|
`turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
|
|
2669
3046
|
);
|