@love-moon/conductor-cli 0.2.39 → 0.2.41
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 +258 -153
- 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 +321 -121
- package/src/fire/resume.js +498 -107
- package/src/handoff-log-mask.js +64 -0
- package/src/runtime-backends.js +67 -17
- 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-config.js
CHANGED
|
@@ -37,6 +37,11 @@ const DEFAULT_CLIs = {
|
|
|
37
37
|
execArgs: "",
|
|
38
38
|
description: "OpenCode CLI (Conductor runs opencode serve with permission=allow)"
|
|
39
39
|
},
|
|
40
|
+
copilot: {
|
|
41
|
+
command: "copilot",
|
|
42
|
+
execArgs: "",
|
|
43
|
+
description: "GitHub Copilot (built in via SDK)"
|
|
44
|
+
},
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
const backendUrl =
|
|
@@ -63,6 +68,13 @@ function colorize(text, color) {
|
|
|
63
68
|
return `${COLORS[color] || ""}${text}${COLORS.reset}`;
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
function isBuiltInCopilotAvailable() {
|
|
72
|
+
return Boolean(
|
|
73
|
+
packageJson?.dependencies?.["@github/copilot-sdk"] ||
|
|
74
|
+
packageJson?.optionalDependencies?.["@github/copilot-sdk"],
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
function buildConfigEntryLines(cli, info, { commented = false } = {}) {
|
|
67
79
|
const fullCommand = info.execArgs
|
|
68
80
|
? `${info.command} ${info.execArgs}`
|
|
@@ -278,6 +290,10 @@ function detectInstalledCLIs() {
|
|
|
278
290
|
if (!RUNTIME_SUPPORTED_BACKENDS.includes(key)) {
|
|
279
291
|
continue;
|
|
280
292
|
}
|
|
293
|
+
if (key === "copilot" && isBuiltInCopilotAvailable()) {
|
|
294
|
+
detected.push(key);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
281
297
|
if (isCommandAvailable(info.command)) {
|
|
282
298
|
detected.push(key);
|
|
283
299
|
}
|
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
|
|
|
@@ -155,70 +168,8 @@ export function resolveConfiguredPrePrompt({ configFilePath, backend, sessionBac
|
|
|
155
168
|
return undefined;
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
function parseCommandParts(commandLine) {
|
|
159
|
-
const input = String(commandLine || "").trim();
|
|
160
|
-
if (!input) {
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const parts = [];
|
|
165
|
-
let current = "";
|
|
166
|
-
let quote = "";
|
|
167
|
-
let escaping = false;
|
|
168
|
-
let tokenStarted = false;
|
|
169
|
-
|
|
170
|
-
for (const char of input) {
|
|
171
|
-
if (escaping) {
|
|
172
|
-
current += char;
|
|
173
|
-
tokenStarted = true;
|
|
174
|
-
escaping = false;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (char === "\\") {
|
|
179
|
-
escaping = true;
|
|
180
|
-
tokenStarted = true;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (quote) {
|
|
185
|
-
if (char === quote) {
|
|
186
|
-
quote = "";
|
|
187
|
-
} else {
|
|
188
|
-
current += char;
|
|
189
|
-
}
|
|
190
|
-
tokenStarted = true;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (char === "'" || char === "\"") {
|
|
195
|
-
quote = char;
|
|
196
|
-
tokenStarted = true;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (/\s/.test(char)) {
|
|
201
|
-
if (tokenStarted) {
|
|
202
|
-
parts.push(current);
|
|
203
|
-
current = "";
|
|
204
|
-
tokenStarted = false;
|
|
205
|
-
}
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
current += char;
|
|
210
|
-
tokenStarted = true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (tokenStarted) {
|
|
214
|
-
parts.push(current);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return parts;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
171
|
function extractModelOptionFromCommandLine(commandLine) {
|
|
221
|
-
const parts = parseCommandParts(commandLine);
|
|
172
|
+
const { parts } = parseCommandParts(commandLine);
|
|
222
173
|
for (let index = 0; index < parts.length; index += 1) {
|
|
223
174
|
const token = String(parts[index] || "").trim();
|
|
224
175
|
if (!token) {
|
|
@@ -664,7 +615,8 @@ async function main() {
|
|
|
664
615
|
let resumeContext = null;
|
|
665
616
|
if (cliArgs.resumeSessionId) {
|
|
666
617
|
const bootstrap = await bootstrapResumeContextForFire({
|
|
667
|
-
backend: cliArgs.
|
|
618
|
+
backend: cliArgs.backend,
|
|
619
|
+
sessionBackend: cliArgs.sessionBackend,
|
|
668
620
|
configFile: cliArgs.configFile,
|
|
669
621
|
resumeSessionId: cliArgs.resumeSessionId,
|
|
670
622
|
});
|
|
@@ -678,6 +630,7 @@ async function main() {
|
|
|
678
630
|
let reconnectTaskId = null;
|
|
679
631
|
let pendingRemoteStopEvent = null;
|
|
680
632
|
const pendingRemoteInterruptQueue = createPendingRemoteInterruptQueue();
|
|
633
|
+
const completedRefreshSessionRequests = new Map();
|
|
681
634
|
let conductor = null;
|
|
682
635
|
let reconnectResumeInFlight = false;
|
|
683
636
|
let fireShuttingDown = false;
|
|
@@ -762,6 +715,41 @@ async function main() {
|
|
|
762
715
|
return await pendingRemoteInterruptQueue.enqueue(event);
|
|
763
716
|
};
|
|
764
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
|
+
|
|
765
753
|
if (cliArgs.configFile) {
|
|
766
754
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
767
755
|
}
|
|
@@ -796,7 +784,10 @@ async function main() {
|
|
|
796
784
|
conductor = await ConductorClient.connect({
|
|
797
785
|
projectPath: runtimeProjectPath,
|
|
798
786
|
extraEnv: env,
|
|
799
|
-
extraHeaders: buildConductorConnectHeaders(
|
|
787
|
+
extraHeaders: buildConductorConnectHeaders(pkgJson.version, {
|
|
788
|
+
backends: [cliArgs.backend],
|
|
789
|
+
capabilities: ["refresh_session_inplace"],
|
|
790
|
+
}),
|
|
800
791
|
configFile: cliArgs.configFile,
|
|
801
792
|
onConnected: (event) => {
|
|
802
793
|
fireWatchdog.onConnected(event);
|
|
@@ -813,6 +804,7 @@ async function main() {
|
|
|
813
804
|
},
|
|
814
805
|
onStopTask: handleStopTaskCommand,
|
|
815
806
|
onInterruptTurn: handleInterruptTurnCommand,
|
|
807
|
+
onRefreshSession: handleRefreshSessionCommand,
|
|
816
808
|
});
|
|
817
809
|
|
|
818
810
|
const taskContext = await ensureTaskContext(conductor, {
|
|
@@ -852,7 +844,10 @@ async function main() {
|
|
|
852
844
|
}
|
|
853
845
|
}
|
|
854
846
|
|
|
855
|
-
|
|
847
|
+
let nextResumeSessionId = cliArgs.resumeSessionId;
|
|
848
|
+
let nextInitialPrompt =
|
|
849
|
+
taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "";
|
|
850
|
+
let pendingRefreshSessionRequest = null;
|
|
856
851
|
|
|
857
852
|
const sessionCommandLine = resolveAiSessionCommandLine(
|
|
858
853
|
cliArgs.backend,
|
|
@@ -867,19 +862,6 @@ async function main() {
|
|
|
867
862
|
env: process.env,
|
|
868
863
|
});
|
|
869
864
|
|
|
870
|
-
backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
|
|
871
|
-
initialImages: cliArgs.initialImages,
|
|
872
|
-
cwd: runtimeProjectPath,
|
|
873
|
-
resumeSessionId: resolvedResumeSessionId,
|
|
874
|
-
configFile: cliArgs.configFile,
|
|
875
|
-
...(cliArgs.sessionOptions || {}),
|
|
876
|
-
...(sessionCommandLine ? { commandLine: sessionCommandLine } : {}),
|
|
877
|
-
logger: { log },
|
|
878
|
-
...(resolvedPrePrompt ? { prePrompt: resolvedPrePrompt } : {}),
|
|
879
|
-
sessionStoreKey: taskContext.taskId ? `task-${taskContext.taskId}` : undefined,
|
|
880
|
-
resumePersistedSession: Boolean(!resolvedResumeSessionId && taskContext.taskId),
|
|
881
|
-
});
|
|
882
|
-
|
|
883
865
|
log(`Using backend: ${cliArgs.backend}`);
|
|
884
866
|
|
|
885
867
|
try {
|
|
@@ -892,26 +874,6 @@ async function main() {
|
|
|
892
874
|
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
893
875
|
}
|
|
894
876
|
|
|
895
|
-
const runner = new BridgeRunner({
|
|
896
|
-
backendSession,
|
|
897
|
-
conductor,
|
|
898
|
-
taskId: taskContext.taskId,
|
|
899
|
-
pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
|
|
900
|
-
initialPrompt: taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "",
|
|
901
|
-
initialPromptDelivery: taskContext.initialPromptDelivery || "none",
|
|
902
|
-
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
903
|
-
cliArgs: cliArgs.rawBackendArgs,
|
|
904
|
-
backendName: cliArgs.backend,
|
|
905
|
-
resumeSessionId: resolvedResumeSessionId,
|
|
906
|
-
daemonName: resolvedDaemonName,
|
|
907
|
-
});
|
|
908
|
-
reconnectRunner = runner;
|
|
909
|
-
if (pendingRemoteStopEvent) {
|
|
910
|
-
await runner.requestStopFromRemote(pendingRemoteStopEvent);
|
|
911
|
-
pendingRemoteStopEvent = null;
|
|
912
|
-
}
|
|
913
|
-
await pendingRemoteInterruptQueue.flushWith((event) => runner.requestInterruptFromRemote(event));
|
|
914
|
-
|
|
915
877
|
const signals = new AbortController();
|
|
916
878
|
let shutdownSignal = null;
|
|
917
879
|
let backendShutdownRequested = false;
|
|
@@ -955,68 +917,148 @@ async function main() {
|
|
|
955
917
|
}
|
|
956
918
|
}
|
|
957
919
|
|
|
958
|
-
let runnerError = null;
|
|
959
920
|
try {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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),
|
|
963
936
|
});
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
986
1014
|
? {
|
|
987
1015
|
status: "KILLED",
|
|
988
|
-
summary: `
|
|
1016
|
+
summary: `terminated by ${shutdownSignal}`,
|
|
989
1017
|
}
|
|
990
|
-
:
|
|
1018
|
+
: runnerError
|
|
991
1019
|
? {
|
|
992
1020
|
status: "KILLED",
|
|
993
|
-
summary:
|
|
1021
|
+
summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
|
|
994
1022
|
}
|
|
995
|
-
:
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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();
|
|
1007
1050
|
}
|
|
1008
|
-
} catch (error) {
|
|
1009
|
-
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
1010
|
-
}
|
|
1011
|
-
} else {
|
|
1012
|
-
log(`Skipping final status report: task was deleted by user`);
|
|
1013
|
-
// Also clear any pending durable outbox retries (e.g. task_stop_ack)
|
|
1014
|
-
// that would keep failing against the deleted task.
|
|
1015
|
-
if (typeof conductor.clearDurableOutboxTimer === "function") {
|
|
1016
|
-
conductor.clearDurableOutboxTimer();
|
|
1017
1051
|
}
|
|
1018
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
if (runnerError) {
|
|
1055
|
+
throw runnerError;
|
|
1056
|
+
}
|
|
1057
|
+
break;
|
|
1019
1058
|
}
|
|
1059
|
+
} finally {
|
|
1060
|
+
process.off("SIGINT", onSigint);
|
|
1061
|
+
process.off("SIGTERM", onSigterm);
|
|
1020
1062
|
if (shutdownSignal === "SIGINT") {
|
|
1021
1063
|
process.exitCode = 130;
|
|
1022
1064
|
} else if (shutdownSignal === "SIGTERM") {
|
|
@@ -1242,6 +1284,12 @@ Environment:
|
|
|
1242
1284
|
const requestedBackend = conductorArgs.backend
|
|
1243
1285
|
? normalizeRuntimeBackendName(conductorArgs.backend)
|
|
1244
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
|
+
}
|
|
1245
1293
|
const configuredBackend = await resolveConfiguredRuntimeBackend(requestedBackend, allowCliList, {
|
|
1246
1294
|
configFilePath: configFileFromArgs,
|
|
1247
1295
|
});
|
|
@@ -1266,7 +1314,9 @@ Environment:
|
|
|
1266
1314
|
const isAllowedExternalBackend =
|
|
1267
1315
|
!isBuiltInRuntimeBackend(sessionBackend) &&
|
|
1268
1316
|
advertisedExternalBackends.has(sessionBackend);
|
|
1269
|
-
|
|
1317
|
+
const isCommandOptionalBuiltInBackend =
|
|
1318
|
+
isCommandOptionalBuiltInRuntimeBackend(sessionBackend || backend);
|
|
1319
|
+
if (backend && shouldRequireBackend && !hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltInBackend) {
|
|
1270
1320
|
throw new Error(
|
|
1271
1321
|
`Unsupported backend "${backend}". Supported backends: ${[...runtimeSupportedBackends].join(", ") || "none configured"}.`,
|
|
1272
1322
|
);
|
|
@@ -1618,6 +1668,7 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
|
1618
1668
|
|
|
1619
1669
|
export async function bootstrapResumeContextForFire({
|
|
1620
1670
|
backend,
|
|
1671
|
+
sessionBackend,
|
|
1621
1672
|
configFile,
|
|
1622
1673
|
resumeSessionId,
|
|
1623
1674
|
env = process.env,
|
|
@@ -1641,7 +1692,8 @@ export async function bootstrapResumeContextForFire({
|
|
|
1641
1692
|
return { resumeContext, runtimeProjectPath };
|
|
1642
1693
|
}
|
|
1643
1694
|
|
|
1644
|
-
|
|
1695
|
+
const resumeLookupBackend = backend || sessionBackend;
|
|
1696
|
+
resumeContext = await resolveResumeContextFn(resumeLookupBackend, resumeSessionId, {
|
|
1645
1697
|
configFilePath: configFile,
|
|
1646
1698
|
});
|
|
1647
1699
|
const sessionLocation = resumeContext.sessionPath ? ` at ${resumeContext.sessionPath}` : "";
|
|
@@ -1790,6 +1842,7 @@ export class BridgeRunner {
|
|
|
1790
1842
|
os.hostname();
|
|
1791
1843
|
this.needsReconnectRecovery = false;
|
|
1792
1844
|
this.remoteStopInfo = null;
|
|
1845
|
+
this.refreshSessionRequest = null;
|
|
1793
1846
|
this.remoteInterruptsByReplyTo = new Map();
|
|
1794
1847
|
this.pendingInterruptRetryTimers = new Map();
|
|
1795
1848
|
this.activeTurnReplyTo = "";
|
|
@@ -2009,7 +2062,11 @@ export class BridgeRunner {
|
|
|
2009
2062
|
}
|
|
2010
2063
|
|
|
2011
2064
|
shouldSuppressReconnectRecovery() {
|
|
2012
|
-
return this.stopped || Boolean(this.remoteStopInfo);
|
|
2065
|
+
return this.stopped || Boolean(this.remoteStopInfo) || Boolean(this.refreshSessionRequest);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
getRefreshSessionRequest() {
|
|
2069
|
+
return this.refreshSessionRequest;
|
|
2013
2070
|
}
|
|
2014
2071
|
|
|
2015
2072
|
getRemoteStopReason() {
|
|
@@ -2052,6 +2109,54 @@ export class BridgeRunner {
|
|
|
2052
2109
|
}
|
|
2053
2110
|
}
|
|
2054
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
|
+
|
|
2055
2160
|
normalizeReplyTarget(replyTo) {
|
|
2056
2161
|
return typeof replyTo === "string" ? replyTo.trim() : "";
|
|
2057
2162
|
}
|