@love-moon/conductor-cli 0.2.7 → 0.2.9
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-fire.js +101 -2
- package/package.json +3 -3
- package/src/daemon.js +219 -14
package/bin/conductor-fire.js
CHANGED
|
@@ -182,6 +182,45 @@ async function main() {
|
|
|
182
182
|
log(`Using backend: ${cliArgs.backend}`);
|
|
183
183
|
|
|
184
184
|
const env = buildEnv();
|
|
185
|
+
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
186
|
+
let reconnectRunner = null;
|
|
187
|
+
let reconnectTaskId = null;
|
|
188
|
+
let conductor = null;
|
|
189
|
+
let reconnectResumeInFlight = false;
|
|
190
|
+
|
|
191
|
+
const scheduleReconnectRecovery = ({ isReconnect }) => {
|
|
192
|
+
if (!isReconnect) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
log("Conductor connection restored");
|
|
196
|
+
if (reconnectRunner && typeof reconnectRunner.noteReconnect === "function") {
|
|
197
|
+
reconnectRunner.noteReconnect();
|
|
198
|
+
}
|
|
199
|
+
if (!conductor || !reconnectTaskId || reconnectResumeInFlight) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
reconnectResumeInFlight = true;
|
|
203
|
+
void (async () => {
|
|
204
|
+
try {
|
|
205
|
+
await conductor.sendAgentResume({
|
|
206
|
+
active_tasks: [reconnectTaskId],
|
|
207
|
+
source: "conductor-fire",
|
|
208
|
+
metadata: { reconnect: true },
|
|
209
|
+
});
|
|
210
|
+
if (!launchedByDaemon) {
|
|
211
|
+
await conductor.sendTaskStatus(reconnectTaskId, {
|
|
212
|
+
status: "RUNNING",
|
|
213
|
+
summary: "conductor fire reconnected",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
log(`Failed to report reconnect resume: ${error?.message || error}`);
|
|
218
|
+
} finally {
|
|
219
|
+
reconnectResumeInFlight = false;
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
};
|
|
223
|
+
|
|
185
224
|
if (cliArgs.configFile) {
|
|
186
225
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
187
226
|
}
|
|
@@ -200,10 +239,11 @@ async function main() {
|
|
|
200
239
|
// Ignore config loading errors, rely on env vars or defaults
|
|
201
240
|
}
|
|
202
241
|
|
|
203
|
-
|
|
242
|
+
conductor = await ConductorClient.connect({
|
|
204
243
|
projectPath: CLI_PROJECT_PATH,
|
|
205
244
|
extraEnv: env,
|
|
206
245
|
configFile: cliArgs.configFile,
|
|
246
|
+
onConnected: scheduleReconnectRecovery,
|
|
207
247
|
});
|
|
208
248
|
|
|
209
249
|
const taskContext = await ensureTaskContext(conductor, {
|
|
@@ -219,6 +259,17 @@ async function main() {
|
|
|
219
259
|
taskContext.appUrl ? ` (${taskContext.appUrl})` : ""
|
|
220
260
|
}`,
|
|
221
261
|
);
|
|
262
|
+
reconnectTaskId = taskContext.taskId;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await conductor.sendAgentResume({
|
|
266
|
+
active_tasks: [taskContext.taskId],
|
|
267
|
+
source: "conductor-fire",
|
|
268
|
+
metadata: { reconnect: false },
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
272
|
+
}
|
|
222
273
|
|
|
223
274
|
const runner = new BridgeRunner({
|
|
224
275
|
backendSession,
|
|
@@ -230,10 +281,10 @@ async function main() {
|
|
|
230
281
|
cliArgs: cliArgs.rawBackendArgs,
|
|
231
282
|
backendName: cliArgs.backend,
|
|
232
283
|
});
|
|
284
|
+
reconnectRunner = runner;
|
|
233
285
|
|
|
234
286
|
const signals = new AbortController();
|
|
235
287
|
let shutdownSignal = null;
|
|
236
|
-
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
237
288
|
const onSigint = () => {
|
|
238
289
|
shutdownSignal = shutdownSignal || "SIGINT";
|
|
239
290
|
signals.abort();
|
|
@@ -1010,6 +1061,8 @@ class BridgeRunner {
|
|
|
1010
1061
|
this.runningTurn = false;
|
|
1011
1062
|
this.processedMessageIds = new Set();
|
|
1012
1063
|
this.lastRuntimeStatusSignature = null;
|
|
1064
|
+
this.lastRuntimeStatusPayload = null;
|
|
1065
|
+
this.needsReconnectRecovery = false;
|
|
1013
1066
|
}
|
|
1014
1067
|
|
|
1015
1068
|
async start(abortSignal) {
|
|
@@ -1025,6 +1078,9 @@ class BridgeRunner {
|
|
|
1025
1078
|
await this.backfillPendingUserMessages();
|
|
1026
1079
|
|
|
1027
1080
|
while (!this.stopped) {
|
|
1081
|
+
if (this.needsReconnectRecovery && !this.runningTurn) {
|
|
1082
|
+
await this.recoverAfterReconnect();
|
|
1083
|
+
}
|
|
1028
1084
|
let processed = false;
|
|
1029
1085
|
try {
|
|
1030
1086
|
processed = await this.processIncomingBatch();
|
|
@@ -1038,6 +1094,26 @@ class BridgeRunner {
|
|
|
1038
1094
|
}
|
|
1039
1095
|
}
|
|
1040
1096
|
|
|
1097
|
+
noteReconnect() {
|
|
1098
|
+
this.needsReconnectRecovery = true;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async recoverAfterReconnect() {
|
|
1102
|
+
if (!this.needsReconnectRecovery) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
this.needsReconnectRecovery = false;
|
|
1106
|
+
log(`Recovering task ${this.taskId} after reconnect`);
|
|
1107
|
+
// With durable web outbox enabled, user messages are replayed by the server.
|
|
1108
|
+
// Re-running DB-history backfill here can re-drive stale prompts and confuse
|
|
1109
|
+
// the local TUI session after reconnect. Keep startup backfill, but disable
|
|
1110
|
+
// reconnect backfill by default (opt-in for debugging/legacy fallback).
|
|
1111
|
+
if (process.env.CONDUCTOR_FIRE_RECONNECT_BACKFILL === "1") {
|
|
1112
|
+
await this.backfillPendingUserMessages();
|
|
1113
|
+
}
|
|
1114
|
+
await this.replayLastRuntimeStatus();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1041
1117
|
async processIncomingBatch() {
|
|
1042
1118
|
const result = await this.conductor.receiveMessages(this.taskId, 20);
|
|
1043
1119
|
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
@@ -1159,6 +1235,9 @@ class BridgeRunner {
|
|
|
1159
1235
|
return;
|
|
1160
1236
|
}
|
|
1161
1237
|
this.lastRuntimeStatusSignature = signature;
|
|
1238
|
+
this.lastRuntimeStatusPayload = {
|
|
1239
|
+
...runtime,
|
|
1240
|
+
};
|
|
1162
1241
|
|
|
1163
1242
|
try {
|
|
1164
1243
|
await this.conductor.sendRuntimeStatus(this.taskId, {
|
|
@@ -1170,6 +1249,20 @@ class BridgeRunner {
|
|
|
1170
1249
|
}
|
|
1171
1250
|
}
|
|
1172
1251
|
|
|
1252
|
+
async replayLastRuntimeStatus() {
|
|
1253
|
+
if (!this.lastRuntimeStatusPayload) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
await this.conductor.sendRuntimeStatus(this.taskId, {
|
|
1258
|
+
...this.lastRuntimeStatusPayload,
|
|
1259
|
+
created_at: new Date().toISOString(),
|
|
1260
|
+
});
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
log(`Failed to replay runtime status after reconnect: ${error?.message || error}`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1173
1266
|
async respondToMessage(message) {
|
|
1174
1267
|
const content = String(message.content || "").trim();
|
|
1175
1268
|
if (!content) {
|
|
@@ -1180,6 +1273,7 @@ class BridgeRunner {
|
|
|
1180
1273
|
return;
|
|
1181
1274
|
}
|
|
1182
1275
|
this.lastRuntimeStatusSignature = null;
|
|
1276
|
+
this.runningTurn = true;
|
|
1183
1277
|
log(`Processing message ${replyTo} (${message.role})`);
|
|
1184
1278
|
try {
|
|
1185
1279
|
await this.reportRuntimeStatus(
|
|
@@ -1238,11 +1332,14 @@ class BridgeRunner {
|
|
|
1238
1332
|
replyTo,
|
|
1239
1333
|
);
|
|
1240
1334
|
await this.reportError(`${this.backendName} 处理失败: ${errorMessage}`, replyTo);
|
|
1335
|
+
} finally {
|
|
1336
|
+
this.runningTurn = false;
|
|
1241
1337
|
}
|
|
1242
1338
|
}
|
|
1243
1339
|
|
|
1244
1340
|
async handleSyntheticMessage(content, { includeImages }) {
|
|
1245
1341
|
this.lastRuntimeStatusSignature = null;
|
|
1342
|
+
this.runningTurn = true;
|
|
1246
1343
|
try {
|
|
1247
1344
|
const result = await this.backendSession.runTurn(content, {
|
|
1248
1345
|
useInitialImages: includeImages,
|
|
@@ -1269,6 +1366,8 @@ class BridgeRunner {
|
|
|
1269
1366
|
});
|
|
1270
1367
|
} catch (error) {
|
|
1271
1368
|
await this.reportError(`初始提示执行失败: ${error.message}`);
|
|
1369
|
+
} finally {
|
|
1370
|
+
this.runningTurn = false;
|
|
1272
1371
|
}
|
|
1273
1372
|
}
|
|
1274
1373
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"conductor": "bin/conductor.js"
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@love-moon/tui-driver": "0.2.
|
|
20
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
19
|
+
"@love-moon/tui-driver": "0.2.9",
|
|
20
|
+
"@love-moon/conductor-sdk": "0.2.9",
|
|
21
21
|
"dotenv": "^16.4.5",
|
|
22
22
|
"enquirer": "^2.4.1",
|
|
23
23
|
"js-yaml": "^4.1.1",
|
package/src/daemon.js
CHANGED
|
@@ -61,6 +61,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
61
61
|
const killFn = deps.kill || process.kill;
|
|
62
62
|
let requestShutdown = async () => {};
|
|
63
63
|
let shutdownSignalHandled = false;
|
|
64
|
+
let forcedSignalExitHandled = false;
|
|
64
65
|
|
|
65
66
|
const exitAndReturn = (code) => {
|
|
66
67
|
exitFn(code);
|
|
@@ -145,6 +146,14 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
145
146
|
process.env.CONDUCTOR_STOP_FORCE_KILL_TIMEOUT_MS,
|
|
146
147
|
5000,
|
|
147
148
|
);
|
|
149
|
+
const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
|
|
150
|
+
process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
|
|
151
|
+
1000,
|
|
152
|
+
);
|
|
153
|
+
const SHUTDOWN_DISCONNECT_TIMEOUT_MS = parsePositiveInt(
|
|
154
|
+
process.env.CONDUCTOR_SHUTDOWN_DISCONNECT_TIMEOUT_MS,
|
|
155
|
+
1000,
|
|
156
|
+
);
|
|
148
157
|
|
|
149
158
|
try {
|
|
150
159
|
mkdirSyncFn(WORKSPACE_ROOT, { recursive: true });
|
|
@@ -216,8 +225,16 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
216
225
|
};
|
|
217
226
|
|
|
218
227
|
process.on("exit", cleanupLock);
|
|
228
|
+
const signalExitCode = (signal) => (signal === "SIGINT" ? 130 : 143);
|
|
219
229
|
const handleSignal = (signal) => {
|
|
220
|
-
if (shutdownSignalHandled)
|
|
230
|
+
if (shutdownSignalHandled) {
|
|
231
|
+
if (forcedSignalExitHandled) return;
|
|
232
|
+
forcedSignalExitHandled = true;
|
|
233
|
+
log(`Received ${signal} again, forcing exit now`);
|
|
234
|
+
cleanupLock();
|
|
235
|
+
exitFn(signalExitCode(signal));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
221
238
|
shutdownSignalHandled = true;
|
|
222
239
|
void (async () => {
|
|
223
240
|
try {
|
|
@@ -227,7 +244,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
227
244
|
logError(`Graceful shutdown failed on ${signal}: ${err?.message || err}`);
|
|
228
245
|
} finally {
|
|
229
246
|
cleanupLock();
|
|
230
|
-
exitFn(
|
|
247
|
+
exitFn(signalExitCode(signal));
|
|
231
248
|
}
|
|
232
249
|
})();
|
|
233
250
|
};
|
|
@@ -272,6 +289,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
272
289
|
let didRecoverStaleTasks = false;
|
|
273
290
|
const activeTaskProcesses = new Map();
|
|
274
291
|
const suppressedExitStatusReports = new Set();
|
|
292
|
+
const seenCommandRequestIds = new Set();
|
|
275
293
|
const client = createWebSocketClient(sdkConfig, {
|
|
276
294
|
extraHeaders: {
|
|
277
295
|
"x-conductor-host": AGENT_NAME,
|
|
@@ -282,11 +300,18 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
282
300
|
log("Connected to backend");
|
|
283
301
|
}
|
|
284
302
|
disconnectedSinceLastConnectedLog = false;
|
|
303
|
+
sendAgentResume(isReconnect).catch((error) => {
|
|
304
|
+
logError(`sendAgentResume failed: ${error?.message || error}`);
|
|
305
|
+
});
|
|
285
306
|
if (!didRecoverStaleTasks) {
|
|
286
307
|
didRecoverStaleTasks = true;
|
|
287
308
|
recoverStaleTasks().catch((error) => {
|
|
288
309
|
logError(`recoverStaleTasks failed: ${error?.message || error}`);
|
|
289
310
|
});
|
|
311
|
+
} else if (isReconnect) {
|
|
312
|
+
reconcileAssignedTasks().catch((error) => {
|
|
313
|
+
logError(`reconcileAssignedTasks failed: ${error?.message || error}`);
|
|
314
|
+
});
|
|
290
315
|
}
|
|
291
316
|
},
|
|
292
317
|
onDisconnected: () => {
|
|
@@ -356,6 +381,102 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
356
381
|
}
|
|
357
382
|
}
|
|
358
383
|
|
|
384
|
+
async function reconcileAssignedTasks() {
|
|
385
|
+
try {
|
|
386
|
+
const response = await fetchFn(`${BACKEND_HTTP}/api/tasks`, {
|
|
387
|
+
method: "GET",
|
|
388
|
+
headers: {
|
|
389
|
+
Authorization: `Bearer ${AGENT_TOKEN}`,
|
|
390
|
+
Accept: "application/json",
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
logError(`Failed to reconcile tasks: HTTP ${response.status}`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const tasks = await response.json();
|
|
398
|
+
if (!Array.isArray(tasks)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const localTaskIds = new Set(activeTaskProcesses.keys());
|
|
402
|
+
const assigned = tasks.filter((task) => {
|
|
403
|
+
const agentHost = String(task?.agent_host || "").trim();
|
|
404
|
+
const status = String(task?.status || "").trim().toLowerCase();
|
|
405
|
+
return agentHost === AGENT_NAME && (status === "unknown" || status === "running");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
let killedCount = 0;
|
|
409
|
+
for (const task of assigned) {
|
|
410
|
+
const taskId = String(task?.id || "");
|
|
411
|
+
if (!taskId) continue;
|
|
412
|
+
if (localTaskIds.has(taskId)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const patchResp = await fetchFn(`${BACKEND_HTTP}/api/tasks/${taskId}`, {
|
|
416
|
+
method: "PATCH",
|
|
417
|
+
headers: {
|
|
418
|
+
Authorization: `Bearer ${AGENT_TOKEN}`,
|
|
419
|
+
Accept: "application/json",
|
|
420
|
+
"Content-Type": "application/json",
|
|
421
|
+
},
|
|
422
|
+
body: JSON.stringify({ status: "killed" }),
|
|
423
|
+
});
|
|
424
|
+
if (patchResp.ok) {
|
|
425
|
+
killedCount += 1;
|
|
426
|
+
} else {
|
|
427
|
+
logError(`Failed to reconcile stale task ${taskId}: HTTP ${patchResp.status}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (assigned.length || localTaskIds.size) {
|
|
432
|
+
log(
|
|
433
|
+
`Reconciled tasks after reconnect: backendAssigned=${assigned.length} localActive=${localTaskIds.size} markedKilled=${killedCount}`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
logError(`reconcileAssignedTasks error: ${error?.message || error}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function sendAgentResume(isReconnect = false) {
|
|
442
|
+
await client.sendJson({
|
|
443
|
+
type: "agent_resume",
|
|
444
|
+
payload: {
|
|
445
|
+
active_tasks: [...activeTaskProcesses.keys()],
|
|
446
|
+
source: "conductor-daemon",
|
|
447
|
+
metadata: { is_reconnect: Boolean(isReconnect) },
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function markRequestSeen(requestId) {
|
|
453
|
+
if (!requestId) return true;
|
|
454
|
+
if (seenCommandRequestIds.has(requestId)) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
seenCommandRequestIds.add(requestId);
|
|
458
|
+
if (seenCommandRequestIds.size > 2000) {
|
|
459
|
+
const first = seenCommandRequestIds.values().next();
|
|
460
|
+
if (!first.done) {
|
|
461
|
+
seenCommandRequestIds.delete(first.value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function sendAgentCommandAck({ requestId, taskId, eventType, accepted = true }) {
|
|
468
|
+
if (!requestId) return Promise.resolve();
|
|
469
|
+
return client.sendJson({
|
|
470
|
+
type: "agent_command_ack",
|
|
471
|
+
payload: {
|
|
472
|
+
request_id: String(requestId),
|
|
473
|
+
task_id: taskId ? String(taskId) : undefined,
|
|
474
|
+
event_type: eventType,
|
|
475
|
+
accepted: Boolean(accepted),
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
359
480
|
function handleEvent(event) {
|
|
360
481
|
if (event.type === "create_task") {
|
|
361
482
|
handleCreateTask(event.payload);
|
|
@@ -369,8 +490,18 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
369
490
|
function handleStopTask(payload) {
|
|
370
491
|
const taskId = payload?.task_id;
|
|
371
492
|
if (!taskId) return;
|
|
372
|
-
|
|
373
493
|
const requestId = payload?.request_id ? String(payload.request_id) : "";
|
|
494
|
+
if (requestId && !markRequestSeen(requestId)) {
|
|
495
|
+
log(`Duplicate stop_task ignored for ${taskId} (request_id=${requestId})`);
|
|
496
|
+
sendAgentCommandAck({
|
|
497
|
+
requestId,
|
|
498
|
+
taskId,
|
|
499
|
+
eventType: "stop_task",
|
|
500
|
+
accepted: true,
|
|
501
|
+
}).catch(() => {});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
374
505
|
const sendStopAck = (accepted) => {
|
|
375
506
|
if (!requestId) return;
|
|
376
507
|
client
|
|
@@ -385,6 +516,14 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
385
516
|
.catch((err) => {
|
|
386
517
|
logError(`Failed to report task_stop_ack for ${taskId}: ${err?.message || err}`);
|
|
387
518
|
});
|
|
519
|
+
sendAgentCommandAck({
|
|
520
|
+
requestId,
|
|
521
|
+
taskId,
|
|
522
|
+
eventType: "stop_task",
|
|
523
|
+
accepted,
|
|
524
|
+
}).catch((err) => {
|
|
525
|
+
logError(`Failed to report agent_command_ack(stop_task) for ${taskId}: ${err?.message || err}`);
|
|
526
|
+
});
|
|
388
527
|
};
|
|
389
528
|
|
|
390
529
|
const record = activeTaskProcesses.get(taskId);
|
|
@@ -485,11 +624,35 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
485
624
|
}
|
|
486
625
|
|
|
487
626
|
async function handleCreateTask(payload) {
|
|
488
|
-
const {
|
|
627
|
+
const {
|
|
628
|
+
task_id: taskId,
|
|
629
|
+
project_id: projectId,
|
|
630
|
+
backend_type: backendType,
|
|
631
|
+
initial_content: initialContent,
|
|
632
|
+
request_id: requestIdRaw,
|
|
633
|
+
} =
|
|
489
634
|
payload || {};
|
|
635
|
+
const requestId = requestIdRaw ? String(requestIdRaw) : "";
|
|
490
636
|
|
|
491
637
|
if (!taskId || !projectId) {
|
|
492
638
|
logError(`Invalid create_task payload: ${JSON.stringify(payload)}`);
|
|
639
|
+
sendAgentCommandAck({
|
|
640
|
+
requestId,
|
|
641
|
+
taskId,
|
|
642
|
+
eventType: "create_task",
|
|
643
|
+
accepted: false,
|
|
644
|
+
}).catch(() => {});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (requestId && !markRequestSeen(requestId)) {
|
|
649
|
+
log(`Duplicate create_task ignored for ${taskId} (request_id=${requestId})`);
|
|
650
|
+
sendAgentCommandAck({
|
|
651
|
+
requestId,
|
|
652
|
+
taskId,
|
|
653
|
+
eventType: "create_task",
|
|
654
|
+
accepted: true,
|
|
655
|
+
}).catch(() => {});
|
|
493
656
|
return;
|
|
494
657
|
}
|
|
495
658
|
|
|
@@ -497,6 +660,12 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
497
660
|
const effectiveBackend = backendType || SUPPORTED_BACKENDS[0];
|
|
498
661
|
if (!SUPPORTED_BACKENDS.includes(effectiveBackend)) {
|
|
499
662
|
logError(`Unsupported backend: ${effectiveBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
|
|
663
|
+
sendAgentCommandAck({
|
|
664
|
+
requestId,
|
|
665
|
+
taskId,
|
|
666
|
+
eventType: "create_task",
|
|
667
|
+
accepted: false,
|
|
668
|
+
}).catch(() => {});
|
|
500
669
|
client
|
|
501
670
|
.sendJson({
|
|
502
671
|
type: "task_status_update",
|
|
@@ -511,6 +680,15 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
511
680
|
return;
|
|
512
681
|
}
|
|
513
682
|
|
|
683
|
+
sendAgentCommandAck({
|
|
684
|
+
requestId,
|
|
685
|
+
taskId,
|
|
686
|
+
eventType: "create_task",
|
|
687
|
+
accepted: true,
|
|
688
|
+
}).catch((err) => {
|
|
689
|
+
logError(`Failed to report agent_command_ack(create_task) for ${taskId}: ${err?.message || err}`);
|
|
690
|
+
});
|
|
691
|
+
|
|
514
692
|
const cliCommand = ALLOW_CLI_LIST[effectiveBackend];
|
|
515
693
|
|
|
516
694
|
log("");
|
|
@@ -731,15 +909,19 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
731
909
|
activeEntries.map(async ([taskId, record]) => {
|
|
732
910
|
suppressedExitStatusReports.add(taskId);
|
|
733
911
|
try {
|
|
734
|
-
await
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
912
|
+
await withTimeout(
|
|
913
|
+
client.sendJson({
|
|
914
|
+
type: "task_status_update",
|
|
915
|
+
payload: {
|
|
916
|
+
task_id: taskId,
|
|
917
|
+
project_id: record.projectId,
|
|
918
|
+
status: "KILLED",
|
|
919
|
+
summary: `daemon shutdown (${reason})`,
|
|
920
|
+
},
|
|
921
|
+
}),
|
|
922
|
+
SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
|
|
923
|
+
`report shutdown status for ${taskId}`,
|
|
924
|
+
);
|
|
743
925
|
} catch (err) {
|
|
744
926
|
logError(`Failed to report shutdown status (KILLED) for ${taskId}: ${err?.message || err}`);
|
|
745
927
|
}
|
|
@@ -762,7 +944,11 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
762
944
|
activeTaskProcesses.clear();
|
|
763
945
|
|
|
764
946
|
try {
|
|
765
|
-
await
|
|
947
|
+
await withTimeout(
|
|
948
|
+
Promise.resolve(client.disconnect()),
|
|
949
|
+
SHUTDOWN_DISCONNECT_TIMEOUT_MS,
|
|
950
|
+
"disconnect daemon websocket",
|
|
951
|
+
);
|
|
766
952
|
} catch (error) {
|
|
767
953
|
logError(`Failed to disconnect client on daemon close: ${error?.message || error}`);
|
|
768
954
|
}
|
|
@@ -828,6 +1014,25 @@ function parsePositiveInt(value, fallback) {
|
|
|
828
1014
|
return fallback;
|
|
829
1015
|
}
|
|
830
1016
|
|
|
1017
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
1018
|
+
let timer = null;
|
|
1019
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1020
|
+
timer = setTimeout(() => {
|
|
1021
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
1022
|
+
}, timeoutMs);
|
|
1023
|
+
if (typeof timer?.unref === "function") {
|
|
1024
|
+
timer.unref();
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
try {
|
|
1028
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
1029
|
+
} finally {
|
|
1030
|
+
if (timer) {
|
|
1031
|
+
clearTimeout(timer);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
831
1036
|
function expandHomePath(inputPath, homeDir) {
|
|
832
1037
|
if (typeof inputPath !== "string" || !inputPath) {
|
|
833
1038
|
return inputPath;
|