svamp-cli 0.1.63 → 0.1.64
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/{api-Cegey1dh.mjs → api-BRbsyqJ4.mjs} +9 -2
- package/dist/cli.mjs +25 -20
- package/dist/{commands-3Zu9sCxa.mjs → commands-BfMlD9o4.mjs} +2 -2
- package/dist/{commands-BRow9cGp.mjs → commands-Brx7D-77.mjs} +1 -1
- package/dist/{commands-dm3PSNsk.mjs → commands-CF32XIau.mjs} +3 -3
- package/dist/{commands-6EyqaoCp.mjs → commands-UFi0_ESV.mjs} +48 -24
- package/dist/index.mjs +1 -1
- package/dist/{package-7U32bRBY.mjs → package-Dg0hQJRC.mjs} +2 -2
- package/dist/{run-BaMf-bAo.mjs → run-BImPgXHd.mjs} +828 -360
- package/dist/{run-COqTRwXb.mjs → run-C7VxH4X8.mjs} +80 -28
- package/dist/{tunnel-dl6vFKgd.mjs → tunnel-C3UsqTxi.mjs} +53 -65
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os__default from 'os';
|
|
2
|
-
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile, unlink } from 'fs/promises';
|
|
3
|
-
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
2
|
+
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile, rename, unlink } from 'fs/promises';
|
|
3
|
+
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
4
4
|
import path, { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { spawn as spawn$1 } from 'child_process';
|
|
@@ -70,7 +70,7 @@ const ISOLATION_PREFERENCE = ["nono", "docker", "podman"];
|
|
|
70
70
|
|
|
71
71
|
function resolveRoleLevel(sharing, userEmail) {
|
|
72
72
|
let level = -1;
|
|
73
|
-
if (userEmail) {
|
|
73
|
+
if (userEmail && Array.isArray(sharing.allowedUsers)) {
|
|
74
74
|
const sharedUser = sharing.allowedUsers.find(
|
|
75
75
|
(u) => u.email.toLowerCase() === userEmail.toLowerCase()
|
|
76
76
|
);
|
|
@@ -303,7 +303,10 @@ function loadPersistedMachineMetadata(svampHomeDir) {
|
|
|
303
303
|
function savePersistedMachineMetadata(svampHomeDir, data) {
|
|
304
304
|
try {
|
|
305
305
|
mkdirSync(svampHomeDir, { recursive: true });
|
|
306
|
-
|
|
306
|
+
const filePath = getMachineMetadataPath(svampHomeDir);
|
|
307
|
+
const tmpPath = filePath + ".tmp";
|
|
308
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
309
|
+
renameSync(tmpPath, filePath);
|
|
307
310
|
} catch (err) {
|
|
308
311
|
console.error("[HYPHA MACHINE] Failed to persist machine metadata:", err);
|
|
309
312
|
}
|
|
@@ -313,9 +316,36 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
313
316
|
let currentDaemonState = { ...daemonState };
|
|
314
317
|
let metadataVersion = 1;
|
|
315
318
|
let daemonStateVersion = 1;
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
+
const listeners = [];
|
|
320
|
+
const removeListener = (listener, reason) => {
|
|
321
|
+
const idx = listeners.indexOf(listener);
|
|
322
|
+
if (idx >= 0) {
|
|
323
|
+
listeners.splice(idx, 1);
|
|
324
|
+
console.log(`[HYPHA MACHINE] Listener removed (${reason}), remaining: ${listeners.length}`);
|
|
325
|
+
const rintfId = listener._rintf_service_id;
|
|
326
|
+
if (rintfId) {
|
|
327
|
+
server.unregisterService(rintfId).catch(() => {
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const notifyListeners = (update) => {
|
|
333
|
+
const snapshot = [...listeners];
|
|
334
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
335
|
+
const listener = snapshot[i];
|
|
336
|
+
try {
|
|
337
|
+
const result = listener.onUpdate(update);
|
|
338
|
+
if (result && typeof result.catch === "function") {
|
|
339
|
+
result.catch((err) => {
|
|
340
|
+
console.error(`[HYPHA MACHINE] Async listener error:`, err);
|
|
341
|
+
removeListener(listener, "async error");
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error(`[HYPHA MACHINE] Listener error:`, err);
|
|
346
|
+
removeListener(listener, "sync error");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
319
349
|
};
|
|
320
350
|
const serviceInfo = await server.registerService(
|
|
321
351
|
{
|
|
@@ -335,11 +365,14 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
335
365
|
};
|
|
336
366
|
},
|
|
337
367
|
// Heartbeat
|
|
338
|
-
heartbeat: async (context) =>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
heartbeat: async (context) => {
|
|
369
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
370
|
+
return {
|
|
371
|
+
time: Date.now(),
|
|
372
|
+
status: currentDaemonState.status,
|
|
373
|
+
machineId
|
|
374
|
+
};
|
|
375
|
+
},
|
|
343
376
|
// List active sessions on this machine
|
|
344
377
|
listSessions: async (context) => {
|
|
345
378
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
@@ -401,7 +434,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
401
434
|
machineId
|
|
402
435
|
});
|
|
403
436
|
if (result.type === "success" && result.sessionId) {
|
|
404
|
-
|
|
437
|
+
notifyListeners({
|
|
405
438
|
type: "new-session",
|
|
406
439
|
sessionId: result.sessionId,
|
|
407
440
|
machineId
|
|
@@ -414,7 +447,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
414
447
|
stopSession: async (sessionId, context) => {
|
|
415
448
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
416
449
|
const result = handlers.stopSession(sessionId);
|
|
417
|
-
|
|
450
|
+
notifyListeners({
|
|
418
451
|
type: "session-stopped",
|
|
419
452
|
sessionId,
|
|
420
453
|
machineId
|
|
@@ -423,7 +456,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
423
456
|
},
|
|
424
457
|
// Restart agent process in a session (machine-level fallback)
|
|
425
458
|
restartSession: async (sessionId, context) => {
|
|
426
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
459
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
427
460
|
return await handlers.restartSession(sessionId);
|
|
428
461
|
},
|
|
429
462
|
// Stop the daemon
|
|
@@ -455,7 +488,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
455
488
|
sharing: currentMetadata.sharing,
|
|
456
489
|
securityContextConfig: currentMetadata.securityContextConfig
|
|
457
490
|
});
|
|
458
|
-
|
|
491
|
+
notifyListeners({
|
|
459
492
|
type: "update-machine",
|
|
460
493
|
machineId,
|
|
461
494
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -485,7 +518,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
485
518
|
}
|
|
486
519
|
currentDaemonState = newState;
|
|
487
520
|
daemonStateVersion++;
|
|
488
|
-
|
|
521
|
+
notifyListeners({
|
|
489
522
|
type: "update-machine",
|
|
490
523
|
machineId,
|
|
491
524
|
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
@@ -515,7 +548,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
515
548
|
sharing: currentMetadata.sharing,
|
|
516
549
|
securityContextConfig: currentMetadata.securityContextConfig
|
|
517
550
|
});
|
|
518
|
-
|
|
551
|
+
notifyListeners({
|
|
519
552
|
type: "update-machine",
|
|
520
553
|
machineId,
|
|
521
554
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -536,60 +569,18 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
536
569
|
sharing: currentMetadata.sharing,
|
|
537
570
|
securityContextConfig: currentMetadata.securityContextConfig
|
|
538
571
|
});
|
|
539
|
-
|
|
572
|
+
notifyListeners({
|
|
540
573
|
type: "update-machine",
|
|
541
574
|
machineId,
|
|
542
575
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
543
576
|
});
|
|
544
577
|
return { success: true };
|
|
545
578
|
},
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
subscribe: async function* (context) {
|
|
579
|
+
// Register a listener for real-time updates (app calls this with _rintf callback)
|
|
580
|
+
registerListener: async (callback, context) => {
|
|
549
581
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let callerDisconnected = false;
|
|
553
|
-
const callerClientId = context?.from;
|
|
554
|
-
const push = (update) => {
|
|
555
|
-
pending.push(update);
|
|
556
|
-
wake?.();
|
|
557
|
-
};
|
|
558
|
-
const onDisconnect = (event) => {
|
|
559
|
-
if (callerClientId && event.client_id === callerClientId) {
|
|
560
|
-
callerDisconnected = true;
|
|
561
|
-
const w = wake;
|
|
562
|
-
wake = null;
|
|
563
|
-
w?.();
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
server.on("remote_client_disconnected", onDisconnect);
|
|
567
|
-
subscribers.add(push);
|
|
568
|
-
console.log(`[HYPHA MACHINE] subscribe() started (total: ${subscribers.size})`);
|
|
569
|
-
try {
|
|
570
|
-
yield {
|
|
571
|
-
type: "update-machine",
|
|
572
|
-
machineId,
|
|
573
|
-
metadata: { value: currentMetadata, version: metadataVersion },
|
|
574
|
-
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
575
|
-
};
|
|
576
|
-
while (!callerDisconnected) {
|
|
577
|
-
while (pending.length === 0 && !callerDisconnected) {
|
|
578
|
-
await new Promise((r) => {
|
|
579
|
-
wake = r;
|
|
580
|
-
});
|
|
581
|
-
wake = null;
|
|
582
|
-
}
|
|
583
|
-
while (pending.length > 0) {
|
|
584
|
-
yield pending.shift();
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
} finally {
|
|
588
|
-
server.off("remote_client_disconnected", onDisconnect);
|
|
589
|
-
subscribers.delete(push);
|
|
590
|
-
wake?.();
|
|
591
|
-
console.log(`[HYPHA MACHINE] subscribe() ended (remaining: ${subscribers.size})`);
|
|
592
|
-
}
|
|
582
|
+
listeners.push(callback);
|
|
583
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
593
584
|
},
|
|
594
585
|
// Shell access
|
|
595
586
|
bash: async (command, cwd, context) => {
|
|
@@ -615,7 +606,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
615
606
|
const targetPath = resolve(path || homedir());
|
|
616
607
|
const home = homedir();
|
|
617
608
|
const isOwner = !currentMetadata.sharing?.enabled || context?.user?.email && currentMetadata.sharing.owner && context.user.email.toLowerCase() === currentMetadata.sharing.owner.toLowerCase();
|
|
618
|
-
if (!isOwner && !targetPath.startsWith(home)) {
|
|
609
|
+
if (!isOwner && targetPath !== home && !targetPath.startsWith(home + "/")) {
|
|
619
610
|
throw new Error(`Access denied: path must be within ${home}`);
|
|
620
611
|
}
|
|
621
612
|
const showHidden = options?.showHidden ?? false;
|
|
@@ -656,7 +647,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
656
647
|
},
|
|
657
648
|
/** Add and start a new supervised process. */
|
|
658
649
|
processAdd: async (params, context) => {
|
|
659
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
650
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
660
651
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
661
652
|
return handlers.supervisor.add(params.spec);
|
|
662
653
|
},
|
|
@@ -665,7 +656,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
665
656
|
* Returns { action: 'created'|'updated'|'no-change', info: ProcessInfo }
|
|
666
657
|
*/
|
|
667
658
|
processApply: async (params, context) => {
|
|
668
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
659
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
669
660
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
670
661
|
return handlers.supervisor.apply(params.spec);
|
|
671
662
|
},
|
|
@@ -674,7 +665,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
674
665
|
* Returns updated ProcessInfo.
|
|
675
666
|
*/
|
|
676
667
|
processUpdate: async (params, context) => {
|
|
677
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
668
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
678
669
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
679
670
|
return handlers.supervisor.update(params.idOrName, params.spec);
|
|
680
671
|
},
|
|
@@ -719,7 +710,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
719
710
|
serviceList: async (context) => {
|
|
720
711
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
721
712
|
try {
|
|
722
|
-
const { listServiceGroups } = await import('./api-
|
|
713
|
+
const { listServiceGroups } = await import('./api-BRbsyqJ4.mjs');
|
|
723
714
|
return await listServiceGroups();
|
|
724
715
|
} catch (err) {
|
|
725
716
|
return [];
|
|
@@ -728,13 +719,13 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
728
719
|
/** Get full details of a single service group (includes backends + health). */
|
|
729
720
|
serviceGet: async (params, context) => {
|
|
730
721
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
731
|
-
const { getServiceGroup } = await import('./api-
|
|
722
|
+
const { getServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
732
723
|
return getServiceGroup(params.name);
|
|
733
724
|
},
|
|
734
725
|
/** Delete a service group. */
|
|
735
726
|
serviceDelete: async (params, context) => {
|
|
736
727
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
737
|
-
const { deleteServiceGroup } = await import('./api-
|
|
728
|
+
const { deleteServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
738
729
|
return deleteServiceGroup(params.name);
|
|
739
730
|
},
|
|
740
731
|
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
@@ -745,19 +736,27 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
745
736
|
return { success: false, error: "No OpenAI API key found. Set OPENAI_API_KEY or pass apiKey." };
|
|
746
737
|
}
|
|
747
738
|
try {
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
739
|
+
const wisCtrl = new AbortController();
|
|
740
|
+
const wisTimer = setTimeout(() => wisCtrl.abort(), 15e3);
|
|
741
|
+
let response;
|
|
742
|
+
try {
|
|
743
|
+
response = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: {
|
|
746
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
747
|
+
"Content-Type": "application/json"
|
|
748
|
+
},
|
|
749
|
+
body: JSON.stringify({
|
|
750
|
+
session: {
|
|
751
|
+
type: "realtime",
|
|
752
|
+
model: params.model || "gpt-realtime-mini"
|
|
753
|
+
}
|
|
754
|
+
}),
|
|
755
|
+
signal: wisCtrl.signal
|
|
756
|
+
});
|
|
757
|
+
} finally {
|
|
758
|
+
clearTimeout(wisTimer);
|
|
759
|
+
}
|
|
761
760
|
if (!response.ok) {
|
|
762
761
|
return { success: false, error: `OpenAI API error: ${response.status}` };
|
|
763
762
|
}
|
|
@@ -776,7 +775,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
776
775
|
updateMetadata: (newMetadata) => {
|
|
777
776
|
currentMetadata = newMetadata;
|
|
778
777
|
metadataVersion++;
|
|
779
|
-
|
|
778
|
+
notifyListeners({
|
|
780
779
|
type: "update-machine",
|
|
781
780
|
machineId,
|
|
782
781
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -785,13 +784,17 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
785
784
|
updateDaemonState: (newState) => {
|
|
786
785
|
currentDaemonState = newState;
|
|
787
786
|
daemonStateVersion++;
|
|
788
|
-
|
|
787
|
+
notifyListeners({
|
|
789
788
|
type: "update-machine",
|
|
790
789
|
machineId,
|
|
791
790
|
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
792
791
|
});
|
|
793
792
|
},
|
|
794
793
|
disconnect: async () => {
|
|
794
|
+
const toRemove = [...listeners];
|
|
795
|
+
for (const listener of toRemove) {
|
|
796
|
+
removeListener(listener, "disconnect");
|
|
797
|
+
}
|
|
795
798
|
await server.unregisterService(serviceInfo.id);
|
|
796
799
|
}
|
|
797
800
|
};
|
|
@@ -809,17 +812,21 @@ function loadMessages(messagesDir, sessionId) {
|
|
|
809
812
|
} catch {
|
|
810
813
|
}
|
|
811
814
|
}
|
|
812
|
-
return messages.slice(-
|
|
815
|
+
return messages.slice(-1e3);
|
|
813
816
|
} catch {
|
|
814
817
|
return [];
|
|
815
818
|
}
|
|
816
819
|
}
|
|
817
820
|
function appendMessage(messagesDir, sessionId, msg) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
+
try {
|
|
822
|
+
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
823
|
+
if (!existsSync(messagesDir)) {
|
|
824
|
+
mkdirSync$1(messagesDir, { recursive: true });
|
|
825
|
+
}
|
|
826
|
+
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.error(`[HYPHA SESSION ${sessionId}] Failed to persist message: ${err?.message ?? err}`);
|
|
821
829
|
}
|
|
822
|
-
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
823
830
|
}
|
|
824
831
|
async function registerSessionService(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
825
832
|
const messages = options?.messagesDir ? loadMessages(options.messagesDir) : [];
|
|
@@ -834,9 +841,36 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
834
841
|
mode: "remote",
|
|
835
842
|
time: Date.now()
|
|
836
843
|
};
|
|
837
|
-
const
|
|
838
|
-
const
|
|
839
|
-
|
|
844
|
+
const listeners = [];
|
|
845
|
+
const removeListener = (listener, reason) => {
|
|
846
|
+
const idx = listeners.indexOf(listener);
|
|
847
|
+
if (idx >= 0) {
|
|
848
|
+
listeners.splice(idx, 1);
|
|
849
|
+
console.log(`[HYPHA SESSION ${sessionId}] Listener removed (${reason}), remaining: ${listeners.length}`);
|
|
850
|
+
const rintfId = listener._rintf_service_id;
|
|
851
|
+
if (rintfId) {
|
|
852
|
+
server.unregisterService(rintfId).catch(() => {
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
const notifyListeners = (update) => {
|
|
858
|
+
const snapshot = [...listeners];
|
|
859
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
860
|
+
const listener = snapshot[i];
|
|
861
|
+
try {
|
|
862
|
+
const result = listener.onUpdate(update);
|
|
863
|
+
if (result && typeof result.catch === "function") {
|
|
864
|
+
result.catch((err) => {
|
|
865
|
+
console.error(`[HYPHA SESSION ${sessionId}] Async listener error:`, err);
|
|
866
|
+
removeListener(listener, "async error");
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
} catch (err) {
|
|
870
|
+
console.error(`[HYPHA SESSION ${sessionId}] Listener error:`, err);
|
|
871
|
+
removeListener(listener, "sync error");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
840
874
|
};
|
|
841
875
|
const pushMessage = (content, role = "agent") => {
|
|
842
876
|
let wrappedContent;
|
|
@@ -867,7 +901,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
867
901
|
if (options?.messagesDir) {
|
|
868
902
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
869
903
|
}
|
|
870
|
-
|
|
904
|
+
notifyListeners({
|
|
871
905
|
type: "new-message",
|
|
872
906
|
sessionId,
|
|
873
907
|
message: msg
|
|
@@ -928,7 +962,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
928
962
|
if (options?.messagesDir) {
|
|
929
963
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
930
964
|
}
|
|
931
|
-
|
|
965
|
+
notifyListeners({
|
|
932
966
|
type: "new-message",
|
|
933
967
|
sessionId,
|
|
934
968
|
message: msg
|
|
@@ -955,7 +989,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
955
989
|
}
|
|
956
990
|
metadata = newMetadata;
|
|
957
991
|
metadataVersion++;
|
|
958
|
-
|
|
992
|
+
notifyListeners({
|
|
959
993
|
type: "update-session",
|
|
960
994
|
sessionId,
|
|
961
995
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -973,7 +1007,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
973
1007
|
* Null values remove keys from the config.
|
|
974
1008
|
*/
|
|
975
1009
|
updateConfig: async (patch, context) => {
|
|
976
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1010
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
977
1011
|
callbacks.onUpdateConfig?.(patch);
|
|
978
1012
|
return { success: true };
|
|
979
1013
|
},
|
|
@@ -996,7 +1030,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
996
1030
|
}
|
|
997
1031
|
agentState = newState;
|
|
998
1032
|
agentStateVersion++;
|
|
999
|
-
|
|
1033
|
+
notifyListeners({
|
|
1000
1034
|
type: "update-session",
|
|
1001
1035
|
sessionId,
|
|
1002
1036
|
agentState: { value: agentState, version: agentStateVersion }
|
|
@@ -1019,7 +1053,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1019
1053
|
return { success: true };
|
|
1020
1054
|
},
|
|
1021
1055
|
switchMode: async (mode, context) => {
|
|
1022
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1056
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1023
1057
|
callbacks.onSwitchMode(mode);
|
|
1024
1058
|
return { success: true };
|
|
1025
1059
|
},
|
|
@@ -1034,16 +1068,18 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1034
1068
|
},
|
|
1035
1069
|
// ── Activity ──
|
|
1036
1070
|
keepAlive: async (thinking, mode, context) => {
|
|
1071
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1037
1072
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1038
|
-
|
|
1073
|
+
notifyListeners({
|
|
1039
1074
|
type: "activity",
|
|
1040
1075
|
sessionId,
|
|
1041
1076
|
...lastActivity
|
|
1042
1077
|
});
|
|
1043
1078
|
},
|
|
1044
1079
|
sessionEnd: async (context) => {
|
|
1080
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1045
1081
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1046
|
-
|
|
1082
|
+
notifyListeners({
|
|
1047
1083
|
type: "activity",
|
|
1048
1084
|
sessionId,
|
|
1049
1085
|
...lastActivity
|
|
@@ -1104,6 +1140,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1104
1140
|
},
|
|
1105
1141
|
/** Returns the caller's effective role (null if no access). Does not throw. */
|
|
1106
1142
|
getEffectiveRole: async (context) => {
|
|
1143
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1107
1144
|
const role = getEffectiveRole(context, metadata.sharing);
|
|
1108
1145
|
return { role };
|
|
1109
1146
|
},
|
|
@@ -1117,7 +1154,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1117
1154
|
}
|
|
1118
1155
|
metadata = { ...metadata, sharing: newSharing };
|
|
1119
1156
|
metadataVersion++;
|
|
1120
|
-
|
|
1157
|
+
notifyListeners({
|
|
1121
1158
|
type: "update-session",
|
|
1122
1159
|
sessionId,
|
|
1123
1160
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1135,7 +1172,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1135
1172
|
}
|
|
1136
1173
|
metadata = { ...metadata, securityContext: newSecurityContext };
|
|
1137
1174
|
metadataVersion++;
|
|
1138
|
-
|
|
1175
|
+
notifyListeners({
|
|
1139
1176
|
type: "update-session",
|
|
1140
1177
|
sessionId,
|
|
1141
1178
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1150,67 +1187,69 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1150
1187
|
}
|
|
1151
1188
|
return await callbacks.onApplySystemPrompt(prompt);
|
|
1152
1189
|
},
|
|
1153
|
-
// ──
|
|
1154
|
-
|
|
1155
|
-
// Returns an async generator that yields real-time updates for this session.
|
|
1156
|
-
// hypha-rpc proxies the generator across the RPC boundary — the frontend
|
|
1157
|
-
// iterates with `for await (const update of service.subscribe())`.
|
|
1158
|
-
//
|
|
1159
|
-
// Initial state is replayed as the first batch of yields so the frontend
|
|
1160
|
-
// can reconstruct full session state without a separate RPC call.
|
|
1161
|
-
// Cleanup is automatic: when the frontend disconnects, hypha-rpc calls the
|
|
1162
|
-
// generator's close method, triggering the finally block which removes the
|
|
1163
|
-
// subscriber. No reverse `_rintf` service is registered.
|
|
1164
|
-
subscribe: async function* (context) {
|
|
1190
|
+
// ── Listener Registration ──
|
|
1191
|
+
registerListener: async (callback, context) => {
|
|
1165
1192
|
authorizeRequest(context, metadata.sharing, "view");
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1193
|
+
listeners.push(callback);
|
|
1194
|
+
const replayMessages = messages.slice(-50);
|
|
1195
|
+
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
1196
|
+
for (const msg of replayMessages) {
|
|
1197
|
+
if (listeners.indexOf(callback) < 0) break;
|
|
1198
|
+
try {
|
|
1199
|
+
const result = callback.onUpdate({
|
|
1200
|
+
type: "new-message",
|
|
1201
|
+
sessionId,
|
|
1202
|
+
message: msg
|
|
1203
|
+
});
|
|
1204
|
+
if (result && typeof result.catch === "function") {
|
|
1205
|
+
try {
|
|
1206
|
+
await Promise.race([
|
|
1207
|
+
result,
|
|
1208
|
+
new Promise(
|
|
1209
|
+
(_, reject) => setTimeout(() => reject(new Error("Replay message timeout")), REPLAY_MESSAGE_TIMEOUT_MS)
|
|
1210
|
+
)
|
|
1211
|
+
]);
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1214
|
+
removeListener(callback, "replay error");
|
|
1215
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1220
|
+
removeListener(callback, "replay error");
|
|
1221
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1180
1222
|
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1223
|
+
}
|
|
1224
|
+
if (listeners.indexOf(callback) < 0) {
|
|
1225
|
+
return { success: false, error: "Listener was removed during replay" };
|
|
1226
|
+
}
|
|
1185
1227
|
try {
|
|
1186
|
-
|
|
1228
|
+
const result = callback.onUpdate({
|
|
1187
1229
|
type: "update-session",
|
|
1188
1230
|
sessionId,
|
|
1189
1231
|
metadata: { value: metadata, version: metadataVersion },
|
|
1190
1232
|
agentState: { value: agentState, version: agentStateVersion }
|
|
1191
|
-
};
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1233
|
+
});
|
|
1234
|
+
if (result && typeof result.catch === "function") {
|
|
1235
|
+
result.catch(() => {
|
|
1236
|
+
});
|
|
1195
1237
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
try {
|
|
1241
|
+
const result = callback.onUpdate({
|
|
1242
|
+
type: "activity",
|
|
1243
|
+
sessionId,
|
|
1244
|
+
...lastActivity
|
|
1245
|
+
});
|
|
1246
|
+
if (result && typeof result.catch === "function") {
|
|
1247
|
+
result.catch(() => {
|
|
1248
|
+
});
|
|
1207
1249
|
}
|
|
1208
|
-
}
|
|
1209
|
-
server.off("remote_client_disconnected", onDisconnect);
|
|
1210
|
-
subscribers.delete(push);
|
|
1211
|
-
wake?.();
|
|
1212
|
-
console.log(`[HYPHA SESSION ${sessionId}] subscribe() ended (remaining: ${subscribers.size})`);
|
|
1250
|
+
} catch {
|
|
1213
1251
|
}
|
|
1252
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
1214
1253
|
}
|
|
1215
1254
|
},
|
|
1216
1255
|
{ overwrite: true }
|
|
@@ -1225,7 +1264,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1225
1264
|
updateMetadata: (newMetadata) => {
|
|
1226
1265
|
metadata = newMetadata;
|
|
1227
1266
|
metadataVersion++;
|
|
1228
|
-
|
|
1267
|
+
notifyListeners({
|
|
1229
1268
|
type: "update-session",
|
|
1230
1269
|
sessionId,
|
|
1231
1270
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1234,7 +1273,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1234
1273
|
updateAgentState: (newAgentState) => {
|
|
1235
1274
|
agentState = newAgentState;
|
|
1236
1275
|
agentStateVersion++;
|
|
1237
|
-
|
|
1276
|
+
notifyListeners({
|
|
1238
1277
|
type: "update-session",
|
|
1239
1278
|
sessionId,
|
|
1240
1279
|
agentState: { value: agentState, version: agentStateVersion }
|
|
@@ -1242,7 +1281,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1242
1281
|
},
|
|
1243
1282
|
sendKeepAlive: (thinking, mode) => {
|
|
1244
1283
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1245
|
-
|
|
1284
|
+
notifyListeners({
|
|
1246
1285
|
type: "activity",
|
|
1247
1286
|
sessionId,
|
|
1248
1287
|
...lastActivity
|
|
@@ -1250,7 +1289,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1250
1289
|
},
|
|
1251
1290
|
sendSessionEnd: () => {
|
|
1252
1291
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1253
|
-
|
|
1292
|
+
notifyListeners({
|
|
1254
1293
|
type: "activity",
|
|
1255
1294
|
sessionId,
|
|
1256
1295
|
...lastActivity
|
|
@@ -1266,12 +1305,16 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1266
1305
|
} catch {
|
|
1267
1306
|
}
|
|
1268
1307
|
}
|
|
1269
|
-
|
|
1308
|
+
notifyListeners({
|
|
1270
1309
|
type: "clear-messages",
|
|
1271
1310
|
sessionId
|
|
1272
1311
|
});
|
|
1273
1312
|
},
|
|
1274
1313
|
disconnect: async () => {
|
|
1314
|
+
const toRemove = [...listeners];
|
|
1315
|
+
for (const listener of toRemove) {
|
|
1316
|
+
removeListener(listener, "disconnect");
|
|
1317
|
+
}
|
|
1275
1318
|
await server.unregisterService(serviceInfo.id);
|
|
1276
1319
|
}
|
|
1277
1320
|
};
|
|
@@ -1406,6 +1449,19 @@ class SessionArtifactSync {
|
|
|
1406
1449
|
this.log(`[ARTIFACT SYNC] Created new collection: ${this.collectionId}`);
|
|
1407
1450
|
}
|
|
1408
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* fetch() with an AbortSignal-based timeout to prevent indefinite hangs
|
|
1454
|
+
* on slow/stalled presigned URL servers.
|
|
1455
|
+
*/
|
|
1456
|
+
async fetchWithTimeout(url, options = {}, timeoutMs = 6e4) {
|
|
1457
|
+
const controller = new AbortController();
|
|
1458
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1459
|
+
try {
|
|
1460
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
1461
|
+
} finally {
|
|
1462
|
+
clearTimeout(timer);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1409
1465
|
/**
|
|
1410
1466
|
* Upload a file to an artifact using the presigned URL pattern:
|
|
1411
1467
|
* 1. put_file() returns a presigned upload URL
|
|
@@ -1420,11 +1476,11 @@ class SessionArtifactSync {
|
|
|
1420
1476
|
if (!putUrl || typeof putUrl !== "string") {
|
|
1421
1477
|
throw new Error(`put_file returned invalid URL for ${filePath}: ${putUrl}`);
|
|
1422
1478
|
}
|
|
1423
|
-
const resp = await
|
|
1479
|
+
const resp = await this.fetchWithTimeout(putUrl, {
|
|
1424
1480
|
method: "PUT",
|
|
1425
1481
|
body: content,
|
|
1426
1482
|
headers: { "Content-Type": "application/octet-stream" }
|
|
1427
|
-
});
|
|
1483
|
+
}, 12e4);
|
|
1428
1484
|
if (!resp.ok) {
|
|
1429
1485
|
throw new Error(`Upload failed for ${filePath}: ${resp.status} ${resp.statusText}`);
|
|
1430
1486
|
}
|
|
@@ -1441,7 +1497,7 @@ class SessionArtifactSync {
|
|
|
1441
1497
|
_rkwargs: true
|
|
1442
1498
|
});
|
|
1443
1499
|
if (!getUrl || typeof getUrl !== "string") return null;
|
|
1444
|
-
const resp = await
|
|
1500
|
+
const resp = await this.fetchWithTimeout(getUrl, {}, 6e4);
|
|
1445
1501
|
if (!resp.ok) return null;
|
|
1446
1502
|
return await resp.text();
|
|
1447
1503
|
}
|
|
@@ -1459,16 +1515,27 @@ class SessionArtifactSync {
|
|
|
1459
1515
|
const artifactAlias = `session-${sessionId}`;
|
|
1460
1516
|
const sessionJsonPath = join$1(sessionsDir, "session.json");
|
|
1461
1517
|
const messagesPath = join$1(sessionsDir, "messages.jsonl");
|
|
1462
|
-
|
|
1518
|
+
let sessionData = null;
|
|
1519
|
+
if (existsSync(sessionJsonPath)) {
|
|
1520
|
+
try {
|
|
1521
|
+
sessionData = JSON.parse(readFileSync(sessionJsonPath, "utf-8"));
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1463
1525
|
const messagesExist = existsSync(messagesPath);
|
|
1464
1526
|
const messageCount = messagesExist ? readFileSync(messagesPath, "utf-8").split("\n").filter((l) => l.trim()).length : 0;
|
|
1465
1527
|
let artifactId;
|
|
1528
|
+
let existingArtifactId = null;
|
|
1466
1529
|
try {
|
|
1467
1530
|
const existing = await this.artifactManager.read({
|
|
1468
1531
|
artifact_id: artifactAlias,
|
|
1469
1532
|
_rkwargs: true
|
|
1470
1533
|
});
|
|
1471
|
-
|
|
1534
|
+
existingArtifactId = existing.id;
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
if (existingArtifactId) {
|
|
1538
|
+
artifactId = existingArtifactId;
|
|
1472
1539
|
await this.artifactManager.edit({
|
|
1473
1540
|
artifact_id: artifactId,
|
|
1474
1541
|
manifest: {
|
|
@@ -1482,7 +1549,7 @@ class SessionArtifactSync {
|
|
|
1482
1549
|
stage: true,
|
|
1483
1550
|
_rkwargs: true
|
|
1484
1551
|
});
|
|
1485
|
-
}
|
|
1552
|
+
} else {
|
|
1486
1553
|
const artifact = await this.artifactManager.create({
|
|
1487
1554
|
alias: artifactAlias,
|
|
1488
1555
|
parent_id: this.collectionId,
|
|
@@ -1536,6 +1603,16 @@ class SessionArtifactSync {
|
|
|
1536
1603
|
}, delayMs);
|
|
1537
1604
|
this.syncTimers.set(sessionId, timer);
|
|
1538
1605
|
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Cancel any pending debounced sync for a session (e.g., when session is stopped).
|
|
1608
|
+
*/
|
|
1609
|
+
cancelSync(sessionId) {
|
|
1610
|
+
const existing = this.syncTimers.get(sessionId);
|
|
1611
|
+
if (existing) {
|
|
1612
|
+
clearTimeout(existing);
|
|
1613
|
+
this.syncTimers.delete(sessionId);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1539
1616
|
/**
|
|
1540
1617
|
* Download a session from artifact store to local disk.
|
|
1541
1618
|
*/
|
|
@@ -1903,6 +1980,7 @@ function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
|
1903
1980
|
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1904
1981
|
ctx.activeToolCalls.delete(toolCallId);
|
|
1905
1982
|
ctx.toolCallStartTimes.delete(toolCallId);
|
|
1983
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
1906
1984
|
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1907
1985
|
if (timeout) {
|
|
1908
1986
|
clearTimeout(timeout);
|
|
@@ -1912,7 +1990,12 @@ function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
|
1912
1990
|
ctx.emit({ type: "tool-result", toolName: toolKindStr, result: content, callId: toolCallId });
|
|
1913
1991
|
if (ctx.activeToolCalls.size === 0) {
|
|
1914
1992
|
ctx.clearIdleTimeout();
|
|
1915
|
-
ctx.
|
|
1993
|
+
const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
1994
|
+
ctx.setIdleTimeout(() => {
|
|
1995
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
1996
|
+
ctx.emitIdleStatus();
|
|
1997
|
+
}
|
|
1998
|
+
}, idleTimeoutMs);
|
|
1916
1999
|
}
|
|
1917
2000
|
}
|
|
1918
2001
|
function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
@@ -1921,6 +2004,7 @@ function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
|
1921
2004
|
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1922
2005
|
ctx.activeToolCalls.delete(toolCallId);
|
|
1923
2006
|
ctx.toolCallStartTimes.delete(toolCallId);
|
|
2007
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
1924
2008
|
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1925
2009
|
if (timeout) {
|
|
1926
2010
|
clearTimeout(timeout);
|
|
@@ -1936,7 +2020,12 @@ function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
|
1936
2020
|
});
|
|
1937
2021
|
if (ctx.activeToolCalls.size === 0) {
|
|
1938
2022
|
ctx.clearIdleTimeout();
|
|
1939
|
-
ctx.
|
|
2023
|
+
const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
2024
|
+
ctx.setIdleTimeout(() => {
|
|
2025
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
2026
|
+
ctx.emitIdleStatus();
|
|
2027
|
+
}
|
|
2028
|
+
}, idleTimeoutMs);
|
|
1940
2029
|
}
|
|
1941
2030
|
}
|
|
1942
2031
|
function handleToolCallUpdate(update, ctx) {
|
|
@@ -2185,10 +2274,22 @@ class AcpBackend {
|
|
|
2185
2274
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2186
2275
|
});
|
|
2187
2276
|
this.process.on("exit", (code, signal) => {
|
|
2188
|
-
if (
|
|
2277
|
+
if (this.disposed) return;
|
|
2278
|
+
if (code !== 0 && code !== null) {
|
|
2189
2279
|
signalStartupFailure(new Error(`Exit code: ${code}`));
|
|
2190
2280
|
this.log(`[ACP] Process exited: code=${code}, signal=${signal}`);
|
|
2191
2281
|
this.emit({ type: "status", status: "stopped", detail: `Exit code: ${code}` });
|
|
2282
|
+
} else if (code === null && this.waitingForResponse) {
|
|
2283
|
+
this.log(`[ACP] Process killed by signal: ${signal} (mid-turn)`);
|
|
2284
|
+
this.waitingForResponse = false;
|
|
2285
|
+
if (this.idleResolver) {
|
|
2286
|
+
this.idleResolver();
|
|
2287
|
+
this.idleResolver = null;
|
|
2288
|
+
}
|
|
2289
|
+
this.emit({ type: "status", status: "stopped", detail: `Process killed by signal: ${signal}` });
|
|
2290
|
+
} else if (code === 0 && this.waitingForResponse) {
|
|
2291
|
+
this.log(`[ACP] Process exited cleanly but response was pending \u2014 emitting idle`);
|
|
2292
|
+
this.emitIdleStatus();
|
|
2192
2293
|
}
|
|
2193
2294
|
});
|
|
2194
2295
|
const streams = nodeToWebStreams(this.process.stdin, this.process.stdout);
|
|
@@ -2343,12 +2444,14 @@ class AcpBackend {
|
|
|
2343
2444
|
const maybeErr = error;
|
|
2344
2445
|
if (startupFailure && error === startupFailure) return true;
|
|
2345
2446
|
if (maybeErr.code === "ENOENT" || maybeErr.code === "EACCES" || maybeErr.code === "EPIPE") return true;
|
|
2447
|
+
if (maybeErr.code === "DISPOSED") return true;
|
|
2346
2448
|
const msg = error.message.toLowerCase();
|
|
2347
2449
|
if (msg.includes("api key") || msg.includes("not configured") || msg.includes("401") || msg.includes("403")) return true;
|
|
2348
2450
|
return false;
|
|
2349
2451
|
};
|
|
2350
2452
|
await withRetry(
|
|
2351
2453
|
async () => {
|
|
2454
|
+
if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
|
|
2352
2455
|
let timeoutHandle = null;
|
|
2353
2456
|
try {
|
|
2354
2457
|
const result = await Promise.race([
|
|
@@ -2399,6 +2502,7 @@ class AcpBackend {
|
|
|
2399
2502
|
this.log(`[ACP] Creating new session...`);
|
|
2400
2503
|
const sessionResponse = await withRetry(
|
|
2401
2504
|
async () => {
|
|
2505
|
+
if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
|
|
2402
2506
|
let timeoutHandle = null;
|
|
2403
2507
|
try {
|
|
2404
2508
|
const result = await Promise.race([
|
|
@@ -2548,9 +2652,16 @@ class AcpBackend {
|
|
|
2548
2652
|
handleThinkingUpdate(update, ctx);
|
|
2549
2653
|
}
|
|
2550
2654
|
emitIdleStatus() {
|
|
2655
|
+
const resolver = this.idleResolver;
|
|
2656
|
+
this.idleResolver = null;
|
|
2657
|
+
this.waitingForResponse = false;
|
|
2551
2658
|
this.emit({ type: "status", status: "idle" });
|
|
2552
|
-
if (
|
|
2553
|
-
this.
|
|
2659
|
+
if (resolver) {
|
|
2660
|
+
const newPromptInFlight = this.waitingForResponse;
|
|
2661
|
+
resolver();
|
|
2662
|
+
if (newPromptInFlight) {
|
|
2663
|
+
this.waitingForResponse = true;
|
|
2664
|
+
}
|
|
2554
2665
|
}
|
|
2555
2666
|
}
|
|
2556
2667
|
async sendPrompt(sessionId, prompt) {
|
|
@@ -2604,9 +2715,14 @@ class AcpBackend {
|
|
|
2604
2715
|
}
|
|
2605
2716
|
async cancel(sessionId) {
|
|
2606
2717
|
if (!this.connection || !this.acpSessionId) return;
|
|
2718
|
+
this.waitingForResponse = false;
|
|
2719
|
+
if (this.idleResolver) {
|
|
2720
|
+
this.idleResolver();
|
|
2721
|
+
this.idleResolver = null;
|
|
2722
|
+
}
|
|
2607
2723
|
try {
|
|
2608
2724
|
await this.connection.cancel({ sessionId: this.acpSessionId });
|
|
2609
|
-
this.emit({ type: "status", status: "
|
|
2725
|
+
this.emit({ type: "status", status: "cancelled", detail: "Cancelled by user" });
|
|
2610
2726
|
} catch (error) {
|
|
2611
2727
|
this.log("[ACP] Error cancelling:", error);
|
|
2612
2728
|
}
|
|
@@ -2629,16 +2745,24 @@ class AcpBackend {
|
|
|
2629
2745
|
}
|
|
2630
2746
|
}
|
|
2631
2747
|
if (this.process) {
|
|
2632
|
-
|
|
2748
|
+
try {
|
|
2749
|
+
this.process.kill("SIGTERM");
|
|
2750
|
+
} catch {
|
|
2751
|
+
}
|
|
2633
2752
|
await new Promise((resolve) => {
|
|
2634
2753
|
const timeout = setTimeout(() => {
|
|
2635
|
-
|
|
2754
|
+
try {
|
|
2755
|
+
if (this.process) this.process.kill("SIGKILL");
|
|
2756
|
+
} catch {
|
|
2757
|
+
}
|
|
2636
2758
|
resolve();
|
|
2637
2759
|
}, 1e3);
|
|
2638
|
-
|
|
2760
|
+
const done = () => {
|
|
2639
2761
|
clearTimeout(timeout);
|
|
2640
2762
|
resolve();
|
|
2641
|
-
}
|
|
2763
|
+
};
|
|
2764
|
+
this.process?.once("exit", done);
|
|
2765
|
+
this.process?.once("close", done);
|
|
2642
2766
|
});
|
|
2643
2767
|
this.process = null;
|
|
2644
2768
|
}
|
|
@@ -2653,6 +2777,7 @@ class AcpBackend {
|
|
|
2653
2777
|
for (const timeout of this.toolCallTimeouts.values()) clearTimeout(timeout);
|
|
2654
2778
|
this.toolCallTimeouts.clear();
|
|
2655
2779
|
this.toolCallStartTimes.clear();
|
|
2780
|
+
this.toolCallIdToNameMap.clear();
|
|
2656
2781
|
}
|
|
2657
2782
|
}
|
|
2658
2783
|
|
|
@@ -2718,6 +2843,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2718
2843
|
let pendingText = "";
|
|
2719
2844
|
let turnText = "";
|
|
2720
2845
|
let flushTimer = null;
|
|
2846
|
+
let bridgeStopped = false;
|
|
2721
2847
|
function flushText() {
|
|
2722
2848
|
if (pendingText) {
|
|
2723
2849
|
sessionService.pushMessage({
|
|
@@ -2732,6 +2858,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2732
2858
|
}
|
|
2733
2859
|
}
|
|
2734
2860
|
backend.onMessage((msg) => {
|
|
2861
|
+
if (bridgeStopped) return;
|
|
2735
2862
|
switch (msg.type) {
|
|
2736
2863
|
case "model-output": {
|
|
2737
2864
|
if (msg.textDelta) {
|
|
@@ -2763,6 +2890,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2763
2890
|
setMetadata((m) => ({ ...m, lifecycleState: "running" }));
|
|
2764
2891
|
} else if (msg.status === "error") {
|
|
2765
2892
|
flushText();
|
|
2893
|
+
turnText = "";
|
|
2766
2894
|
sessionService.pushMessage(
|
|
2767
2895
|
{ type: "message", message: `Agent process exited unexpectedly: ${msg.detail || "Unknown error"}` },
|
|
2768
2896
|
"event"
|
|
@@ -2771,8 +2899,12 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2771
2899
|
setMetadata((m) => ({ ...m, lifecycleState: "error" }));
|
|
2772
2900
|
} else if (msg.status === "stopped") {
|
|
2773
2901
|
flushText();
|
|
2902
|
+
turnText = "";
|
|
2774
2903
|
sessionService.sendSessionEnd();
|
|
2775
2904
|
setMetadata((m) => ({ ...m, lifecycleState: "stopped" }));
|
|
2905
|
+
} else if (msg.status === "cancelled") {
|
|
2906
|
+
flushText();
|
|
2907
|
+
turnText = "";
|
|
2776
2908
|
}
|
|
2777
2909
|
break;
|
|
2778
2910
|
}
|
|
@@ -2853,6 +2985,14 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2853
2985
|
}
|
|
2854
2986
|
}
|
|
2855
2987
|
});
|
|
2988
|
+
return () => {
|
|
2989
|
+
bridgeStopped = true;
|
|
2990
|
+
if (flushTimer) {
|
|
2991
|
+
clearTimeout(flushTimer);
|
|
2992
|
+
flushTimer = null;
|
|
2993
|
+
}
|
|
2994
|
+
pendingText = "";
|
|
2995
|
+
};
|
|
2856
2996
|
}
|
|
2857
2997
|
class HyphaPermissionHandler {
|
|
2858
2998
|
constructor(shouldAutoAllow, log) {
|
|
@@ -2918,6 +3058,10 @@ class CodexMcpBackend {
|
|
|
2918
3058
|
client;
|
|
2919
3059
|
transport = null;
|
|
2920
3060
|
disposed = false;
|
|
3061
|
+
turnCancelled = false;
|
|
3062
|
+
// set by cancel() to suppress 'idle' in sendPrompt() finally
|
|
3063
|
+
turnId = 0;
|
|
3064
|
+
// monotonically increasing; each sendPrompt() captures its own ID
|
|
2921
3065
|
codexSessionId = null;
|
|
2922
3066
|
conversationId = null;
|
|
2923
3067
|
svampSessionId = null;
|
|
@@ -2969,7 +3113,10 @@ class CodexMcpBackend {
|
|
|
2969
3113
|
}
|
|
2970
3114
|
async sendPrompt(sessionId, prompt) {
|
|
2971
3115
|
if (!this.connected) throw new Error("Codex not connected");
|
|
3116
|
+
this.turnCancelled = false;
|
|
3117
|
+
const myTurnId = ++this.turnId;
|
|
2972
3118
|
this.emit({ type: "status", status: "running" });
|
|
3119
|
+
let hadError = false;
|
|
2973
3120
|
try {
|
|
2974
3121
|
let response;
|
|
2975
3122
|
if (this.codexSessionId) {
|
|
@@ -2990,16 +3137,20 @@ class CodexMcpBackend {
|
|
|
2990
3137
|
}
|
|
2991
3138
|
}
|
|
2992
3139
|
} catch (err) {
|
|
3140
|
+
hadError = true;
|
|
2993
3141
|
this.log(`[Codex] Error in sendPrompt: ${err.message}`);
|
|
2994
3142
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2995
3143
|
throw err;
|
|
2996
3144
|
} finally {
|
|
2997
|
-
this.
|
|
3145
|
+
if (!this.turnCancelled && !hadError && this.turnId === myTurnId) {
|
|
3146
|
+
this.emit({ type: "status", status: "idle" });
|
|
3147
|
+
}
|
|
2998
3148
|
}
|
|
2999
3149
|
}
|
|
3000
3150
|
async cancel(_sessionId) {
|
|
3001
3151
|
this.log("[Codex] Cancel requested");
|
|
3002
|
-
this.
|
|
3152
|
+
this.turnCancelled = true;
|
|
3153
|
+
this.emit({ type: "status", status: "cancelled" });
|
|
3003
3154
|
}
|
|
3004
3155
|
async respondToPermission(requestId, approved) {
|
|
3005
3156
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -3194,8 +3345,8 @@ class CodexMcpBackend {
|
|
|
3194
3345
|
this.emit({ type: "status", status: "running" });
|
|
3195
3346
|
break;
|
|
3196
3347
|
case "task_complete":
|
|
3348
|
+
break;
|
|
3197
3349
|
case "turn_aborted":
|
|
3198
|
-
this.emit({ type: "status", status: "idle" });
|
|
3199
3350
|
break;
|
|
3200
3351
|
case "agent_message": {
|
|
3201
3352
|
const content = event.content;
|
|
@@ -3827,7 +3978,10 @@ class ProcessSupervisor {
|
|
|
3827
3978
|
/** Start a stopped/failed process by id or name. */
|
|
3828
3979
|
async start(idOrName) {
|
|
3829
3980
|
const entry = this.require(idOrName);
|
|
3830
|
-
if (entry.child
|
|
3981
|
+
if (entry.child) {
|
|
3982
|
+
if (entry.stopping) throw new Error(`Process '${entry.spec.name}' is being stopped, try again shortly`);
|
|
3983
|
+
throw new Error(`Process '${entry.spec.name}' is already running`);
|
|
3984
|
+
}
|
|
3831
3985
|
entry.stopping = false;
|
|
3832
3986
|
await this.startEntry(entry, false);
|
|
3833
3987
|
}
|
|
@@ -3847,15 +4001,21 @@ class ProcessSupervisor {
|
|
|
3847
4001
|
/** Restart a process (stop if running, then start again). */
|
|
3848
4002
|
async restart(idOrName) {
|
|
3849
4003
|
const entry = this.require(idOrName);
|
|
3850
|
-
if (entry.
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
4004
|
+
if (entry.restarting) return;
|
|
4005
|
+
entry.restarting = true;
|
|
4006
|
+
try {
|
|
4007
|
+
if (entry.child) {
|
|
4008
|
+
entry.stopping = true;
|
|
4009
|
+
this.clearTimers(entry);
|
|
4010
|
+
await this.killChild(entry.child);
|
|
4011
|
+
entry.child = void 0;
|
|
4012
|
+
}
|
|
4013
|
+
entry.stopping = false;
|
|
4014
|
+
entry.state.restartCount++;
|
|
4015
|
+
await this.startEntry(entry, false);
|
|
4016
|
+
} finally {
|
|
4017
|
+
entry.restarting = false;
|
|
3855
4018
|
}
|
|
3856
|
-
entry.stopping = false;
|
|
3857
|
-
entry.state.restartCount++;
|
|
3858
|
-
await this.startEntry(entry, false);
|
|
3859
4019
|
}
|
|
3860
4020
|
/** Stop the process and remove it from supervision (deletes persisted spec). */
|
|
3861
4021
|
async remove(idOrName) {
|
|
@@ -3989,7 +4149,9 @@ class ProcessSupervisor {
|
|
|
3989
4149
|
}
|
|
3990
4150
|
async persistSpec(spec) {
|
|
3991
4151
|
const filePath = path.join(this.persistDir, `${spec.id}.json`);
|
|
3992
|
-
|
|
4152
|
+
const tmpPath = filePath + ".tmp";
|
|
4153
|
+
await writeFile(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4154
|
+
await rename(tmpPath, filePath);
|
|
3993
4155
|
}
|
|
3994
4156
|
async deleteSpec(id) {
|
|
3995
4157
|
try {
|
|
@@ -4063,7 +4225,13 @@ class ProcessSupervisor {
|
|
|
4063
4225
|
};
|
|
4064
4226
|
child.stdout?.on("data", appendLog);
|
|
4065
4227
|
child.stderr?.on("data", appendLog);
|
|
4066
|
-
child.on("
|
|
4228
|
+
child.on("error", (err) => {
|
|
4229
|
+
console.error(`[SUPERVISOR] Process '${spec.name}' error: ${err.message}`);
|
|
4230
|
+
});
|
|
4231
|
+
child.on("close", (code, signal) => {
|
|
4232
|
+
if (entry.child !== child) return;
|
|
4233
|
+
this.onProcessExit(entry, code, signal);
|
|
4234
|
+
});
|
|
4067
4235
|
if (spec.probe) this.setupProbe(entry);
|
|
4068
4236
|
if (spec.ttl !== void 0) this.setupTTL(entry);
|
|
4069
4237
|
console.log(`[SUPERVISOR] Started '${spec.name}' pid=${child.pid}`);
|
|
@@ -4151,6 +4319,8 @@ class ProcessSupervisor {
|
|
|
4151
4319
|
}
|
|
4152
4320
|
}
|
|
4153
4321
|
async triggerProbeRestart(entry) {
|
|
4322
|
+
if (entry.restarting) return;
|
|
4323
|
+
if (entry.stopping) return;
|
|
4154
4324
|
console.warn(`[SUPERVISOR] Restarting '${entry.spec.name}' due to probe failures`);
|
|
4155
4325
|
entry.state.consecutiveProbeFailures = 0;
|
|
4156
4326
|
this.clearTimers(entry);
|
|
@@ -4175,6 +4345,7 @@ class ProcessSupervisor {
|
|
|
4175
4345
|
console.log(`[SUPERVISOR] Process '${entry.spec.name}' TTL expired`);
|
|
4176
4346
|
entry.state.status = "expired";
|
|
4177
4347
|
entry.stopping = true;
|
|
4348
|
+
this.clearTimers(entry);
|
|
4178
4349
|
const cleanup = async () => {
|
|
4179
4350
|
if (entry.child) await this.killChild(entry.child);
|
|
4180
4351
|
this.entries.delete(entry.spec.id);
|
|
@@ -4186,13 +4357,36 @@ class ProcessSupervisor {
|
|
|
4186
4357
|
// ── Process kill helper ───────────────────────────────────────────────────
|
|
4187
4358
|
killChild(child) {
|
|
4188
4359
|
return new Promise((resolve) => {
|
|
4189
|
-
|
|
4360
|
+
let resolved = false;
|
|
4361
|
+
let forceKillTimer;
|
|
4362
|
+
let hardDeadlineTimer;
|
|
4363
|
+
const done = () => {
|
|
4364
|
+
if (!resolved) {
|
|
4365
|
+
resolved = true;
|
|
4366
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4367
|
+
if (hardDeadlineTimer) clearTimeout(hardDeadlineTimer);
|
|
4368
|
+
resolve();
|
|
4369
|
+
}
|
|
4370
|
+
};
|
|
4190
4371
|
child.once("exit", done);
|
|
4191
|
-
child.
|
|
4192
|
-
|
|
4193
|
-
child.kill("
|
|
4372
|
+
child.once("close", done);
|
|
4373
|
+
try {
|
|
4374
|
+
child.kill("SIGTERM");
|
|
4375
|
+
} catch {
|
|
4376
|
+
}
|
|
4377
|
+
forceKillTimer = setTimeout(() => {
|
|
4378
|
+
try {
|
|
4379
|
+
child.kill("SIGKILL");
|
|
4380
|
+
} catch {
|
|
4381
|
+
}
|
|
4382
|
+
hardDeadlineTimer = setTimeout(() => {
|
|
4383
|
+
if (!resolved) {
|
|
4384
|
+
resolved = true;
|
|
4385
|
+
console.warn(`[SUPERVISOR] Process pid=${child.pid} did not exit after SIGKILL \u2014 forcing resolution`);
|
|
4386
|
+
resolve();
|
|
4387
|
+
}
|
|
4388
|
+
}, 2e3);
|
|
4194
4389
|
}, 5e3);
|
|
4195
|
-
child.once("exit", () => clearTimeout(forceKill));
|
|
4196
4390
|
});
|
|
4197
4391
|
}
|
|
4198
4392
|
// ── Timer cleanup ─────────────────────────────────────────────────────────
|
|
@@ -4290,7 +4484,9 @@ function readSvampConfig(configPath) {
|
|
|
4290
4484
|
function writeSvampConfig(configPath, config) {
|
|
4291
4485
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
4292
4486
|
const content = JSON.stringify(config, null, 2);
|
|
4293
|
-
|
|
4487
|
+
const tmpPath = configPath + ".tmp";
|
|
4488
|
+
writeFileSync(tmpPath, content);
|
|
4489
|
+
renameSync(tmpPath, configPath);
|
|
4294
4490
|
return content;
|
|
4295
4491
|
}
|
|
4296
4492
|
function getRalphStateFilePath(directory, sessionId) {
|
|
@@ -4344,7 +4540,9 @@ started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResu
|
|
|
4344
4540
|
|
|
4345
4541
|
${state.task}
|
|
4346
4542
|
`;
|
|
4347
|
-
|
|
4543
|
+
const tmpPath = `${filePath}.tmp`;
|
|
4544
|
+
writeFileSync(tmpPath, content);
|
|
4545
|
+
renameSync(tmpPath, filePath);
|
|
4348
4546
|
}
|
|
4349
4547
|
function removeRalphState(filePath) {
|
|
4350
4548
|
try {
|
|
@@ -4433,20 +4631,17 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
4433
4631
|
ralphSystemPrompt: ralphSysPrompt
|
|
4434
4632
|
}]
|
|
4435
4633
|
}));
|
|
4436
|
-
sessionService.updateMetadata(getMetadata());
|
|
4437
4634
|
sessionService.pushMessage(
|
|
4438
|
-
{ type: "message", message: buildIterationStatus(1, state.max_iterations, state.completion_promise) },
|
|
4635
|
+
{ type: "message", message: buildIterationStatus(state.iteration + 1, state.max_iterations, state.completion_promise) },
|
|
4439
4636
|
"event"
|
|
4440
4637
|
);
|
|
4441
|
-
logger.log(`[svampConfig] Ralph loop started: "${state.task.slice(0, 50)}..."`);
|
|
4638
|
+
logger.log(`[svampConfig] Ralph loop started/resumed at iteration ${state.iteration + 1}: "${state.task.slice(0, 50)}..."`);
|
|
4442
4639
|
onRalphLoopActivated?.();
|
|
4443
4640
|
} else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
|
|
4444
4641
|
setMetadata((m) => ({ ...m, ralphLoop }));
|
|
4445
|
-
sessionService.updateMetadata(getMetadata());
|
|
4446
4642
|
}
|
|
4447
4643
|
} else if (prevRalph?.active) {
|
|
4448
4644
|
setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
|
|
4449
|
-
sessionService.updateMetadata(getMetadata());
|
|
4450
4645
|
sessionService.pushMessage(
|
|
4451
4646
|
{ type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
|
|
4452
4647
|
"event"
|
|
@@ -4679,18 +4874,19 @@ function loadSessionIndex() {
|
|
|
4679
4874
|
}
|
|
4680
4875
|
}
|
|
4681
4876
|
function saveSessionIndex(index) {
|
|
4682
|
-
|
|
4877
|
+
const tmp = SESSION_INDEX_FILE + ".tmp";
|
|
4878
|
+
writeFileSync(tmp, JSON.stringify(index, null, 2), "utf-8");
|
|
4879
|
+
renameSync(tmp, SESSION_INDEX_FILE);
|
|
4683
4880
|
}
|
|
4684
4881
|
function saveSession(session) {
|
|
4685
4882
|
const sessionDir = getSessionDir(session.directory, session.sessionId);
|
|
4686
4883
|
if (!existsSync$1(sessionDir)) {
|
|
4687
4884
|
mkdirSync(sessionDir, { recursive: true });
|
|
4688
4885
|
}
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
);
|
|
4886
|
+
const filePath = getSessionFilePath(session.directory, session.sessionId);
|
|
4887
|
+
const tmpPath = filePath + ".tmp";
|
|
4888
|
+
writeFileSync(tmpPath, JSON.stringify(session, null, 2), "utf-8");
|
|
4889
|
+
renameSync(tmpPath, filePath);
|
|
4694
4890
|
const index = loadSessionIndex();
|
|
4695
4891
|
index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
|
|
4696
4892
|
saveSessionIndex(index);
|
|
@@ -4714,6 +4910,16 @@ function deletePersistedSession(sessionId) {
|
|
|
4714
4910
|
if (existsSync$1(configFile)) unlinkSync(configFile);
|
|
4715
4911
|
} catch {
|
|
4716
4912
|
}
|
|
4913
|
+
const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
|
|
4914
|
+
try {
|
|
4915
|
+
if (existsSync$1(ralphStateFile)) unlinkSync(ralphStateFile);
|
|
4916
|
+
} catch {
|
|
4917
|
+
}
|
|
4918
|
+
const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
|
|
4919
|
+
try {
|
|
4920
|
+
if (existsSync$1(ralphProgressFile)) unlinkSync(ralphProgressFile);
|
|
4921
|
+
} catch {
|
|
4922
|
+
}
|
|
4717
4923
|
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
4718
4924
|
try {
|
|
4719
4925
|
rmdirSync(sessionDir);
|
|
@@ -4779,7 +4985,9 @@ function createLogger() {
|
|
|
4779
4985
|
}
|
|
4780
4986
|
function writeDaemonStateFile(state) {
|
|
4781
4987
|
ensureHomeDir();
|
|
4782
|
-
|
|
4988
|
+
const tmpPath = DAEMON_STATE_FILE + ".tmp";
|
|
4989
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
4990
|
+
renameSync(tmpPath, DAEMON_STATE_FILE);
|
|
4783
4991
|
}
|
|
4784
4992
|
function readDaemonStateFile() {
|
|
4785
4993
|
try {
|
|
@@ -5022,6 +5230,7 @@ async function startDaemon(options) {
|
|
|
5022
5230
|
if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
|
|
5023
5231
|
return await spawnAgentSession(sessionId, directory, agentName, options2, resumeSessionId);
|
|
5024
5232
|
}
|
|
5233
|
+
let stagedCredentials = null;
|
|
5025
5234
|
try {
|
|
5026
5235
|
let parseBashPermission2 = function(permission) {
|
|
5027
5236
|
if (permission === "Bash") return;
|
|
@@ -5055,17 +5264,23 @@ async function startDaemon(options) {
|
|
|
5055
5264
|
resolve2();
|
|
5056
5265
|
return;
|
|
5057
5266
|
}
|
|
5267
|
+
let settled = false;
|
|
5268
|
+
const done = () => {
|
|
5269
|
+
if (settled) return;
|
|
5270
|
+
settled = true;
|
|
5271
|
+
clearTimeout(timeout);
|
|
5272
|
+
proc.off("exit", exitHandler);
|
|
5273
|
+
resolve2();
|
|
5274
|
+
};
|
|
5058
5275
|
const timeout = setTimeout(() => {
|
|
5059
5276
|
try {
|
|
5060
5277
|
proc.kill("SIGKILL");
|
|
5061
5278
|
} catch {
|
|
5062
5279
|
}
|
|
5063
|
-
|
|
5280
|
+
done();
|
|
5064
5281
|
}, timeoutMs);
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
resolve2();
|
|
5068
|
-
});
|
|
5282
|
+
const exitHandler = () => done();
|
|
5283
|
+
proc.on("exit", exitHandler);
|
|
5069
5284
|
if (!proc.killed) {
|
|
5070
5285
|
proc.kill(signal);
|
|
5071
5286
|
}
|
|
@@ -5159,6 +5374,8 @@ async function startDaemon(options) {
|
|
|
5159
5374
|
let userMessagePending = false;
|
|
5160
5375
|
let turnInitiatedByUser = true;
|
|
5161
5376
|
let isKillingClaude = false;
|
|
5377
|
+
let isRestartingClaude = false;
|
|
5378
|
+
let isSwitchingMode = false;
|
|
5162
5379
|
let checkSvampConfig;
|
|
5163
5380
|
let cleanupSvampConfig;
|
|
5164
5381
|
const CLAUDE_PERMISSION_MODE_MAP = {
|
|
@@ -5166,7 +5383,6 @@ async function startDaemon(options) {
|
|
|
5166
5383
|
};
|
|
5167
5384
|
const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
|
|
5168
5385
|
let isolationCleanupFiles = [];
|
|
5169
|
-
let stagedCredentials = null;
|
|
5170
5386
|
const spawnClaude = (initialMessage, meta) => {
|
|
5171
5387
|
const effectiveMeta = { ...lastSpawnMeta, ...meta };
|
|
5172
5388
|
let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
@@ -5234,12 +5450,17 @@ async function startDaemon(options) {
|
|
|
5234
5450
|
});
|
|
5235
5451
|
claudeProcess = child;
|
|
5236
5452
|
logger.log(`[Session ${sessionId}] Claude PID: ${child.pid}, stdin: ${!!child.stdin}, stdout: ${!!child.stdout}, stderr: ${!!child.stderr}`);
|
|
5453
|
+
child.stdin?.on("error", (err) => {
|
|
5454
|
+
logger.log(`[Session ${sessionId}] Claude stdin error: ${err.message}`);
|
|
5455
|
+
});
|
|
5237
5456
|
child.on("error", (err) => {
|
|
5238
5457
|
logger.log(`[Session ${sessionId}] Claude process error: ${err.message}`);
|
|
5239
5458
|
sessionService.pushMessage(
|
|
5240
5459
|
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure Claude Code CLI is installed.` },
|
|
5241
5460
|
"event"
|
|
5242
5461
|
);
|
|
5462
|
+
sessionWasProcessing = false;
|
|
5463
|
+
claudeProcess = null;
|
|
5243
5464
|
signalProcessing(false);
|
|
5244
5465
|
sessionService.sendSessionEnd();
|
|
5245
5466
|
});
|
|
@@ -5351,7 +5572,7 @@ async function startDaemon(options) {
|
|
|
5351
5572
|
}
|
|
5352
5573
|
const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
5353
5574
|
if (textBlocks.length > 0) {
|
|
5354
|
-
lastAssistantText
|
|
5575
|
+
lastAssistantText += textBlocks.join("\n");
|
|
5355
5576
|
}
|
|
5356
5577
|
}
|
|
5357
5578
|
if (msg.type === "result") {
|
|
@@ -5389,9 +5610,12 @@ async function startDaemon(options) {
|
|
|
5389
5610
|
turnInitiatedByUser = true;
|
|
5390
5611
|
continue;
|
|
5391
5612
|
}
|
|
5613
|
+
if (msg.session_id) {
|
|
5614
|
+
claudeResumeId = msg.session_id;
|
|
5615
|
+
}
|
|
5392
5616
|
signalProcessing(false);
|
|
5393
5617
|
sessionWasProcessing = false;
|
|
5394
|
-
if (claudeResumeId) {
|
|
5618
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
5395
5619
|
saveSession({
|
|
5396
5620
|
sessionId,
|
|
5397
5621
|
directory,
|
|
@@ -5411,7 +5635,7 @@ async function startDaemon(options) {
|
|
|
5411
5635
|
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
5412
5636
|
}
|
|
5413
5637
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5414
|
-
if (queueLen > 0 && claudeResumeId) {
|
|
5638
|
+
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5415
5639
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
5416
5640
|
} else if (claudeResumeId) {
|
|
5417
5641
|
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
@@ -5435,6 +5659,7 @@ async function startDaemon(options) {
|
|
|
5435
5659
|
logger.log(`[Session ${sessionId}] ${reason}`);
|
|
5436
5660
|
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
5437
5661
|
if (isFreshMode && rlState.original_resume_id) {
|
|
5662
|
+
claudeResumeId = rlState.original_resume_id;
|
|
5438
5663
|
(async () => {
|
|
5439
5664
|
try {
|
|
5440
5665
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
@@ -5442,7 +5667,8 @@ async function startDaemon(options) {
|
|
|
5442
5667
|
await killAndWaitForExit2(claudeProcess);
|
|
5443
5668
|
isKillingClaude = false;
|
|
5444
5669
|
}
|
|
5445
|
-
|
|
5670
|
+
if (trackedSession.stopped) return;
|
|
5671
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5446
5672
|
const progressPath = getRalphProgressFilePath(directory, sessionId);
|
|
5447
5673
|
let resumeMessage;
|
|
5448
5674
|
try {
|
|
@@ -5482,7 +5708,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5482
5708
|
if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
|
|
5483
5709
|
updatedRlState.original_resume_id = claudeResumeId;
|
|
5484
5710
|
}
|
|
5485
|
-
|
|
5711
|
+
try {
|
|
5712
|
+
writeRalphState(getRalphStateFilePath(directory, sessionId), updatedRlState);
|
|
5713
|
+
} catch (writeErr) {
|
|
5714
|
+
logger.log(`[Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
5715
|
+
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
5716
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
5717
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
5718
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
5719
|
+
break;
|
|
5720
|
+
}
|
|
5486
5721
|
const ralphLoop = {
|
|
5487
5722
|
active: true,
|
|
5488
5723
|
task: rlState.task,
|
|
@@ -5502,13 +5737,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5502
5737
|
const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
|
|
5503
5738
|
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
5504
5739
|
if (isFreshMode) {
|
|
5740
|
+
isKillingClaude = true;
|
|
5505
5741
|
setTimeout(async () => {
|
|
5506
5742
|
try {
|
|
5507
5743
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5508
|
-
isKillingClaude = true;
|
|
5509
5744
|
await killAndWaitForExit2(claudeProcess);
|
|
5510
|
-
isKillingClaude = false;
|
|
5511
5745
|
}
|
|
5746
|
+
isKillingClaude = false;
|
|
5747
|
+
if (trackedSession.stopped) return;
|
|
5748
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5512
5749
|
claudeResumeId = void 0;
|
|
5513
5750
|
userMessagePending = true;
|
|
5514
5751
|
turnInitiatedByUser = true;
|
|
@@ -5520,25 +5757,34 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5520
5757
|
} catch (err) {
|
|
5521
5758
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
|
|
5522
5759
|
isKillingClaude = false;
|
|
5760
|
+
sessionWasProcessing = false;
|
|
5523
5761
|
signalProcessing(false);
|
|
5524
5762
|
}
|
|
5525
5763
|
}, cooldownMs);
|
|
5526
5764
|
} else {
|
|
5527
5765
|
setTimeout(() => {
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5766
|
+
if (trackedSession.stopped) return;
|
|
5767
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5768
|
+
try {
|
|
5769
|
+
userMessagePending = true;
|
|
5770
|
+
turnInitiatedByUser = true;
|
|
5771
|
+
sessionWasProcessing = true;
|
|
5772
|
+
signalProcessing(true);
|
|
5773
|
+
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
5774
|
+
sessionService.pushMessage(rlState.task, "user");
|
|
5775
|
+
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5776
|
+
const stdinMsg = JSON.stringify({
|
|
5777
|
+
type: "user",
|
|
5778
|
+
message: { role: "user", content: prompt }
|
|
5779
|
+
});
|
|
5780
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
5781
|
+
} else {
|
|
5782
|
+
spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
|
|
5783
|
+
}
|
|
5784
|
+
} catch (err) {
|
|
5785
|
+
logger.log(`[Session ${sessionId}] Error in continue Ralph iteration: ${err.message}`);
|
|
5786
|
+
sessionWasProcessing = false;
|
|
5787
|
+
signalProcessing(false);
|
|
5542
5788
|
}
|
|
5543
5789
|
}, cooldownMs);
|
|
5544
5790
|
}
|
|
@@ -5553,10 +5799,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5553
5799
|
}
|
|
5554
5800
|
}
|
|
5555
5801
|
sessionService.pushMessage(msg, "agent");
|
|
5556
|
-
if (msg.session_id) {
|
|
5557
|
-
claudeResumeId = msg.session_id;
|
|
5558
|
-
}
|
|
5559
5802
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
5803
|
+
lastAssistantText = "";
|
|
5560
5804
|
if (!userMessagePending) {
|
|
5561
5805
|
turnInitiatedByUser = false;
|
|
5562
5806
|
logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
|
|
@@ -5567,18 +5811,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5567
5811
|
claudeResumeId = msg.session_id;
|
|
5568
5812
|
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
5569
5813
|
sessionService.updateMetadata(sessionMetadata);
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5814
|
+
if (!trackedSession.stopped) {
|
|
5815
|
+
saveSession({
|
|
5816
|
+
sessionId,
|
|
5817
|
+
directory,
|
|
5818
|
+
claudeResumeId,
|
|
5819
|
+
permissionMode: currentPermissionMode,
|
|
5820
|
+
spawnMeta: lastSpawnMeta,
|
|
5821
|
+
metadata: sessionMetadata,
|
|
5822
|
+
createdAt: Date.now(),
|
|
5823
|
+
machineId,
|
|
5824
|
+
wasProcessing: sessionWasProcessing
|
|
5825
|
+
});
|
|
5826
|
+
artifactSync.scheduleDebouncedSync(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId);
|
|
5827
|
+
}
|
|
5582
5828
|
if (isConversationClear) {
|
|
5583
5829
|
logger.log(`[Session ${sessionId}] Conversation cleared (/clear) \u2014 new Claude session: ${msg.session_id}`);
|
|
5584
5830
|
sessionService.clearMessages();
|
|
@@ -5604,6 +5850,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5604
5850
|
}
|
|
5605
5851
|
}
|
|
5606
5852
|
});
|
|
5853
|
+
child.stdout?.on("close", () => {
|
|
5854
|
+
const remaining = stdoutBuffer.trim();
|
|
5855
|
+
if (remaining) {
|
|
5856
|
+
logger.log(`[Session ${sessionId}] stdout close with remaining buffer (${remaining.length} chars): ${remaining.slice(0, 200)}`);
|
|
5857
|
+
try {
|
|
5858
|
+
const msg = JSON.parse(remaining);
|
|
5859
|
+
sessionService.pushMessage(msg, "agent");
|
|
5860
|
+
} catch {
|
|
5861
|
+
logger.log(`[Session ${sessionId}] Discarding non-JSON stdout remainder on close`);
|
|
5862
|
+
}
|
|
5863
|
+
stdoutBuffer = "";
|
|
5864
|
+
}
|
|
5865
|
+
});
|
|
5607
5866
|
let stderrBuffer = "";
|
|
5608
5867
|
child.stderr?.on("data", (chunk) => {
|
|
5609
5868
|
const text = chunk.toString();
|
|
@@ -5633,7 +5892,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5633
5892
|
sessionService.updateMetadata(sessionMetadata);
|
|
5634
5893
|
sessionWasProcessing = false;
|
|
5635
5894
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5636
|
-
if (queueLen > 0 && claudeResumeId) {
|
|
5895
|
+
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5637
5896
|
signalProcessing(false);
|
|
5638
5897
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
5639
5898
|
} else {
|
|
@@ -5667,6 +5926,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5667
5926
|
};
|
|
5668
5927
|
const restartClaudeHandler = async () => {
|
|
5669
5928
|
logger.log(`[Session ${sessionId}] Restart Claude requested`);
|
|
5929
|
+
if (isRestartingClaude || isSwitchingMode) {
|
|
5930
|
+
return { success: false, message: "Restart already in progress." };
|
|
5931
|
+
}
|
|
5932
|
+
isRestartingClaude = true;
|
|
5670
5933
|
try {
|
|
5671
5934
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5672
5935
|
isKillingClaude = true;
|
|
@@ -5675,6 +5938,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5675
5938
|
await killAndWaitForExit2(claudeProcess);
|
|
5676
5939
|
isKillingClaude = false;
|
|
5677
5940
|
}
|
|
5941
|
+
if (trackedSession?.stopped) {
|
|
5942
|
+
return { success: false, message: "Session was stopped during restart." };
|
|
5943
|
+
}
|
|
5678
5944
|
if (claudeResumeId) {
|
|
5679
5945
|
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
5680
5946
|
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
@@ -5687,6 +5953,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5687
5953
|
isKillingClaude = false;
|
|
5688
5954
|
logger.log(`[Session ${sessionId}] Restart failed: ${err.message}`);
|
|
5689
5955
|
return { success: false, message: `Restart failed: ${err.message}` };
|
|
5956
|
+
} finally {
|
|
5957
|
+
isRestartingClaude = false;
|
|
5690
5958
|
}
|
|
5691
5959
|
};
|
|
5692
5960
|
if (sessionMetadata.sharing?.enabled && isolationCapabilities.preferred) {
|
|
@@ -5705,6 +5973,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5705
5973
|
{ controlledByUser: false },
|
|
5706
5974
|
{
|
|
5707
5975
|
onUserMessage: (content, meta) => {
|
|
5976
|
+
if (trackedSession?.stopped) return;
|
|
5708
5977
|
logger.log(`[Session ${sessionId}] User message received`);
|
|
5709
5978
|
userMessagePending = true;
|
|
5710
5979
|
turnInitiatedByUser = true;
|
|
@@ -5731,7 +6000,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5731
6000
|
if (msgMeta) {
|
|
5732
6001
|
lastSpawnMeta = { ...lastSpawnMeta, ...msgMeta };
|
|
5733
6002
|
}
|
|
5734
|
-
if (isKillingClaude) {
|
|
6003
|
+
if (isKillingClaude || isRestartingClaude || isSwitchingMode) {
|
|
5735
6004
|
logger.log(`[Session ${sessionId}] Message received while restarting Claude, queuing to prevent loss`);
|
|
5736
6005
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
5737
6006
|
sessionMetadata = {
|
|
@@ -5785,6 +6054,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5785
6054
|
if (params.mode) {
|
|
5786
6055
|
currentPermissionMode = toClaudePermissionMode(params.mode);
|
|
5787
6056
|
logger.log(`[Session ${sessionId}] Permission mode changed to: ${currentPermissionMode}`);
|
|
6057
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
6058
|
+
saveSession({
|
|
6059
|
+
sessionId,
|
|
6060
|
+
directory,
|
|
6061
|
+
claudeResumeId,
|
|
6062
|
+
permissionMode: currentPermissionMode,
|
|
6063
|
+
spawnMeta: lastSpawnMeta,
|
|
6064
|
+
metadata: sessionMetadata,
|
|
6065
|
+
createdAt: Date.now(),
|
|
6066
|
+
machineId,
|
|
6067
|
+
wasProcessing: sessionWasProcessing
|
|
6068
|
+
});
|
|
6069
|
+
}
|
|
5788
6070
|
}
|
|
5789
6071
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
5790
6072
|
for (const tool of params.allowTools) {
|
|
@@ -5815,12 +6097,23 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5815
6097
|
},
|
|
5816
6098
|
onSwitchMode: async (mode) => {
|
|
5817
6099
|
logger.log(`[Session ${sessionId}] Switch mode: ${mode}`);
|
|
6100
|
+
if (isRestartingClaude || isSwitchingMode) {
|
|
6101
|
+
logger.log(`[Session ${sessionId}] Switch mode deferred \u2014 restart/switch already in progress`);
|
|
6102
|
+
return;
|
|
6103
|
+
}
|
|
5818
6104
|
currentPermissionMode = mode;
|
|
5819
6105
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
6106
|
+
isSwitchingMode = true;
|
|
5820
6107
|
isKillingClaude = true;
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
6108
|
+
try {
|
|
6109
|
+
await killAndWaitForExit2(claudeProcess);
|
|
6110
|
+
isKillingClaude = false;
|
|
6111
|
+
if (trackedSession?.stopped) return;
|
|
6112
|
+
spawnClaude(void 0, { permissionMode: mode });
|
|
6113
|
+
} finally {
|
|
6114
|
+
isKillingClaude = false;
|
|
6115
|
+
isSwitchingMode = false;
|
|
6116
|
+
}
|
|
5824
6117
|
}
|
|
5825
6118
|
},
|
|
5826
6119
|
onRestartClaude: restartClaudeHandler,
|
|
@@ -5841,11 +6134,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5841
6134
|
onMetadataUpdate: (newMeta) => {
|
|
5842
6135
|
sessionMetadata = {
|
|
5843
6136
|
...newMeta,
|
|
6137
|
+
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6138
|
+
lifecycleState: sessionMetadata.lifecycleState,
|
|
6139
|
+
// Preserve claudeSessionId set by 'system init' (frontend may not have it)
|
|
6140
|
+
...sessionMetadata.claudeSessionId ? { claudeSessionId: sessionMetadata.claudeSessionId } : {},
|
|
5844
6141
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
5845
6142
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
5846
6143
|
};
|
|
5847
6144
|
const queue = newMeta.messageQueue;
|
|
5848
|
-
if (queue && queue.length > 0 && !sessionWasProcessing) {
|
|
6145
|
+
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
5849
6146
|
setTimeout(() => {
|
|
5850
6147
|
processMessageQueueRef?.();
|
|
5851
6148
|
}, 200);
|
|
@@ -5882,7 +6179,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5882
6179
|
},
|
|
5883
6180
|
onReadFile: async (path) => {
|
|
5884
6181
|
const resolvedPath = resolve(directory, path);
|
|
5885
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6182
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
5886
6183
|
throw new Error("Path outside working directory");
|
|
5887
6184
|
}
|
|
5888
6185
|
const buffer = await fs.readFile(resolvedPath);
|
|
@@ -5890,7 +6187,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5890
6187
|
},
|
|
5891
6188
|
onWriteFile: async (path, content) => {
|
|
5892
6189
|
const resolvedPath = resolve(directory, path);
|
|
5893
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6190
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
5894
6191
|
throw new Error("Path outside working directory");
|
|
5895
6192
|
}
|
|
5896
6193
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -5898,7 +6195,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5898
6195
|
},
|
|
5899
6196
|
onListDirectory: async (path) => {
|
|
5900
6197
|
const resolvedDir = resolve(directory, path || ".");
|
|
5901
|
-
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
6198
|
+
if (resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
|
|
5902
6199
|
throw new Error("Path outside working directory");
|
|
5903
6200
|
}
|
|
5904
6201
|
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
@@ -5937,6 +6234,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5937
6234
|
}
|
|
5938
6235
|
}
|
|
5939
6236
|
const resolvedPath = resolve(directory, treePath);
|
|
6237
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6238
|
+
throw new Error("Path outside working directory");
|
|
6239
|
+
}
|
|
5940
6240
|
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
5941
6241
|
return { success: !!tree, tree };
|
|
5942
6242
|
}
|
|
@@ -5953,13 +6253,18 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5953
6253
|
},
|
|
5954
6254
|
sessionService,
|
|
5955
6255
|
logger,
|
|
5956
|
-
() =>
|
|
6256
|
+
() => {
|
|
6257
|
+
if (!trackedSession?.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
6258
|
+
}
|
|
5957
6259
|
);
|
|
5958
6260
|
checkSvampConfig = svampConfig.check;
|
|
5959
6261
|
cleanupSvampConfig = svampConfig.cleanup;
|
|
5960
6262
|
const writeSvampConfigPatch = svampConfig.writeConfig;
|
|
5961
6263
|
processMessageQueueRef = () => {
|
|
5962
6264
|
if (sessionWasProcessing) return;
|
|
6265
|
+
if (trackedSession?.stopped) return;
|
|
6266
|
+
if (isKillingClaude) return;
|
|
6267
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5963
6268
|
const queue = sessionMetadata.messageQueue;
|
|
5964
6269
|
if (queue && queue.length > 0) {
|
|
5965
6270
|
const next = queue[0];
|
|
@@ -5988,22 +6293,33 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5988
6293
|
isKillingClaude = true;
|
|
5989
6294
|
await killAndWaitForExit2(claudeProcess);
|
|
5990
6295
|
isKillingClaude = false;
|
|
6296
|
+
if (trackedSession?.stopped) return;
|
|
6297
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5991
6298
|
claudeResumeId = void 0;
|
|
5992
6299
|
spawnClaude(next.text, queueMeta);
|
|
5993
6300
|
} catch (err) {
|
|
5994
6301
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
5995
6302
|
isKillingClaude = false;
|
|
6303
|
+
sessionWasProcessing = false;
|
|
5996
6304
|
signalProcessing(false);
|
|
5997
6305
|
}
|
|
5998
6306
|
})();
|
|
5999
|
-
} else if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6000
|
-
spawnClaude(next.text, queueMeta);
|
|
6001
6307
|
} else {
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6308
|
+
try {
|
|
6309
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6310
|
+
spawnClaude(next.text, queueMeta);
|
|
6311
|
+
} else {
|
|
6312
|
+
const stdinMsg = JSON.stringify({
|
|
6313
|
+
type: "user",
|
|
6314
|
+
message: { role: "user", content: next.text }
|
|
6315
|
+
});
|
|
6316
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6317
|
+
}
|
|
6318
|
+
} catch (err) {
|
|
6319
|
+
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
6320
|
+
sessionWasProcessing = false;
|
|
6321
|
+
signalProcessing(false);
|
|
6322
|
+
}
|
|
6007
6323
|
}
|
|
6008
6324
|
}
|
|
6009
6325
|
};
|
|
@@ -6022,7 +6338,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6022
6338
|
return claudeProcess || void 0;
|
|
6023
6339
|
}
|
|
6024
6340
|
};
|
|
6025
|
-
pidToTrackedSession.set(
|
|
6341
|
+
pidToTrackedSession.set(randomUUID$1(), trackedSession);
|
|
6026
6342
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6027
6343
|
sessionService.updateMetadata(sessionMetadata);
|
|
6028
6344
|
logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
|
|
@@ -6033,6 +6349,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6033
6349
|
};
|
|
6034
6350
|
} catch (err) {
|
|
6035
6351
|
logger.error(`Failed to spawn session: ${err?.message || err}`, err?.stack);
|
|
6352
|
+
if (stagedCredentials) {
|
|
6353
|
+
stagedCredentials.cleanup().catch(() => {
|
|
6354
|
+
});
|
|
6355
|
+
}
|
|
6036
6356
|
return {
|
|
6037
6357
|
type: "error",
|
|
6038
6358
|
errorMessage: `Failed to register session service: ${err?.message || String(err)}`
|
|
@@ -6100,6 +6420,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6100
6420
|
{ controlledByUser: false },
|
|
6101
6421
|
{
|
|
6102
6422
|
onUserMessage: (content, meta) => {
|
|
6423
|
+
if (acpStopped) return;
|
|
6103
6424
|
logger.log(`[${agentName} Session ${sessionId}] User message received`);
|
|
6104
6425
|
let text;
|
|
6105
6426
|
let msgMeta = meta;
|
|
@@ -6120,8 +6441,35 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6120
6441
|
if (msgMeta?.permissionMode) {
|
|
6121
6442
|
currentPermissionMode = msgMeta.permissionMode;
|
|
6122
6443
|
}
|
|
6444
|
+
if (!acpBackendReady) {
|
|
6445
|
+
logger.log(`[${agentName} Session ${sessionId}] Backend not ready \u2014 queuing message`);
|
|
6446
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
6447
|
+
sessionMetadata = {
|
|
6448
|
+
...sessionMetadata,
|
|
6449
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
6450
|
+
};
|
|
6451
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6452
|
+
return;
|
|
6453
|
+
}
|
|
6454
|
+
if (sessionMetadata.lifecycleState === "running") {
|
|
6455
|
+
logger.log(`[${agentName} Session ${sessionId}] Agent busy \u2014 queuing message`);
|
|
6456
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
6457
|
+
sessionMetadata = {
|
|
6458
|
+
...sessionMetadata,
|
|
6459
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
6460
|
+
};
|
|
6461
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6462
|
+
return;
|
|
6463
|
+
}
|
|
6464
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6465
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6123
6466
|
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
6124
6467
|
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
6468
|
+
if (!acpStopped) {
|
|
6469
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6470
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6471
|
+
sessionService.sendSessionEnd();
|
|
6472
|
+
}
|
|
6125
6473
|
});
|
|
6126
6474
|
},
|
|
6127
6475
|
onAbort: () => {
|
|
@@ -6175,18 +6523,27 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6175
6523
|
onMetadataUpdate: (newMeta) => {
|
|
6176
6524
|
sessionMetadata = {
|
|
6177
6525
|
...newMeta,
|
|
6526
|
+
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6527
|
+
lifecycleState: sessionMetadata.lifecycleState,
|
|
6178
6528
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6179
6529
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6180
6530
|
};
|
|
6531
|
+
if (acpStopped) return;
|
|
6181
6532
|
const queue = newMeta.messageQueue;
|
|
6182
6533
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
6183
6534
|
const next = queue[0];
|
|
6184
6535
|
const remaining = queue.slice(1);
|
|
6185
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0 };
|
|
6536
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6186
6537
|
sessionService.updateMetadata(sessionMetadata);
|
|
6187
6538
|
logger.log(`[Session ${sessionId}] Processing queued message from metadata update: "${next.text.slice(0, 50)}..."`);
|
|
6539
|
+
sessionService.sendKeepAlive(true);
|
|
6188
6540
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6189
6541
|
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6542
|
+
if (!acpStopped) {
|
|
6543
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6544
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6545
|
+
sessionService.sendSessionEnd();
|
|
6546
|
+
}
|
|
6190
6547
|
});
|
|
6191
6548
|
}
|
|
6192
6549
|
},
|
|
@@ -6220,7 +6577,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6220
6577
|
},
|
|
6221
6578
|
onReadFile: async (path) => {
|
|
6222
6579
|
const resolvedPath = resolve(directory, path);
|
|
6223
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6580
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6224
6581
|
throw new Error("Path outside working directory");
|
|
6225
6582
|
}
|
|
6226
6583
|
const buffer = await fs.readFile(resolvedPath);
|
|
@@ -6228,7 +6585,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6228
6585
|
},
|
|
6229
6586
|
onWriteFile: async (path, content) => {
|
|
6230
6587
|
const resolvedPath = resolve(directory, path);
|
|
6231
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6588
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6232
6589
|
throw new Error("Path outside working directory");
|
|
6233
6590
|
}
|
|
6234
6591
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -6236,7 +6593,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6236
6593
|
},
|
|
6237
6594
|
onListDirectory: async (path) => {
|
|
6238
6595
|
const resolvedDir = resolve(directory, path || ".");
|
|
6239
|
-
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
6596
|
+
if (resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
|
|
6240
6597
|
throw new Error("Path outside working directory");
|
|
6241
6598
|
}
|
|
6242
6599
|
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
@@ -6275,12 +6632,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6275
6632
|
}
|
|
6276
6633
|
}
|
|
6277
6634
|
const resolvedPath = resolve(directory, treePath);
|
|
6635
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6636
|
+
throw new Error("Path outside working directory");
|
|
6637
|
+
}
|
|
6278
6638
|
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
6279
6639
|
return { success: !!tree, tree };
|
|
6280
6640
|
}
|
|
6281
6641
|
},
|
|
6282
6642
|
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
6283
6643
|
);
|
|
6644
|
+
let insideOnTurnEnd = false;
|
|
6284
6645
|
const svampConfigChecker = createSvampConfigChecker(
|
|
6285
6646
|
directory,
|
|
6286
6647
|
sessionId,
|
|
@@ -6292,6 +6653,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6292
6653
|
sessionService,
|
|
6293
6654
|
logger,
|
|
6294
6655
|
() => {
|
|
6656
|
+
if (acpStopped) return;
|
|
6657
|
+
if (insideOnTurnEnd) return;
|
|
6295
6658
|
const queue = sessionMetadata.messageQueue;
|
|
6296
6659
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
6297
6660
|
const next = queue[0];
|
|
@@ -6302,6 +6665,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6302
6665
|
sessionService.sendKeepAlive(true);
|
|
6303
6666
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6304
6667
|
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6668
|
+
if (!acpStopped) {
|
|
6669
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6670
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6671
|
+
sessionService.sendSessionEnd();
|
|
6672
|
+
}
|
|
6305
6673
|
});
|
|
6306
6674
|
}
|
|
6307
6675
|
}
|
|
@@ -6355,71 +6723,129 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6355
6723
|
isolationConfig: agentIsoConfig
|
|
6356
6724
|
});
|
|
6357
6725
|
}
|
|
6726
|
+
let acpStopped = false;
|
|
6727
|
+
let acpBackendReady = false;
|
|
6358
6728
|
const onTurnEnd = (lastAssistantText) => {
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
promiseFulfilled =
|
|
6729
|
+
if (acpStopped) return;
|
|
6730
|
+
insideOnTurnEnd = true;
|
|
6731
|
+
try {
|
|
6732
|
+
checkSvampConfig?.();
|
|
6733
|
+
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6734
|
+
if (rlState) {
|
|
6735
|
+
let promiseFulfilled = false;
|
|
6736
|
+
if (rlState.completion_promise) {
|
|
6737
|
+
const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
|
|
6738
|
+
promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
|
|
6739
|
+
}
|
|
6740
|
+
const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
|
|
6741
|
+
if (promiseFulfilled || maxReached) {
|
|
6742
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6743
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
6744
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6745
|
+
const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
|
|
6746
|
+
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
6747
|
+
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
6748
|
+
} else {
|
|
6749
|
+
const pendingQueue = sessionMetadata.messageQueue;
|
|
6750
|
+
if (pendingQueue && pendingQueue.length > 0) {
|
|
6751
|
+
const next = pendingQueue[0];
|
|
6752
|
+
const remaining = pendingQueue.slice(1);
|
|
6753
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6754
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6755
|
+
sessionService.sendKeepAlive(true);
|
|
6756
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6757
|
+
logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
|
|
6758
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6759
|
+
logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
|
|
6760
|
+
if (!acpStopped) {
|
|
6761
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6762
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6763
|
+
sessionService.sendSessionEnd();
|
|
6764
|
+
}
|
|
6765
|
+
});
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
const nextIteration = rlState.iteration + 1;
|
|
6769
|
+
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6770
|
+
try {
|
|
6771
|
+
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
6772
|
+
} catch (writeErr) {
|
|
6773
|
+
logger.log(`[${agentName} Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
6774
|
+
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
6775
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6776
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
6777
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
const ralphLoop = {
|
|
6781
|
+
active: true,
|
|
6782
|
+
task: rlState.task,
|
|
6783
|
+
completionPromise: rlState.completion_promise ?? "none",
|
|
6784
|
+
maxIterations: rlState.max_iterations,
|
|
6785
|
+
currentIteration: nextIteration,
|
|
6786
|
+
startedAt: rlState.started_at,
|
|
6787
|
+
cooldownSeconds: rlState.cooldown_seconds,
|
|
6788
|
+
contextMode: rlState.context_mode || "fresh",
|
|
6789
|
+
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6790
|
+
};
|
|
6791
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop, lifecycleState: "running" };
|
|
6792
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6793
|
+
logger.log(`[${agentName} Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning`);
|
|
6794
|
+
const updatedState = { ...rlState, iteration: nextIteration, context_mode: "continue" };
|
|
6795
|
+
const prompt = buildRalphPrompt(rlState.task, updatedState);
|
|
6796
|
+
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
6797
|
+
setTimeout(() => {
|
|
6798
|
+
if (acpStopped) return;
|
|
6799
|
+
const liveRlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6800
|
+
if (!liveRlState) {
|
|
6801
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6802
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6803
|
+
sessionService.sendKeepAlive(false);
|
|
6804
|
+
sessionService.sendSessionEnd();
|
|
6805
|
+
return;
|
|
6806
|
+
}
|
|
6807
|
+
sessionService.sendKeepAlive(true);
|
|
6808
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6809
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6810
|
+
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
6811
|
+
sessionService.pushMessage(rlState.task, "user");
|
|
6812
|
+
agentBackend.sendPrompt(sessionId, prompt).catch((err) => {
|
|
6813
|
+
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
6814
|
+
if (!acpStopped) {
|
|
6815
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6816
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
6817
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6818
|
+
sessionService.sendSessionEnd();
|
|
6819
|
+
sessionService.pushMessage({ type: "message", message: `Ralph loop error: agent failed to start turn \u2014 loop stopped. (${err.message})` }, "event");
|
|
6820
|
+
}
|
|
6821
|
+
});
|
|
6822
|
+
}, cooldownMs);
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6366
6825
|
}
|
|
6367
|
-
const
|
|
6368
|
-
if (
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
|
|
6373
|
-
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
6374
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
6375
|
-
} else {
|
|
6376
|
-
const nextIteration = rlState.iteration + 1;
|
|
6377
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6378
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
6379
|
-
const ralphLoop = {
|
|
6380
|
-
active: true,
|
|
6381
|
-
task: rlState.task,
|
|
6382
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
6383
|
-
maxIterations: rlState.max_iterations,
|
|
6384
|
-
currentIteration: nextIteration,
|
|
6385
|
-
startedAt: rlState.started_at,
|
|
6386
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
6387
|
-
contextMode: rlState.context_mode || "fresh",
|
|
6388
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6389
|
-
};
|
|
6390
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop };
|
|
6826
|
+
const queue = sessionMetadata.messageQueue;
|
|
6827
|
+
if (queue && queue.length > 0) {
|
|
6828
|
+
const next = queue[0];
|
|
6829
|
+
const remaining = queue.slice(1);
|
|
6830
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6391
6831
|
sessionService.updateMetadata(sessionMetadata);
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
6404
|
-
});
|
|
6405
|
-
}, cooldownMs);
|
|
6406
|
-
return;
|
|
6832
|
+
sessionService.sendKeepAlive(true);
|
|
6833
|
+
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
6834
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6835
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6836
|
+
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6837
|
+
if (!acpStopped) {
|
|
6838
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6839
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6840
|
+
sessionService.sendSessionEnd();
|
|
6841
|
+
}
|
|
6842
|
+
});
|
|
6407
6843
|
}
|
|
6408
|
-
}
|
|
6409
|
-
|
|
6410
|
-
if (queue && queue.length > 0) {
|
|
6411
|
-
const next = queue[0];
|
|
6412
|
-
const remaining = queue.slice(1);
|
|
6413
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6414
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
6415
|
-
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
6416
|
-
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6417
|
-
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6418
|
-
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6419
|
-
});
|
|
6844
|
+
} finally {
|
|
6845
|
+
insideOnTurnEnd = false;
|
|
6420
6846
|
}
|
|
6421
6847
|
};
|
|
6422
|
-
bridgeAcpToSession(
|
|
6848
|
+
const cleanupBridge = bridgeAcpToSession(
|
|
6423
6849
|
agentBackend,
|
|
6424
6850
|
sessionService,
|
|
6425
6851
|
() => sessionMetadata,
|
|
@@ -6441,11 +6867,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6441
6867
|
resumeSessionId,
|
|
6442
6868
|
get childProcess() {
|
|
6443
6869
|
return agentBackend.getProcess?.() || void 0;
|
|
6870
|
+
},
|
|
6871
|
+
onStop: () => {
|
|
6872
|
+
acpStopped = true;
|
|
6873
|
+
cleanupBridge();
|
|
6874
|
+
permissionHandler.rejectAll("session stopped");
|
|
6875
|
+
agentBackend.dispose().catch(() => {
|
|
6876
|
+
});
|
|
6444
6877
|
}
|
|
6445
6878
|
};
|
|
6446
|
-
pidToTrackedSession.set(
|
|
6879
|
+
pidToTrackedSession.set(randomUUID$1(), trackedSession);
|
|
6447
6880
|
logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
|
|
6448
6881
|
agentBackend.startSession().then(() => {
|
|
6882
|
+
acpBackendReady = true;
|
|
6449
6883
|
logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
6450
6884
|
}).catch((err) => {
|
|
6451
6885
|
logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
@@ -6454,6 +6888,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6454
6888
|
"event"
|
|
6455
6889
|
);
|
|
6456
6890
|
sessionService.sendSessionEnd();
|
|
6891
|
+
stopSession(sessionId);
|
|
6457
6892
|
});
|
|
6458
6893
|
return {
|
|
6459
6894
|
type: "success",
|
|
@@ -6473,6 +6908,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6473
6908
|
for (const [pid, session] of pidToTrackedSession) {
|
|
6474
6909
|
if (session.svampSessionId === sessionId) {
|
|
6475
6910
|
session.stopped = true;
|
|
6911
|
+
session.onStop?.();
|
|
6476
6912
|
session.hyphaService?.disconnect().catch(() => {
|
|
6477
6913
|
});
|
|
6478
6914
|
if (session.childProcess) {
|
|
@@ -6484,12 +6920,14 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6484
6920
|
session.cleanupCredentials?.().catch(() => {
|
|
6485
6921
|
});
|
|
6486
6922
|
session.cleanupSvampConfig?.();
|
|
6923
|
+
artifactSync.cancelSync(sessionId);
|
|
6487
6924
|
pidToTrackedSession.delete(pid);
|
|
6488
6925
|
deletePersistedSession(sessionId);
|
|
6489
6926
|
logger.log(`Session ${sessionId} stopped`);
|
|
6490
6927
|
return true;
|
|
6491
6928
|
}
|
|
6492
6929
|
}
|
|
6930
|
+
artifactSync.cancelSync(sessionId);
|
|
6493
6931
|
deletePersistedSession(sessionId);
|
|
6494
6932
|
logger.log(`Session ${sessionId} not found in memory, cleaned up persisted state`);
|
|
6495
6933
|
return false;
|
|
@@ -6626,14 +7064,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6626
7064
|
for (const sessionId of sessionsToAutoContinue) {
|
|
6627
7065
|
setTimeout(async () => {
|
|
6628
7066
|
try {
|
|
6629
|
-
const svc = await
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
7067
|
+
const svc = await Promise.race([
|
|
7068
|
+
server.getService(`svamp-session-${sessionId}`),
|
|
7069
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("getService timeout")), 1e4))
|
|
7070
|
+
]);
|
|
7071
|
+
await Promise.race([
|
|
7072
|
+
svc.sendMessage(
|
|
7073
|
+
JSON.stringify({
|
|
7074
|
+
role: "user",
|
|
7075
|
+
content: { type: "text", text: 'The svamp daemon was restarted, which interrupted this session. Please continue where you left off. IMPORTANT: Do not run any command that would stop or restart the svamp daemon (e.g. "svamp daemon stop") \u2014 that would interrupt the session again.' },
|
|
7076
|
+
meta: { sentFrom: "svamp-daemon-auto-continue" }
|
|
7077
|
+
})
|
|
7078
|
+
),
|
|
7079
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("sendMessage timeout")), 3e4))
|
|
7080
|
+
]);
|
|
6637
7081
|
logger.log(`Auto-continued session ${sessionId}`);
|
|
6638
7082
|
} catch (err) {
|
|
6639
7083
|
logger.log(`Failed to auto-continue session ${sessionId}: ${err.message}`);
|
|
@@ -6647,7 +7091,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6647
7091
|
logger.log(`Resuming Ralph loop for ${sessionsToRalphResume.length} session(s)...`);
|
|
6648
7092
|
for (const { sessionId, directory: sessDir } of sessionsToRalphResume) {
|
|
6649
7093
|
try {
|
|
6650
|
-
const svc = await
|
|
7094
|
+
const svc = await Promise.race([
|
|
7095
|
+
server.getService(`svamp-session-${sessionId}`),
|
|
7096
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("getService timeout")), 1e4))
|
|
7097
|
+
]);
|
|
6651
7098
|
const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
|
|
6652
7099
|
if (!rlState) continue;
|
|
6653
7100
|
const initDelayMs = 2e3;
|
|
@@ -6668,17 +7115,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6668
7115
|
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
6669
7116
|
const prompt = buildRalphPrompt(currentState.task, currentState);
|
|
6670
7117
|
const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
|
|
6671
|
-
await
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
7118
|
+
await Promise.race([
|
|
7119
|
+
svc.sendMessage(
|
|
7120
|
+
JSON.stringify({
|
|
7121
|
+
role: "user",
|
|
7122
|
+
content: { type: "text", text: prompt },
|
|
7123
|
+
meta: {
|
|
7124
|
+
sentFrom: "svamp-daemon-ralph-resume",
|
|
7125
|
+
appendSystemPrompt: ralphSysPrompt,
|
|
7126
|
+
...isFreshMode ? { ralphFreshContext: true } : {}
|
|
7127
|
+
}
|
|
7128
|
+
})
|
|
7129
|
+
),
|
|
7130
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("sendMessage timeout")), 3e4))
|
|
7131
|
+
]);
|
|
6682
7132
|
logger.log(`Resumed Ralph loop for session ${sessionId} at iteration ${currentState.iteration} (${isFreshMode ? "fresh" : "continue"})`);
|
|
6683
7133
|
} catch (err) {
|
|
6684
7134
|
logger.log(`Failed to resume Ralph loop for session ${sessionId}: ${err.message}`);
|
|
@@ -6764,9 +7214,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6764
7214
|
process.kill(child.pid, 0);
|
|
6765
7215
|
} catch {
|
|
6766
7216
|
logger.log(`Removing stale session (child PID ${child.pid} dead): ${session.svampSessionId}`);
|
|
7217
|
+
session.stopped = true;
|
|
7218
|
+
session.onStop?.();
|
|
6767
7219
|
session.hyphaService?.disconnect().catch(() => {
|
|
6768
7220
|
});
|
|
7221
|
+
session.cleanupCredentials?.().catch(() => {
|
|
7222
|
+
});
|
|
7223
|
+
session.cleanupSvampConfig?.();
|
|
7224
|
+
if (session.svampSessionId) artifactSync.cancelSync(session.svampSessionId);
|
|
6769
7225
|
pidToTrackedSession.delete(key);
|
|
7226
|
+
if (session.svampSessionId) deletePersistedSession(session.svampSessionId);
|
|
6770
7227
|
}
|
|
6771
7228
|
}
|
|
6772
7229
|
}
|
|
@@ -6834,6 +7291,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6834
7291
|
clearInterval(heartbeatInterval);
|
|
6835
7292
|
if (proxyTokenRefreshInterval) clearInterval(proxyTokenRefreshInterval);
|
|
6836
7293
|
if (unhandledRejectionResetTimer) clearTimeout(unhandledRejectionResetTimer);
|
|
7294
|
+
for (const [, session] of pidToTrackedSession) {
|
|
7295
|
+
session.stopped = true;
|
|
7296
|
+
session.onStop?.();
|
|
7297
|
+
}
|
|
6837
7298
|
machineService.updateDaemonState({
|
|
6838
7299
|
...initialDaemonState,
|
|
6839
7300
|
status: "shutting-down",
|
|
@@ -6841,7 +7302,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6841
7302
|
shutdownSource: source
|
|
6842
7303
|
});
|
|
6843
7304
|
await new Promise((r) => setTimeout(r, 200));
|
|
6844
|
-
for (const [
|
|
7305
|
+
for (const [, session] of pidToTrackedSession) {
|
|
6845
7306
|
session.hyphaService?.disconnect().catch(() => {
|
|
6846
7307
|
});
|
|
6847
7308
|
if (session.childProcess) {
|
|
@@ -6858,14 +7319,21 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6858
7319
|
if (shouldMarkStopped) {
|
|
6859
7320
|
try {
|
|
6860
7321
|
const index = loadSessionIndex();
|
|
7322
|
+
let markedCount = 0;
|
|
6861
7323
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
7324
|
+
try {
|
|
7325
|
+
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
7326
|
+
if (existsSync$1(filePath)) {
|
|
7327
|
+
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
7328
|
+
const tmpPath = filePath + ".tmp";
|
|
7329
|
+
writeFileSync(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
7330
|
+
renameSync(tmpPath, filePath);
|
|
7331
|
+
markedCount++;
|
|
7332
|
+
}
|
|
7333
|
+
} catch {
|
|
6866
7334
|
}
|
|
6867
7335
|
}
|
|
6868
|
-
logger.log(
|
|
7336
|
+
logger.log(`Marked ${markedCount} session(s) as stopped (--cleanup mode)`);
|
|
6869
7337
|
} catch {
|
|
6870
7338
|
}
|
|
6871
7339
|
} else {
|