svamp-cli 0.1.62 → 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-DTNg_sCX.mjs → commands-BfMlD9o4.mjs} +2 -2
- package/dist/{commands-CBTCiCjK.mjs → commands-Brx7D-77.mjs} +1 -1
- package/dist/{commands-DXia-6W4.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-BYxU0Wzp.mjs → package-Dg0hQJRC.mjs} +1 -1
- package/dist/{run-Bds_JVXp.mjs → run-BImPgXHd.mjs} +829 -337
- package/dist/{run-Dg1i7D_o.mjs → run-C7VxH4X8.mjs} +80 -28
- package/dist/{tunnel-CtPReHFY.mjs → tunnel-C3UsqTxi.mjs} +53 -64
- package/package.json +1 -1
|
@@ -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,48 +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
|
-
const push = (update) => {
|
|
553
|
-
pending.push(update);
|
|
554
|
-
wake?.();
|
|
555
|
-
};
|
|
556
|
-
subscribers.add(push);
|
|
557
|
-
console.log(`[HYPHA MACHINE] subscribe() started (total: ${subscribers.size})`);
|
|
558
|
-
try {
|
|
559
|
-
yield {
|
|
560
|
-
type: "update-machine",
|
|
561
|
-
machineId,
|
|
562
|
-
metadata: { value: currentMetadata, version: metadataVersion },
|
|
563
|
-
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
564
|
-
};
|
|
565
|
-
while (true) {
|
|
566
|
-
while (pending.length === 0) {
|
|
567
|
-
await new Promise((r) => {
|
|
568
|
-
wake = r;
|
|
569
|
-
});
|
|
570
|
-
wake = null;
|
|
571
|
-
}
|
|
572
|
-
while (pending.length > 0) {
|
|
573
|
-
yield pending.shift();
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
} finally {
|
|
577
|
-
subscribers.delete(push);
|
|
578
|
-
wake?.();
|
|
579
|
-
console.log(`[HYPHA MACHINE] subscribe() ended (remaining: ${subscribers.size})`);
|
|
580
|
-
}
|
|
582
|
+
listeners.push(callback);
|
|
583
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
581
584
|
},
|
|
582
585
|
// Shell access
|
|
583
586
|
bash: async (command, cwd, context) => {
|
|
@@ -603,7 +606,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
603
606
|
const targetPath = resolve(path || homedir());
|
|
604
607
|
const home = homedir();
|
|
605
608
|
const isOwner = !currentMetadata.sharing?.enabled || context?.user?.email && currentMetadata.sharing.owner && context.user.email.toLowerCase() === currentMetadata.sharing.owner.toLowerCase();
|
|
606
|
-
if (!isOwner && !targetPath.startsWith(home)) {
|
|
609
|
+
if (!isOwner && targetPath !== home && !targetPath.startsWith(home + "/")) {
|
|
607
610
|
throw new Error(`Access denied: path must be within ${home}`);
|
|
608
611
|
}
|
|
609
612
|
const showHidden = options?.showHidden ?? false;
|
|
@@ -644,7 +647,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
644
647
|
},
|
|
645
648
|
/** Add and start a new supervised process. */
|
|
646
649
|
processAdd: async (params, context) => {
|
|
647
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
650
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
648
651
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
649
652
|
return handlers.supervisor.add(params.spec);
|
|
650
653
|
},
|
|
@@ -653,7 +656,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
653
656
|
* Returns { action: 'created'|'updated'|'no-change', info: ProcessInfo }
|
|
654
657
|
*/
|
|
655
658
|
processApply: async (params, context) => {
|
|
656
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
659
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
657
660
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
658
661
|
return handlers.supervisor.apply(params.spec);
|
|
659
662
|
},
|
|
@@ -662,7 +665,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
662
665
|
* Returns updated ProcessInfo.
|
|
663
666
|
*/
|
|
664
667
|
processUpdate: async (params, context) => {
|
|
665
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
668
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
666
669
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
667
670
|
return handlers.supervisor.update(params.idOrName, params.spec);
|
|
668
671
|
},
|
|
@@ -707,7 +710,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
707
710
|
serviceList: async (context) => {
|
|
708
711
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
709
712
|
try {
|
|
710
|
-
const { listServiceGroups } = await import('./api-
|
|
713
|
+
const { listServiceGroups } = await import('./api-BRbsyqJ4.mjs');
|
|
711
714
|
return await listServiceGroups();
|
|
712
715
|
} catch (err) {
|
|
713
716
|
return [];
|
|
@@ -716,13 +719,13 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
716
719
|
/** Get full details of a single service group (includes backends + health). */
|
|
717
720
|
serviceGet: async (params, context) => {
|
|
718
721
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
719
|
-
const { getServiceGroup } = await import('./api-
|
|
722
|
+
const { getServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
720
723
|
return getServiceGroup(params.name);
|
|
721
724
|
},
|
|
722
725
|
/** Delete a service group. */
|
|
723
726
|
serviceDelete: async (params, context) => {
|
|
724
727
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
725
|
-
const { deleteServiceGroup } = await import('./api-
|
|
728
|
+
const { deleteServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
726
729
|
return deleteServiceGroup(params.name);
|
|
727
730
|
},
|
|
728
731
|
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
@@ -733,19 +736,27 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
733
736
|
return { success: false, error: "No OpenAI API key found. Set OPENAI_API_KEY or pass apiKey." };
|
|
734
737
|
}
|
|
735
738
|
try {
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
}
|
|
749
760
|
if (!response.ok) {
|
|
750
761
|
return { success: false, error: `OpenAI API error: ${response.status}` };
|
|
751
762
|
}
|
|
@@ -764,7 +775,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
764
775
|
updateMetadata: (newMetadata) => {
|
|
765
776
|
currentMetadata = newMetadata;
|
|
766
777
|
metadataVersion++;
|
|
767
|
-
|
|
778
|
+
notifyListeners({
|
|
768
779
|
type: "update-machine",
|
|
769
780
|
machineId,
|
|
770
781
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -773,13 +784,17 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
773
784
|
updateDaemonState: (newState) => {
|
|
774
785
|
currentDaemonState = newState;
|
|
775
786
|
daemonStateVersion++;
|
|
776
|
-
|
|
787
|
+
notifyListeners({
|
|
777
788
|
type: "update-machine",
|
|
778
789
|
machineId,
|
|
779
790
|
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
780
791
|
});
|
|
781
792
|
},
|
|
782
793
|
disconnect: async () => {
|
|
794
|
+
const toRemove = [...listeners];
|
|
795
|
+
for (const listener of toRemove) {
|
|
796
|
+
removeListener(listener, "disconnect");
|
|
797
|
+
}
|
|
783
798
|
await server.unregisterService(serviceInfo.id);
|
|
784
799
|
}
|
|
785
800
|
};
|
|
@@ -797,17 +812,21 @@ function loadMessages(messagesDir, sessionId) {
|
|
|
797
812
|
} catch {
|
|
798
813
|
}
|
|
799
814
|
}
|
|
800
|
-
return messages.slice(-
|
|
815
|
+
return messages.slice(-1e3);
|
|
801
816
|
} catch {
|
|
802
817
|
return [];
|
|
803
818
|
}
|
|
804
819
|
}
|
|
805
820
|
function appendMessage(messagesDir, sessionId, msg) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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}`);
|
|
809
829
|
}
|
|
810
|
-
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
811
830
|
}
|
|
812
831
|
async function registerSessionService(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
813
832
|
const messages = options?.messagesDir ? loadMessages(options.messagesDir) : [];
|
|
@@ -822,9 +841,36 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
822
841
|
mode: "remote",
|
|
823
842
|
time: Date.now()
|
|
824
843
|
};
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
|
|
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
|
+
}
|
|
828
874
|
};
|
|
829
875
|
const pushMessage = (content, role = "agent") => {
|
|
830
876
|
let wrappedContent;
|
|
@@ -855,7 +901,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
855
901
|
if (options?.messagesDir) {
|
|
856
902
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
857
903
|
}
|
|
858
|
-
|
|
904
|
+
notifyListeners({
|
|
859
905
|
type: "new-message",
|
|
860
906
|
sessionId,
|
|
861
907
|
message: msg
|
|
@@ -916,7 +962,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
916
962
|
if (options?.messagesDir) {
|
|
917
963
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
918
964
|
}
|
|
919
|
-
|
|
965
|
+
notifyListeners({
|
|
920
966
|
type: "new-message",
|
|
921
967
|
sessionId,
|
|
922
968
|
message: msg
|
|
@@ -943,7 +989,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
943
989
|
}
|
|
944
990
|
metadata = newMetadata;
|
|
945
991
|
metadataVersion++;
|
|
946
|
-
|
|
992
|
+
notifyListeners({
|
|
947
993
|
type: "update-session",
|
|
948
994
|
sessionId,
|
|
949
995
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -961,7 +1007,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
961
1007
|
* Null values remove keys from the config.
|
|
962
1008
|
*/
|
|
963
1009
|
updateConfig: async (patch, context) => {
|
|
964
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1010
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
965
1011
|
callbacks.onUpdateConfig?.(patch);
|
|
966
1012
|
return { success: true };
|
|
967
1013
|
},
|
|
@@ -984,7 +1030,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
984
1030
|
}
|
|
985
1031
|
agentState = newState;
|
|
986
1032
|
agentStateVersion++;
|
|
987
|
-
|
|
1033
|
+
notifyListeners({
|
|
988
1034
|
type: "update-session",
|
|
989
1035
|
sessionId,
|
|
990
1036
|
agentState: { value: agentState, version: agentStateVersion }
|
|
@@ -1007,7 +1053,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1007
1053
|
return { success: true };
|
|
1008
1054
|
},
|
|
1009
1055
|
switchMode: async (mode, context) => {
|
|
1010
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1056
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1011
1057
|
callbacks.onSwitchMode(mode);
|
|
1012
1058
|
return { success: true };
|
|
1013
1059
|
},
|
|
@@ -1022,16 +1068,18 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1022
1068
|
},
|
|
1023
1069
|
// ── Activity ──
|
|
1024
1070
|
keepAlive: async (thinking, mode, context) => {
|
|
1071
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1025
1072
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1026
|
-
|
|
1073
|
+
notifyListeners({
|
|
1027
1074
|
type: "activity",
|
|
1028
1075
|
sessionId,
|
|
1029
1076
|
...lastActivity
|
|
1030
1077
|
});
|
|
1031
1078
|
},
|
|
1032
1079
|
sessionEnd: async (context) => {
|
|
1080
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1033
1081
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1034
|
-
|
|
1082
|
+
notifyListeners({
|
|
1035
1083
|
type: "activity",
|
|
1036
1084
|
sessionId,
|
|
1037
1085
|
...lastActivity
|
|
@@ -1092,6 +1140,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1092
1140
|
},
|
|
1093
1141
|
/** Returns the caller's effective role (null if no access). Does not throw. */
|
|
1094
1142
|
getEffectiveRole: async (context) => {
|
|
1143
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1095
1144
|
const role = getEffectiveRole(context, metadata.sharing);
|
|
1096
1145
|
return { role };
|
|
1097
1146
|
},
|
|
@@ -1105,7 +1154,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1105
1154
|
}
|
|
1106
1155
|
metadata = { ...metadata, sharing: newSharing };
|
|
1107
1156
|
metadataVersion++;
|
|
1108
|
-
|
|
1157
|
+
notifyListeners({
|
|
1109
1158
|
type: "update-session",
|
|
1110
1159
|
sessionId,
|
|
1111
1160
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1123,7 +1172,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1123
1172
|
}
|
|
1124
1173
|
metadata = { ...metadata, securityContext: newSecurityContext };
|
|
1125
1174
|
metadataVersion++;
|
|
1126
|
-
|
|
1175
|
+
notifyListeners({
|
|
1127
1176
|
type: "update-session",
|
|
1128
1177
|
sessionId,
|
|
1129
1178
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1138,55 +1187,69 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1138
1187
|
}
|
|
1139
1188
|
return await callbacks.onApplySystemPrompt(prompt);
|
|
1140
1189
|
},
|
|
1141
|
-
// ──
|
|
1142
|
-
|
|
1143
|
-
// Returns an async generator that yields real-time updates for this session.
|
|
1144
|
-
// hypha-rpc proxies the generator across the RPC boundary — the frontend
|
|
1145
|
-
// iterates with `for await (const update of service.subscribe())`.
|
|
1146
|
-
//
|
|
1147
|
-
// Initial state is replayed as the first batch of yields so the frontend
|
|
1148
|
-
// can reconstruct full session state without a separate RPC call.
|
|
1149
|
-
// Cleanup is automatic: when the frontend disconnects, hypha-rpc calls the
|
|
1150
|
-
// generator's close method, triggering the finally block which removes the
|
|
1151
|
-
// subscriber. No reverse `_rintf` service is registered.
|
|
1152
|
-
subscribe: async function* (context) {
|
|
1190
|
+
// ── Listener Registration ──
|
|
1191
|
+
registerListener: async (callback, context) => {
|
|
1153
1192
|
authorizeRequest(context, metadata.sharing, "view");
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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" };
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (listeners.indexOf(callback) < 0) {
|
|
1225
|
+
return { success: false, error: "Listener was removed during replay" };
|
|
1226
|
+
}
|
|
1162
1227
|
try {
|
|
1163
|
-
|
|
1228
|
+
const result = callback.onUpdate({
|
|
1164
1229
|
type: "update-session",
|
|
1165
1230
|
sessionId,
|
|
1166
1231
|
metadata: { value: metadata, version: metadataVersion },
|
|
1167
1232
|
agentState: { value: agentState, version: agentStateVersion }
|
|
1168
|
-
};
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1233
|
+
});
|
|
1234
|
+
if (result && typeof result.catch === "function") {
|
|
1235
|
+
result.catch(() => {
|
|
1236
|
+
});
|
|
1172
1237
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
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
|
+
});
|
|
1184
1249
|
}
|
|
1185
|
-
}
|
|
1186
|
-
subscribers.delete(push);
|
|
1187
|
-
wake?.();
|
|
1188
|
-
console.log(`[HYPHA SESSION ${sessionId}] subscribe() ended (remaining: ${subscribers.size})`);
|
|
1250
|
+
} catch {
|
|
1189
1251
|
}
|
|
1252
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
1190
1253
|
}
|
|
1191
1254
|
},
|
|
1192
1255
|
{ overwrite: true }
|
|
@@ -1201,7 +1264,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1201
1264
|
updateMetadata: (newMetadata) => {
|
|
1202
1265
|
metadata = newMetadata;
|
|
1203
1266
|
metadataVersion++;
|
|
1204
|
-
|
|
1267
|
+
notifyListeners({
|
|
1205
1268
|
type: "update-session",
|
|
1206
1269
|
sessionId,
|
|
1207
1270
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1210,7 +1273,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1210
1273
|
updateAgentState: (newAgentState) => {
|
|
1211
1274
|
agentState = newAgentState;
|
|
1212
1275
|
agentStateVersion++;
|
|
1213
|
-
|
|
1276
|
+
notifyListeners({
|
|
1214
1277
|
type: "update-session",
|
|
1215
1278
|
sessionId,
|
|
1216
1279
|
agentState: { value: agentState, version: agentStateVersion }
|
|
@@ -1218,7 +1281,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1218
1281
|
},
|
|
1219
1282
|
sendKeepAlive: (thinking, mode) => {
|
|
1220
1283
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1221
|
-
|
|
1284
|
+
notifyListeners({
|
|
1222
1285
|
type: "activity",
|
|
1223
1286
|
sessionId,
|
|
1224
1287
|
...lastActivity
|
|
@@ -1226,7 +1289,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1226
1289
|
},
|
|
1227
1290
|
sendSessionEnd: () => {
|
|
1228
1291
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1229
|
-
|
|
1292
|
+
notifyListeners({
|
|
1230
1293
|
type: "activity",
|
|
1231
1294
|
sessionId,
|
|
1232
1295
|
...lastActivity
|
|
@@ -1242,12 +1305,16 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1242
1305
|
} catch {
|
|
1243
1306
|
}
|
|
1244
1307
|
}
|
|
1245
|
-
|
|
1308
|
+
notifyListeners({
|
|
1246
1309
|
type: "clear-messages",
|
|
1247
1310
|
sessionId
|
|
1248
1311
|
});
|
|
1249
1312
|
},
|
|
1250
1313
|
disconnect: async () => {
|
|
1314
|
+
const toRemove = [...listeners];
|
|
1315
|
+
for (const listener of toRemove) {
|
|
1316
|
+
removeListener(listener, "disconnect");
|
|
1317
|
+
}
|
|
1251
1318
|
await server.unregisterService(serviceInfo.id);
|
|
1252
1319
|
}
|
|
1253
1320
|
};
|
|
@@ -1382,6 +1449,19 @@ class SessionArtifactSync {
|
|
|
1382
1449
|
this.log(`[ARTIFACT SYNC] Created new collection: ${this.collectionId}`);
|
|
1383
1450
|
}
|
|
1384
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
|
+
}
|
|
1385
1465
|
/**
|
|
1386
1466
|
* Upload a file to an artifact using the presigned URL pattern:
|
|
1387
1467
|
* 1. put_file() returns a presigned upload URL
|
|
@@ -1396,11 +1476,11 @@ class SessionArtifactSync {
|
|
|
1396
1476
|
if (!putUrl || typeof putUrl !== "string") {
|
|
1397
1477
|
throw new Error(`put_file returned invalid URL for ${filePath}: ${putUrl}`);
|
|
1398
1478
|
}
|
|
1399
|
-
const resp = await
|
|
1479
|
+
const resp = await this.fetchWithTimeout(putUrl, {
|
|
1400
1480
|
method: "PUT",
|
|
1401
1481
|
body: content,
|
|
1402
1482
|
headers: { "Content-Type": "application/octet-stream" }
|
|
1403
|
-
});
|
|
1483
|
+
}, 12e4);
|
|
1404
1484
|
if (!resp.ok) {
|
|
1405
1485
|
throw new Error(`Upload failed for ${filePath}: ${resp.status} ${resp.statusText}`);
|
|
1406
1486
|
}
|
|
@@ -1417,7 +1497,7 @@ class SessionArtifactSync {
|
|
|
1417
1497
|
_rkwargs: true
|
|
1418
1498
|
});
|
|
1419
1499
|
if (!getUrl || typeof getUrl !== "string") return null;
|
|
1420
|
-
const resp = await
|
|
1500
|
+
const resp = await this.fetchWithTimeout(getUrl, {}, 6e4);
|
|
1421
1501
|
if (!resp.ok) return null;
|
|
1422
1502
|
return await resp.text();
|
|
1423
1503
|
}
|
|
@@ -1435,16 +1515,27 @@ class SessionArtifactSync {
|
|
|
1435
1515
|
const artifactAlias = `session-${sessionId}`;
|
|
1436
1516
|
const sessionJsonPath = join$1(sessionsDir, "session.json");
|
|
1437
1517
|
const messagesPath = join$1(sessionsDir, "messages.jsonl");
|
|
1438
|
-
|
|
1518
|
+
let sessionData = null;
|
|
1519
|
+
if (existsSync(sessionJsonPath)) {
|
|
1520
|
+
try {
|
|
1521
|
+
sessionData = JSON.parse(readFileSync(sessionJsonPath, "utf-8"));
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1439
1525
|
const messagesExist = existsSync(messagesPath);
|
|
1440
1526
|
const messageCount = messagesExist ? readFileSync(messagesPath, "utf-8").split("\n").filter((l) => l.trim()).length : 0;
|
|
1441
1527
|
let artifactId;
|
|
1528
|
+
let existingArtifactId = null;
|
|
1442
1529
|
try {
|
|
1443
1530
|
const existing = await this.artifactManager.read({
|
|
1444
1531
|
artifact_id: artifactAlias,
|
|
1445
1532
|
_rkwargs: true
|
|
1446
1533
|
});
|
|
1447
|
-
|
|
1534
|
+
existingArtifactId = existing.id;
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
if (existingArtifactId) {
|
|
1538
|
+
artifactId = existingArtifactId;
|
|
1448
1539
|
await this.artifactManager.edit({
|
|
1449
1540
|
artifact_id: artifactId,
|
|
1450
1541
|
manifest: {
|
|
@@ -1458,7 +1549,7 @@ class SessionArtifactSync {
|
|
|
1458
1549
|
stage: true,
|
|
1459
1550
|
_rkwargs: true
|
|
1460
1551
|
});
|
|
1461
|
-
}
|
|
1552
|
+
} else {
|
|
1462
1553
|
const artifact = await this.artifactManager.create({
|
|
1463
1554
|
alias: artifactAlias,
|
|
1464
1555
|
parent_id: this.collectionId,
|
|
@@ -1512,6 +1603,16 @@ class SessionArtifactSync {
|
|
|
1512
1603
|
}, delayMs);
|
|
1513
1604
|
this.syncTimers.set(sessionId, timer);
|
|
1514
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
|
+
}
|
|
1515
1616
|
/**
|
|
1516
1617
|
* Download a session from artifact store to local disk.
|
|
1517
1618
|
*/
|
|
@@ -1879,6 +1980,7 @@ function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
|
1879
1980
|
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1880
1981
|
ctx.activeToolCalls.delete(toolCallId);
|
|
1881
1982
|
ctx.toolCallStartTimes.delete(toolCallId);
|
|
1983
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
1882
1984
|
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1883
1985
|
if (timeout) {
|
|
1884
1986
|
clearTimeout(timeout);
|
|
@@ -1888,7 +1990,12 @@ function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
|
1888
1990
|
ctx.emit({ type: "tool-result", toolName: toolKindStr, result: content, callId: toolCallId });
|
|
1889
1991
|
if (ctx.activeToolCalls.size === 0) {
|
|
1890
1992
|
ctx.clearIdleTimeout();
|
|
1891
|
-
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);
|
|
1892
1999
|
}
|
|
1893
2000
|
}
|
|
1894
2001
|
function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
@@ -1897,6 +2004,7 @@ function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
|
1897
2004
|
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1898
2005
|
ctx.activeToolCalls.delete(toolCallId);
|
|
1899
2006
|
ctx.toolCallStartTimes.delete(toolCallId);
|
|
2007
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
1900
2008
|
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1901
2009
|
if (timeout) {
|
|
1902
2010
|
clearTimeout(timeout);
|
|
@@ -1912,7 +2020,12 @@ function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
|
1912
2020
|
});
|
|
1913
2021
|
if (ctx.activeToolCalls.size === 0) {
|
|
1914
2022
|
ctx.clearIdleTimeout();
|
|
1915
|
-
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);
|
|
1916
2029
|
}
|
|
1917
2030
|
}
|
|
1918
2031
|
function handleToolCallUpdate(update, ctx) {
|
|
@@ -2161,10 +2274,22 @@ class AcpBackend {
|
|
|
2161
2274
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2162
2275
|
});
|
|
2163
2276
|
this.process.on("exit", (code, signal) => {
|
|
2164
|
-
if (
|
|
2277
|
+
if (this.disposed) return;
|
|
2278
|
+
if (code !== 0 && code !== null) {
|
|
2165
2279
|
signalStartupFailure(new Error(`Exit code: ${code}`));
|
|
2166
2280
|
this.log(`[ACP] Process exited: code=${code}, signal=${signal}`);
|
|
2167
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();
|
|
2168
2293
|
}
|
|
2169
2294
|
});
|
|
2170
2295
|
const streams = nodeToWebStreams(this.process.stdin, this.process.stdout);
|
|
@@ -2319,12 +2444,14 @@ class AcpBackend {
|
|
|
2319
2444
|
const maybeErr = error;
|
|
2320
2445
|
if (startupFailure && error === startupFailure) return true;
|
|
2321
2446
|
if (maybeErr.code === "ENOENT" || maybeErr.code === "EACCES" || maybeErr.code === "EPIPE") return true;
|
|
2447
|
+
if (maybeErr.code === "DISPOSED") return true;
|
|
2322
2448
|
const msg = error.message.toLowerCase();
|
|
2323
2449
|
if (msg.includes("api key") || msg.includes("not configured") || msg.includes("401") || msg.includes("403")) return true;
|
|
2324
2450
|
return false;
|
|
2325
2451
|
};
|
|
2326
2452
|
await withRetry(
|
|
2327
2453
|
async () => {
|
|
2454
|
+
if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
|
|
2328
2455
|
let timeoutHandle = null;
|
|
2329
2456
|
try {
|
|
2330
2457
|
const result = await Promise.race([
|
|
@@ -2375,6 +2502,7 @@ class AcpBackend {
|
|
|
2375
2502
|
this.log(`[ACP] Creating new session...`);
|
|
2376
2503
|
const sessionResponse = await withRetry(
|
|
2377
2504
|
async () => {
|
|
2505
|
+
if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
|
|
2378
2506
|
let timeoutHandle = null;
|
|
2379
2507
|
try {
|
|
2380
2508
|
const result = await Promise.race([
|
|
@@ -2524,9 +2652,16 @@ class AcpBackend {
|
|
|
2524
2652
|
handleThinkingUpdate(update, ctx);
|
|
2525
2653
|
}
|
|
2526
2654
|
emitIdleStatus() {
|
|
2655
|
+
const resolver = this.idleResolver;
|
|
2656
|
+
this.idleResolver = null;
|
|
2657
|
+
this.waitingForResponse = false;
|
|
2527
2658
|
this.emit({ type: "status", status: "idle" });
|
|
2528
|
-
if (
|
|
2529
|
-
this.
|
|
2659
|
+
if (resolver) {
|
|
2660
|
+
const newPromptInFlight = this.waitingForResponse;
|
|
2661
|
+
resolver();
|
|
2662
|
+
if (newPromptInFlight) {
|
|
2663
|
+
this.waitingForResponse = true;
|
|
2664
|
+
}
|
|
2530
2665
|
}
|
|
2531
2666
|
}
|
|
2532
2667
|
async sendPrompt(sessionId, prompt) {
|
|
@@ -2580,9 +2715,14 @@ class AcpBackend {
|
|
|
2580
2715
|
}
|
|
2581
2716
|
async cancel(sessionId) {
|
|
2582
2717
|
if (!this.connection || !this.acpSessionId) return;
|
|
2718
|
+
this.waitingForResponse = false;
|
|
2719
|
+
if (this.idleResolver) {
|
|
2720
|
+
this.idleResolver();
|
|
2721
|
+
this.idleResolver = null;
|
|
2722
|
+
}
|
|
2583
2723
|
try {
|
|
2584
2724
|
await this.connection.cancel({ sessionId: this.acpSessionId });
|
|
2585
|
-
this.emit({ type: "status", status: "
|
|
2725
|
+
this.emit({ type: "status", status: "cancelled", detail: "Cancelled by user" });
|
|
2586
2726
|
} catch (error) {
|
|
2587
2727
|
this.log("[ACP] Error cancelling:", error);
|
|
2588
2728
|
}
|
|
@@ -2605,16 +2745,24 @@ class AcpBackend {
|
|
|
2605
2745
|
}
|
|
2606
2746
|
}
|
|
2607
2747
|
if (this.process) {
|
|
2608
|
-
|
|
2748
|
+
try {
|
|
2749
|
+
this.process.kill("SIGTERM");
|
|
2750
|
+
} catch {
|
|
2751
|
+
}
|
|
2609
2752
|
await new Promise((resolve) => {
|
|
2610
2753
|
const timeout = setTimeout(() => {
|
|
2611
|
-
|
|
2754
|
+
try {
|
|
2755
|
+
if (this.process) this.process.kill("SIGKILL");
|
|
2756
|
+
} catch {
|
|
2757
|
+
}
|
|
2612
2758
|
resolve();
|
|
2613
2759
|
}, 1e3);
|
|
2614
|
-
|
|
2760
|
+
const done = () => {
|
|
2615
2761
|
clearTimeout(timeout);
|
|
2616
2762
|
resolve();
|
|
2617
|
-
}
|
|
2763
|
+
};
|
|
2764
|
+
this.process?.once("exit", done);
|
|
2765
|
+
this.process?.once("close", done);
|
|
2618
2766
|
});
|
|
2619
2767
|
this.process = null;
|
|
2620
2768
|
}
|
|
@@ -2629,6 +2777,7 @@ class AcpBackend {
|
|
|
2629
2777
|
for (const timeout of this.toolCallTimeouts.values()) clearTimeout(timeout);
|
|
2630
2778
|
this.toolCallTimeouts.clear();
|
|
2631
2779
|
this.toolCallStartTimes.clear();
|
|
2780
|
+
this.toolCallIdToNameMap.clear();
|
|
2632
2781
|
}
|
|
2633
2782
|
}
|
|
2634
2783
|
|
|
@@ -2694,6 +2843,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2694
2843
|
let pendingText = "";
|
|
2695
2844
|
let turnText = "";
|
|
2696
2845
|
let flushTimer = null;
|
|
2846
|
+
let bridgeStopped = false;
|
|
2697
2847
|
function flushText() {
|
|
2698
2848
|
if (pendingText) {
|
|
2699
2849
|
sessionService.pushMessage({
|
|
@@ -2708,6 +2858,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2708
2858
|
}
|
|
2709
2859
|
}
|
|
2710
2860
|
backend.onMessage((msg) => {
|
|
2861
|
+
if (bridgeStopped) return;
|
|
2711
2862
|
switch (msg.type) {
|
|
2712
2863
|
case "model-output": {
|
|
2713
2864
|
if (msg.textDelta) {
|
|
@@ -2739,6 +2890,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2739
2890
|
setMetadata((m) => ({ ...m, lifecycleState: "running" }));
|
|
2740
2891
|
} else if (msg.status === "error") {
|
|
2741
2892
|
flushText();
|
|
2893
|
+
turnText = "";
|
|
2742
2894
|
sessionService.pushMessage(
|
|
2743
2895
|
{ type: "message", message: `Agent process exited unexpectedly: ${msg.detail || "Unknown error"}` },
|
|
2744
2896
|
"event"
|
|
@@ -2747,8 +2899,12 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2747
2899
|
setMetadata((m) => ({ ...m, lifecycleState: "error" }));
|
|
2748
2900
|
} else if (msg.status === "stopped") {
|
|
2749
2901
|
flushText();
|
|
2902
|
+
turnText = "";
|
|
2750
2903
|
sessionService.sendSessionEnd();
|
|
2751
2904
|
setMetadata((m) => ({ ...m, lifecycleState: "stopped" }));
|
|
2905
|
+
} else if (msg.status === "cancelled") {
|
|
2906
|
+
flushText();
|
|
2907
|
+
turnText = "";
|
|
2752
2908
|
}
|
|
2753
2909
|
break;
|
|
2754
2910
|
}
|
|
@@ -2829,6 +2985,14 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2829
2985
|
}
|
|
2830
2986
|
}
|
|
2831
2987
|
});
|
|
2988
|
+
return () => {
|
|
2989
|
+
bridgeStopped = true;
|
|
2990
|
+
if (flushTimer) {
|
|
2991
|
+
clearTimeout(flushTimer);
|
|
2992
|
+
flushTimer = null;
|
|
2993
|
+
}
|
|
2994
|
+
pendingText = "";
|
|
2995
|
+
};
|
|
2832
2996
|
}
|
|
2833
2997
|
class HyphaPermissionHandler {
|
|
2834
2998
|
constructor(shouldAutoAllow, log) {
|
|
@@ -2894,6 +3058,10 @@ class CodexMcpBackend {
|
|
|
2894
3058
|
client;
|
|
2895
3059
|
transport = null;
|
|
2896
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
|
|
2897
3065
|
codexSessionId = null;
|
|
2898
3066
|
conversationId = null;
|
|
2899
3067
|
svampSessionId = null;
|
|
@@ -2945,7 +3113,10 @@ class CodexMcpBackend {
|
|
|
2945
3113
|
}
|
|
2946
3114
|
async sendPrompt(sessionId, prompt) {
|
|
2947
3115
|
if (!this.connected) throw new Error("Codex not connected");
|
|
3116
|
+
this.turnCancelled = false;
|
|
3117
|
+
const myTurnId = ++this.turnId;
|
|
2948
3118
|
this.emit({ type: "status", status: "running" });
|
|
3119
|
+
let hadError = false;
|
|
2949
3120
|
try {
|
|
2950
3121
|
let response;
|
|
2951
3122
|
if (this.codexSessionId) {
|
|
@@ -2966,16 +3137,20 @@ class CodexMcpBackend {
|
|
|
2966
3137
|
}
|
|
2967
3138
|
}
|
|
2968
3139
|
} catch (err) {
|
|
3140
|
+
hadError = true;
|
|
2969
3141
|
this.log(`[Codex] Error in sendPrompt: ${err.message}`);
|
|
2970
3142
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2971
3143
|
throw err;
|
|
2972
3144
|
} finally {
|
|
2973
|
-
this.
|
|
3145
|
+
if (!this.turnCancelled && !hadError && this.turnId === myTurnId) {
|
|
3146
|
+
this.emit({ type: "status", status: "idle" });
|
|
3147
|
+
}
|
|
2974
3148
|
}
|
|
2975
3149
|
}
|
|
2976
3150
|
async cancel(_sessionId) {
|
|
2977
3151
|
this.log("[Codex] Cancel requested");
|
|
2978
|
-
this.
|
|
3152
|
+
this.turnCancelled = true;
|
|
3153
|
+
this.emit({ type: "status", status: "cancelled" });
|
|
2979
3154
|
}
|
|
2980
3155
|
async respondToPermission(requestId, approved) {
|
|
2981
3156
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -3170,8 +3345,8 @@ class CodexMcpBackend {
|
|
|
3170
3345
|
this.emit({ type: "status", status: "running" });
|
|
3171
3346
|
break;
|
|
3172
3347
|
case "task_complete":
|
|
3348
|
+
break;
|
|
3173
3349
|
case "turn_aborted":
|
|
3174
|
-
this.emit({ type: "status", status: "idle" });
|
|
3175
3350
|
break;
|
|
3176
3351
|
case "agent_message": {
|
|
3177
3352
|
const content = event.content;
|
|
@@ -3803,7 +3978,10 @@ class ProcessSupervisor {
|
|
|
3803
3978
|
/** Start a stopped/failed process by id or name. */
|
|
3804
3979
|
async start(idOrName) {
|
|
3805
3980
|
const entry = this.require(idOrName);
|
|
3806
|
-
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
|
+
}
|
|
3807
3985
|
entry.stopping = false;
|
|
3808
3986
|
await this.startEntry(entry, false);
|
|
3809
3987
|
}
|
|
@@ -3823,15 +4001,21 @@ class ProcessSupervisor {
|
|
|
3823
4001
|
/** Restart a process (stop if running, then start again). */
|
|
3824
4002
|
async restart(idOrName) {
|
|
3825
4003
|
const entry = this.require(idOrName);
|
|
3826
|
-
if (entry.
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
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;
|
|
3831
4018
|
}
|
|
3832
|
-
entry.stopping = false;
|
|
3833
|
-
entry.state.restartCount++;
|
|
3834
|
-
await this.startEntry(entry, false);
|
|
3835
4019
|
}
|
|
3836
4020
|
/** Stop the process and remove it from supervision (deletes persisted spec). */
|
|
3837
4021
|
async remove(idOrName) {
|
|
@@ -3965,7 +4149,9 @@ class ProcessSupervisor {
|
|
|
3965
4149
|
}
|
|
3966
4150
|
async persistSpec(spec) {
|
|
3967
4151
|
const filePath = path.join(this.persistDir, `${spec.id}.json`);
|
|
3968
|
-
|
|
4152
|
+
const tmpPath = filePath + ".tmp";
|
|
4153
|
+
await writeFile(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4154
|
+
await rename(tmpPath, filePath);
|
|
3969
4155
|
}
|
|
3970
4156
|
async deleteSpec(id) {
|
|
3971
4157
|
try {
|
|
@@ -4039,7 +4225,13 @@ class ProcessSupervisor {
|
|
|
4039
4225
|
};
|
|
4040
4226
|
child.stdout?.on("data", appendLog);
|
|
4041
4227
|
child.stderr?.on("data", appendLog);
|
|
4042
|
-
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
|
+
});
|
|
4043
4235
|
if (spec.probe) this.setupProbe(entry);
|
|
4044
4236
|
if (spec.ttl !== void 0) this.setupTTL(entry);
|
|
4045
4237
|
console.log(`[SUPERVISOR] Started '${spec.name}' pid=${child.pid}`);
|
|
@@ -4127,6 +4319,8 @@ class ProcessSupervisor {
|
|
|
4127
4319
|
}
|
|
4128
4320
|
}
|
|
4129
4321
|
async triggerProbeRestart(entry) {
|
|
4322
|
+
if (entry.restarting) return;
|
|
4323
|
+
if (entry.stopping) return;
|
|
4130
4324
|
console.warn(`[SUPERVISOR] Restarting '${entry.spec.name}' due to probe failures`);
|
|
4131
4325
|
entry.state.consecutiveProbeFailures = 0;
|
|
4132
4326
|
this.clearTimers(entry);
|
|
@@ -4151,6 +4345,7 @@ class ProcessSupervisor {
|
|
|
4151
4345
|
console.log(`[SUPERVISOR] Process '${entry.spec.name}' TTL expired`);
|
|
4152
4346
|
entry.state.status = "expired";
|
|
4153
4347
|
entry.stopping = true;
|
|
4348
|
+
this.clearTimers(entry);
|
|
4154
4349
|
const cleanup = async () => {
|
|
4155
4350
|
if (entry.child) await this.killChild(entry.child);
|
|
4156
4351
|
this.entries.delete(entry.spec.id);
|
|
@@ -4162,13 +4357,36 @@ class ProcessSupervisor {
|
|
|
4162
4357
|
// ── Process kill helper ───────────────────────────────────────────────────
|
|
4163
4358
|
killChild(child) {
|
|
4164
4359
|
return new Promise((resolve) => {
|
|
4165
|
-
|
|
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
|
+
};
|
|
4166
4371
|
child.once("exit", done);
|
|
4167
|
-
child.
|
|
4168
|
-
|
|
4169
|
-
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);
|
|
4170
4389
|
}, 5e3);
|
|
4171
|
-
child.once("exit", () => clearTimeout(forceKill));
|
|
4172
4390
|
});
|
|
4173
4391
|
}
|
|
4174
4392
|
// ── Timer cleanup ─────────────────────────────────────────────────────────
|
|
@@ -4266,7 +4484,9 @@ function readSvampConfig(configPath) {
|
|
|
4266
4484
|
function writeSvampConfig(configPath, config) {
|
|
4267
4485
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
4268
4486
|
const content = JSON.stringify(config, null, 2);
|
|
4269
|
-
|
|
4487
|
+
const tmpPath = configPath + ".tmp";
|
|
4488
|
+
writeFileSync(tmpPath, content);
|
|
4489
|
+
renameSync(tmpPath, configPath);
|
|
4270
4490
|
return content;
|
|
4271
4491
|
}
|
|
4272
4492
|
function getRalphStateFilePath(directory, sessionId) {
|
|
@@ -4320,7 +4540,9 @@ started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResu
|
|
|
4320
4540
|
|
|
4321
4541
|
${state.task}
|
|
4322
4542
|
`;
|
|
4323
|
-
|
|
4543
|
+
const tmpPath = `${filePath}.tmp`;
|
|
4544
|
+
writeFileSync(tmpPath, content);
|
|
4545
|
+
renameSync(tmpPath, filePath);
|
|
4324
4546
|
}
|
|
4325
4547
|
function removeRalphState(filePath) {
|
|
4326
4548
|
try {
|
|
@@ -4409,20 +4631,17 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
4409
4631
|
ralphSystemPrompt: ralphSysPrompt
|
|
4410
4632
|
}]
|
|
4411
4633
|
}));
|
|
4412
|
-
sessionService.updateMetadata(getMetadata());
|
|
4413
4634
|
sessionService.pushMessage(
|
|
4414
|
-
{ 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) },
|
|
4415
4636
|
"event"
|
|
4416
4637
|
);
|
|
4417
|
-
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)}..."`);
|
|
4418
4639
|
onRalphLoopActivated?.();
|
|
4419
4640
|
} else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
|
|
4420
4641
|
setMetadata((m) => ({ ...m, ralphLoop }));
|
|
4421
|
-
sessionService.updateMetadata(getMetadata());
|
|
4422
4642
|
}
|
|
4423
4643
|
} else if (prevRalph?.active) {
|
|
4424
4644
|
setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
|
|
4425
|
-
sessionService.updateMetadata(getMetadata());
|
|
4426
4645
|
sessionService.pushMessage(
|
|
4427
4646
|
{ type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
|
|
4428
4647
|
"event"
|
|
@@ -4655,18 +4874,19 @@ function loadSessionIndex() {
|
|
|
4655
4874
|
}
|
|
4656
4875
|
}
|
|
4657
4876
|
function saveSessionIndex(index) {
|
|
4658
|
-
|
|
4877
|
+
const tmp = SESSION_INDEX_FILE + ".tmp";
|
|
4878
|
+
writeFileSync(tmp, JSON.stringify(index, null, 2), "utf-8");
|
|
4879
|
+
renameSync(tmp, SESSION_INDEX_FILE);
|
|
4659
4880
|
}
|
|
4660
4881
|
function saveSession(session) {
|
|
4661
4882
|
const sessionDir = getSessionDir(session.directory, session.sessionId);
|
|
4662
4883
|
if (!existsSync$1(sessionDir)) {
|
|
4663
4884
|
mkdirSync(sessionDir, { recursive: true });
|
|
4664
4885
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
);
|
|
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);
|
|
4670
4890
|
const index = loadSessionIndex();
|
|
4671
4891
|
index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
|
|
4672
4892
|
saveSessionIndex(index);
|
|
@@ -4690,6 +4910,16 @@ function deletePersistedSession(sessionId) {
|
|
|
4690
4910
|
if (existsSync$1(configFile)) unlinkSync(configFile);
|
|
4691
4911
|
} catch {
|
|
4692
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
|
+
}
|
|
4693
4923
|
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
4694
4924
|
try {
|
|
4695
4925
|
rmdirSync(sessionDir);
|
|
@@ -4755,7 +4985,9 @@ function createLogger() {
|
|
|
4755
4985
|
}
|
|
4756
4986
|
function writeDaemonStateFile(state) {
|
|
4757
4987
|
ensureHomeDir();
|
|
4758
|
-
|
|
4988
|
+
const tmpPath = DAEMON_STATE_FILE + ".tmp";
|
|
4989
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
4990
|
+
renameSync(tmpPath, DAEMON_STATE_FILE);
|
|
4759
4991
|
}
|
|
4760
4992
|
function readDaemonStateFile() {
|
|
4761
4993
|
try {
|
|
@@ -4998,6 +5230,7 @@ async function startDaemon(options) {
|
|
|
4998
5230
|
if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
|
|
4999
5231
|
return await spawnAgentSession(sessionId, directory, agentName, options2, resumeSessionId);
|
|
5000
5232
|
}
|
|
5233
|
+
let stagedCredentials = null;
|
|
5001
5234
|
try {
|
|
5002
5235
|
let parseBashPermission2 = function(permission) {
|
|
5003
5236
|
if (permission === "Bash") return;
|
|
@@ -5031,17 +5264,23 @@ async function startDaemon(options) {
|
|
|
5031
5264
|
resolve2();
|
|
5032
5265
|
return;
|
|
5033
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
|
+
};
|
|
5034
5275
|
const timeout = setTimeout(() => {
|
|
5035
5276
|
try {
|
|
5036
5277
|
proc.kill("SIGKILL");
|
|
5037
5278
|
} catch {
|
|
5038
5279
|
}
|
|
5039
|
-
|
|
5280
|
+
done();
|
|
5040
5281
|
}, timeoutMs);
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
resolve2();
|
|
5044
|
-
});
|
|
5282
|
+
const exitHandler = () => done();
|
|
5283
|
+
proc.on("exit", exitHandler);
|
|
5045
5284
|
if (!proc.killed) {
|
|
5046
5285
|
proc.kill(signal);
|
|
5047
5286
|
}
|
|
@@ -5135,6 +5374,8 @@ async function startDaemon(options) {
|
|
|
5135
5374
|
let userMessagePending = false;
|
|
5136
5375
|
let turnInitiatedByUser = true;
|
|
5137
5376
|
let isKillingClaude = false;
|
|
5377
|
+
let isRestartingClaude = false;
|
|
5378
|
+
let isSwitchingMode = false;
|
|
5138
5379
|
let checkSvampConfig;
|
|
5139
5380
|
let cleanupSvampConfig;
|
|
5140
5381
|
const CLAUDE_PERMISSION_MODE_MAP = {
|
|
@@ -5142,7 +5383,6 @@ async function startDaemon(options) {
|
|
|
5142
5383
|
};
|
|
5143
5384
|
const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
|
|
5144
5385
|
let isolationCleanupFiles = [];
|
|
5145
|
-
let stagedCredentials = null;
|
|
5146
5386
|
const spawnClaude = (initialMessage, meta) => {
|
|
5147
5387
|
const effectiveMeta = { ...lastSpawnMeta, ...meta };
|
|
5148
5388
|
let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
@@ -5210,12 +5450,17 @@ async function startDaemon(options) {
|
|
|
5210
5450
|
});
|
|
5211
5451
|
claudeProcess = child;
|
|
5212
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
|
+
});
|
|
5213
5456
|
child.on("error", (err) => {
|
|
5214
5457
|
logger.log(`[Session ${sessionId}] Claude process error: ${err.message}`);
|
|
5215
5458
|
sessionService.pushMessage(
|
|
5216
5459
|
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure Claude Code CLI is installed.` },
|
|
5217
5460
|
"event"
|
|
5218
5461
|
);
|
|
5462
|
+
sessionWasProcessing = false;
|
|
5463
|
+
claudeProcess = null;
|
|
5219
5464
|
signalProcessing(false);
|
|
5220
5465
|
sessionService.sendSessionEnd();
|
|
5221
5466
|
});
|
|
@@ -5327,7 +5572,7 @@ async function startDaemon(options) {
|
|
|
5327
5572
|
}
|
|
5328
5573
|
const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
5329
5574
|
if (textBlocks.length > 0) {
|
|
5330
|
-
lastAssistantText
|
|
5575
|
+
lastAssistantText += textBlocks.join("\n");
|
|
5331
5576
|
}
|
|
5332
5577
|
}
|
|
5333
5578
|
if (msg.type === "result") {
|
|
@@ -5365,9 +5610,12 @@ async function startDaemon(options) {
|
|
|
5365
5610
|
turnInitiatedByUser = true;
|
|
5366
5611
|
continue;
|
|
5367
5612
|
}
|
|
5613
|
+
if (msg.session_id) {
|
|
5614
|
+
claudeResumeId = msg.session_id;
|
|
5615
|
+
}
|
|
5368
5616
|
signalProcessing(false);
|
|
5369
5617
|
sessionWasProcessing = false;
|
|
5370
|
-
if (claudeResumeId) {
|
|
5618
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
5371
5619
|
saveSession({
|
|
5372
5620
|
sessionId,
|
|
5373
5621
|
directory,
|
|
@@ -5387,7 +5635,7 @@ async function startDaemon(options) {
|
|
|
5387
5635
|
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
5388
5636
|
}
|
|
5389
5637
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5390
|
-
if (queueLen > 0 && claudeResumeId) {
|
|
5638
|
+
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5391
5639
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
5392
5640
|
} else if (claudeResumeId) {
|
|
5393
5641
|
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
@@ -5411,6 +5659,7 @@ async function startDaemon(options) {
|
|
|
5411
5659
|
logger.log(`[Session ${sessionId}] ${reason}`);
|
|
5412
5660
|
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
5413
5661
|
if (isFreshMode && rlState.original_resume_id) {
|
|
5662
|
+
claudeResumeId = rlState.original_resume_id;
|
|
5414
5663
|
(async () => {
|
|
5415
5664
|
try {
|
|
5416
5665
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
@@ -5418,7 +5667,8 @@ async function startDaemon(options) {
|
|
|
5418
5667
|
await killAndWaitForExit2(claudeProcess);
|
|
5419
5668
|
isKillingClaude = false;
|
|
5420
5669
|
}
|
|
5421
|
-
|
|
5670
|
+
if (trackedSession.stopped) return;
|
|
5671
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5422
5672
|
const progressPath = getRalphProgressFilePath(directory, sessionId);
|
|
5423
5673
|
let resumeMessage;
|
|
5424
5674
|
try {
|
|
@@ -5458,7 +5708,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5458
5708
|
if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
|
|
5459
5709
|
updatedRlState.original_resume_id = claudeResumeId;
|
|
5460
5710
|
}
|
|
5461
|
-
|
|
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
|
+
}
|
|
5462
5721
|
const ralphLoop = {
|
|
5463
5722
|
active: true,
|
|
5464
5723
|
task: rlState.task,
|
|
@@ -5478,13 +5737,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5478
5737
|
const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
|
|
5479
5738
|
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
5480
5739
|
if (isFreshMode) {
|
|
5740
|
+
isKillingClaude = true;
|
|
5481
5741
|
setTimeout(async () => {
|
|
5482
5742
|
try {
|
|
5483
5743
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5484
|
-
isKillingClaude = true;
|
|
5485
5744
|
await killAndWaitForExit2(claudeProcess);
|
|
5486
|
-
isKillingClaude = false;
|
|
5487
5745
|
}
|
|
5746
|
+
isKillingClaude = false;
|
|
5747
|
+
if (trackedSession.stopped) return;
|
|
5748
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5488
5749
|
claudeResumeId = void 0;
|
|
5489
5750
|
userMessagePending = true;
|
|
5490
5751
|
turnInitiatedByUser = true;
|
|
@@ -5496,25 +5757,34 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5496
5757
|
} catch (err) {
|
|
5497
5758
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
|
|
5498
5759
|
isKillingClaude = false;
|
|
5760
|
+
sessionWasProcessing = false;
|
|
5499
5761
|
signalProcessing(false);
|
|
5500
5762
|
}
|
|
5501
5763
|
}, cooldownMs);
|
|
5502
5764
|
} else {
|
|
5503
5765
|
setTimeout(() => {
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
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);
|
|
5518
5788
|
}
|
|
5519
5789
|
}, cooldownMs);
|
|
5520
5790
|
}
|
|
@@ -5529,10 +5799,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5529
5799
|
}
|
|
5530
5800
|
}
|
|
5531
5801
|
sessionService.pushMessage(msg, "agent");
|
|
5532
|
-
if (msg.session_id) {
|
|
5533
|
-
claudeResumeId = msg.session_id;
|
|
5534
|
-
}
|
|
5535
5802
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
5803
|
+
lastAssistantText = "";
|
|
5536
5804
|
if (!userMessagePending) {
|
|
5537
5805
|
turnInitiatedByUser = false;
|
|
5538
5806
|
logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
|
|
@@ -5543,18 +5811,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5543
5811
|
claudeResumeId = msg.session_id;
|
|
5544
5812
|
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
5545
5813
|
sessionService.updateMetadata(sessionMetadata);
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
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
|
+
}
|
|
5558
5828
|
if (isConversationClear) {
|
|
5559
5829
|
logger.log(`[Session ${sessionId}] Conversation cleared (/clear) \u2014 new Claude session: ${msg.session_id}`);
|
|
5560
5830
|
sessionService.clearMessages();
|
|
@@ -5580,6 +5850,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5580
5850
|
}
|
|
5581
5851
|
}
|
|
5582
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
|
+
});
|
|
5583
5866
|
let stderrBuffer = "";
|
|
5584
5867
|
child.stderr?.on("data", (chunk) => {
|
|
5585
5868
|
const text = chunk.toString();
|
|
@@ -5609,7 +5892,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5609
5892
|
sessionService.updateMetadata(sessionMetadata);
|
|
5610
5893
|
sessionWasProcessing = false;
|
|
5611
5894
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5612
|
-
if (queueLen > 0 && claudeResumeId) {
|
|
5895
|
+
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5613
5896
|
signalProcessing(false);
|
|
5614
5897
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
5615
5898
|
} else {
|
|
@@ -5643,6 +5926,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5643
5926
|
};
|
|
5644
5927
|
const restartClaudeHandler = async () => {
|
|
5645
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;
|
|
5646
5933
|
try {
|
|
5647
5934
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5648
5935
|
isKillingClaude = true;
|
|
@@ -5651,6 +5938,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5651
5938
|
await killAndWaitForExit2(claudeProcess);
|
|
5652
5939
|
isKillingClaude = false;
|
|
5653
5940
|
}
|
|
5941
|
+
if (trackedSession?.stopped) {
|
|
5942
|
+
return { success: false, message: "Session was stopped during restart." };
|
|
5943
|
+
}
|
|
5654
5944
|
if (claudeResumeId) {
|
|
5655
5945
|
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
5656
5946
|
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
@@ -5663,6 +5953,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5663
5953
|
isKillingClaude = false;
|
|
5664
5954
|
logger.log(`[Session ${sessionId}] Restart failed: ${err.message}`);
|
|
5665
5955
|
return { success: false, message: `Restart failed: ${err.message}` };
|
|
5956
|
+
} finally {
|
|
5957
|
+
isRestartingClaude = false;
|
|
5666
5958
|
}
|
|
5667
5959
|
};
|
|
5668
5960
|
if (sessionMetadata.sharing?.enabled && isolationCapabilities.preferred) {
|
|
@@ -5681,6 +5973,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5681
5973
|
{ controlledByUser: false },
|
|
5682
5974
|
{
|
|
5683
5975
|
onUserMessage: (content, meta) => {
|
|
5976
|
+
if (trackedSession?.stopped) return;
|
|
5684
5977
|
logger.log(`[Session ${sessionId}] User message received`);
|
|
5685
5978
|
userMessagePending = true;
|
|
5686
5979
|
turnInitiatedByUser = true;
|
|
@@ -5707,7 +6000,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5707
6000
|
if (msgMeta) {
|
|
5708
6001
|
lastSpawnMeta = { ...lastSpawnMeta, ...msgMeta };
|
|
5709
6002
|
}
|
|
5710
|
-
if (isKillingClaude) {
|
|
6003
|
+
if (isKillingClaude || isRestartingClaude || isSwitchingMode) {
|
|
5711
6004
|
logger.log(`[Session ${sessionId}] Message received while restarting Claude, queuing to prevent loss`);
|
|
5712
6005
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
5713
6006
|
sessionMetadata = {
|
|
@@ -5761,6 +6054,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5761
6054
|
if (params.mode) {
|
|
5762
6055
|
currentPermissionMode = toClaudePermissionMode(params.mode);
|
|
5763
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
|
+
}
|
|
5764
6070
|
}
|
|
5765
6071
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
5766
6072
|
for (const tool of params.allowTools) {
|
|
@@ -5791,12 +6097,23 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5791
6097
|
},
|
|
5792
6098
|
onSwitchMode: async (mode) => {
|
|
5793
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
|
+
}
|
|
5794
6104
|
currentPermissionMode = mode;
|
|
5795
6105
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
6106
|
+
isSwitchingMode = true;
|
|
5796
6107
|
isKillingClaude = true;
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
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
|
+
}
|
|
5800
6117
|
}
|
|
5801
6118
|
},
|
|
5802
6119
|
onRestartClaude: restartClaudeHandler,
|
|
@@ -5817,11 +6134,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5817
6134
|
onMetadataUpdate: (newMeta) => {
|
|
5818
6135
|
sessionMetadata = {
|
|
5819
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 } : {},
|
|
5820
6141
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
5821
6142
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
5822
6143
|
};
|
|
5823
6144
|
const queue = newMeta.messageQueue;
|
|
5824
|
-
if (queue && queue.length > 0 && !sessionWasProcessing) {
|
|
6145
|
+
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
5825
6146
|
setTimeout(() => {
|
|
5826
6147
|
processMessageQueueRef?.();
|
|
5827
6148
|
}, 200);
|
|
@@ -5858,7 +6179,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5858
6179
|
},
|
|
5859
6180
|
onReadFile: async (path) => {
|
|
5860
6181
|
const resolvedPath = resolve(directory, path);
|
|
5861
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6182
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
5862
6183
|
throw new Error("Path outside working directory");
|
|
5863
6184
|
}
|
|
5864
6185
|
const buffer = await fs.readFile(resolvedPath);
|
|
@@ -5866,7 +6187,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5866
6187
|
},
|
|
5867
6188
|
onWriteFile: async (path, content) => {
|
|
5868
6189
|
const resolvedPath = resolve(directory, path);
|
|
5869
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6190
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
5870
6191
|
throw new Error("Path outside working directory");
|
|
5871
6192
|
}
|
|
5872
6193
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -5874,7 +6195,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5874
6195
|
},
|
|
5875
6196
|
onListDirectory: async (path) => {
|
|
5876
6197
|
const resolvedDir = resolve(directory, path || ".");
|
|
5877
|
-
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
6198
|
+
if (resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
|
|
5878
6199
|
throw new Error("Path outside working directory");
|
|
5879
6200
|
}
|
|
5880
6201
|
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
@@ -5913,6 +6234,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5913
6234
|
}
|
|
5914
6235
|
}
|
|
5915
6236
|
const resolvedPath = resolve(directory, treePath);
|
|
6237
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6238
|
+
throw new Error("Path outside working directory");
|
|
6239
|
+
}
|
|
5916
6240
|
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
5917
6241
|
return { success: !!tree, tree };
|
|
5918
6242
|
}
|
|
@@ -5929,13 +6253,18 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5929
6253
|
},
|
|
5930
6254
|
sessionService,
|
|
5931
6255
|
logger,
|
|
5932
|
-
() =>
|
|
6256
|
+
() => {
|
|
6257
|
+
if (!trackedSession?.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
6258
|
+
}
|
|
5933
6259
|
);
|
|
5934
6260
|
checkSvampConfig = svampConfig.check;
|
|
5935
6261
|
cleanupSvampConfig = svampConfig.cleanup;
|
|
5936
6262
|
const writeSvampConfigPatch = svampConfig.writeConfig;
|
|
5937
6263
|
processMessageQueueRef = () => {
|
|
5938
6264
|
if (sessionWasProcessing) return;
|
|
6265
|
+
if (trackedSession?.stopped) return;
|
|
6266
|
+
if (isKillingClaude) return;
|
|
6267
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5939
6268
|
const queue = sessionMetadata.messageQueue;
|
|
5940
6269
|
if (queue && queue.length > 0) {
|
|
5941
6270
|
const next = queue[0];
|
|
@@ -5964,22 +6293,33 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5964
6293
|
isKillingClaude = true;
|
|
5965
6294
|
await killAndWaitForExit2(claudeProcess);
|
|
5966
6295
|
isKillingClaude = false;
|
|
6296
|
+
if (trackedSession?.stopped) return;
|
|
6297
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5967
6298
|
claudeResumeId = void 0;
|
|
5968
6299
|
spawnClaude(next.text, queueMeta);
|
|
5969
6300
|
} catch (err) {
|
|
5970
6301
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
5971
6302
|
isKillingClaude = false;
|
|
6303
|
+
sessionWasProcessing = false;
|
|
5972
6304
|
signalProcessing(false);
|
|
5973
6305
|
}
|
|
5974
6306
|
})();
|
|
5975
|
-
} else if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
5976
|
-
spawnClaude(next.text, queueMeta);
|
|
5977
6307
|
} else {
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
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
|
+
}
|
|
5983
6323
|
}
|
|
5984
6324
|
}
|
|
5985
6325
|
};
|
|
@@ -5998,7 +6338,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5998
6338
|
return claudeProcess || void 0;
|
|
5999
6339
|
}
|
|
6000
6340
|
};
|
|
6001
|
-
pidToTrackedSession.set(
|
|
6341
|
+
pidToTrackedSession.set(randomUUID$1(), trackedSession);
|
|
6002
6342
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6003
6343
|
sessionService.updateMetadata(sessionMetadata);
|
|
6004
6344
|
logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
|
|
@@ -6009,6 +6349,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6009
6349
|
};
|
|
6010
6350
|
} catch (err) {
|
|
6011
6351
|
logger.error(`Failed to spawn session: ${err?.message || err}`, err?.stack);
|
|
6352
|
+
if (stagedCredentials) {
|
|
6353
|
+
stagedCredentials.cleanup().catch(() => {
|
|
6354
|
+
});
|
|
6355
|
+
}
|
|
6012
6356
|
return {
|
|
6013
6357
|
type: "error",
|
|
6014
6358
|
errorMessage: `Failed to register session service: ${err?.message || String(err)}`
|
|
@@ -6076,6 +6420,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6076
6420
|
{ controlledByUser: false },
|
|
6077
6421
|
{
|
|
6078
6422
|
onUserMessage: (content, meta) => {
|
|
6423
|
+
if (acpStopped) return;
|
|
6079
6424
|
logger.log(`[${agentName} Session ${sessionId}] User message received`);
|
|
6080
6425
|
let text;
|
|
6081
6426
|
let msgMeta = meta;
|
|
@@ -6096,8 +6441,35 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6096
6441
|
if (msgMeta?.permissionMode) {
|
|
6097
6442
|
currentPermissionMode = msgMeta.permissionMode;
|
|
6098
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);
|
|
6099
6466
|
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
6100
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
|
+
}
|
|
6101
6473
|
});
|
|
6102
6474
|
},
|
|
6103
6475
|
onAbort: () => {
|
|
@@ -6151,18 +6523,27 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6151
6523
|
onMetadataUpdate: (newMeta) => {
|
|
6152
6524
|
sessionMetadata = {
|
|
6153
6525
|
...newMeta,
|
|
6526
|
+
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6527
|
+
lifecycleState: sessionMetadata.lifecycleState,
|
|
6154
6528
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6155
6529
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6156
6530
|
};
|
|
6531
|
+
if (acpStopped) return;
|
|
6157
6532
|
const queue = newMeta.messageQueue;
|
|
6158
6533
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
6159
6534
|
const next = queue[0];
|
|
6160
6535
|
const remaining = queue.slice(1);
|
|
6161
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0 };
|
|
6536
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6162
6537
|
sessionService.updateMetadata(sessionMetadata);
|
|
6163
6538
|
logger.log(`[Session ${sessionId}] Processing queued message from metadata update: "${next.text.slice(0, 50)}..."`);
|
|
6539
|
+
sessionService.sendKeepAlive(true);
|
|
6164
6540
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6165
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
|
+
}
|
|
6166
6547
|
});
|
|
6167
6548
|
}
|
|
6168
6549
|
},
|
|
@@ -6196,7 +6577,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6196
6577
|
},
|
|
6197
6578
|
onReadFile: async (path) => {
|
|
6198
6579
|
const resolvedPath = resolve(directory, path);
|
|
6199
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6580
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6200
6581
|
throw new Error("Path outside working directory");
|
|
6201
6582
|
}
|
|
6202
6583
|
const buffer = await fs.readFile(resolvedPath);
|
|
@@ -6204,7 +6585,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6204
6585
|
},
|
|
6205
6586
|
onWriteFile: async (path, content) => {
|
|
6206
6587
|
const resolvedPath = resolve(directory, path);
|
|
6207
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6588
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6208
6589
|
throw new Error("Path outside working directory");
|
|
6209
6590
|
}
|
|
6210
6591
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -6212,7 +6593,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6212
6593
|
},
|
|
6213
6594
|
onListDirectory: async (path) => {
|
|
6214
6595
|
const resolvedDir = resolve(directory, path || ".");
|
|
6215
|
-
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
6596
|
+
if (resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
|
|
6216
6597
|
throw new Error("Path outside working directory");
|
|
6217
6598
|
}
|
|
6218
6599
|
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
@@ -6251,12 +6632,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6251
6632
|
}
|
|
6252
6633
|
}
|
|
6253
6634
|
const resolvedPath = resolve(directory, treePath);
|
|
6635
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6636
|
+
throw new Error("Path outside working directory");
|
|
6637
|
+
}
|
|
6254
6638
|
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
6255
6639
|
return { success: !!tree, tree };
|
|
6256
6640
|
}
|
|
6257
6641
|
},
|
|
6258
6642
|
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
6259
6643
|
);
|
|
6644
|
+
let insideOnTurnEnd = false;
|
|
6260
6645
|
const svampConfigChecker = createSvampConfigChecker(
|
|
6261
6646
|
directory,
|
|
6262
6647
|
sessionId,
|
|
@@ -6268,6 +6653,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6268
6653
|
sessionService,
|
|
6269
6654
|
logger,
|
|
6270
6655
|
() => {
|
|
6656
|
+
if (acpStopped) return;
|
|
6657
|
+
if (insideOnTurnEnd) return;
|
|
6271
6658
|
const queue = sessionMetadata.messageQueue;
|
|
6272
6659
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
6273
6660
|
const next = queue[0];
|
|
@@ -6278,6 +6665,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6278
6665
|
sessionService.sendKeepAlive(true);
|
|
6279
6666
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6280
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
|
+
}
|
|
6281
6673
|
});
|
|
6282
6674
|
}
|
|
6283
6675
|
}
|
|
@@ -6331,71 +6723,129 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6331
6723
|
isolationConfig: agentIsoConfig
|
|
6332
6724
|
});
|
|
6333
6725
|
}
|
|
6726
|
+
let acpStopped = false;
|
|
6727
|
+
let acpBackendReady = false;
|
|
6334
6728
|
const onTurnEnd = (lastAssistantText) => {
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
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
|
+
}
|
|
6342
6825
|
}
|
|
6343
|
-
const
|
|
6344
|
-
if (
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
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.`;
|
|
6349
|
-
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
6350
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
6351
|
-
} else {
|
|
6352
|
-
const nextIteration = rlState.iteration + 1;
|
|
6353
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6354
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
6355
|
-
const ralphLoop = {
|
|
6356
|
-
active: true,
|
|
6357
|
-
task: rlState.task,
|
|
6358
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
6359
|
-
maxIterations: rlState.max_iterations,
|
|
6360
|
-
currentIteration: nextIteration,
|
|
6361
|
-
startedAt: rlState.started_at,
|
|
6362
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
6363
|
-
contextMode: rlState.context_mode || "fresh",
|
|
6364
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6365
|
-
};
|
|
6366
|
-
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" };
|
|
6367
6831
|
sessionService.updateMetadata(sessionMetadata);
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
6380
|
-
});
|
|
6381
|
-
}, cooldownMs);
|
|
6382
|
-
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
|
+
});
|
|
6383
6843
|
}
|
|
6384
|
-
}
|
|
6385
|
-
|
|
6386
|
-
if (queue && queue.length > 0) {
|
|
6387
|
-
const next = queue[0];
|
|
6388
|
-
const remaining = queue.slice(1);
|
|
6389
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6390
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
6391
|
-
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
6392
|
-
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6393
|
-
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6394
|
-
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6395
|
-
});
|
|
6844
|
+
} finally {
|
|
6845
|
+
insideOnTurnEnd = false;
|
|
6396
6846
|
}
|
|
6397
6847
|
};
|
|
6398
|
-
bridgeAcpToSession(
|
|
6848
|
+
const cleanupBridge = bridgeAcpToSession(
|
|
6399
6849
|
agentBackend,
|
|
6400
6850
|
sessionService,
|
|
6401
6851
|
() => sessionMetadata,
|
|
@@ -6417,11 +6867,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6417
6867
|
resumeSessionId,
|
|
6418
6868
|
get childProcess() {
|
|
6419
6869
|
return agentBackend.getProcess?.() || void 0;
|
|
6870
|
+
},
|
|
6871
|
+
onStop: () => {
|
|
6872
|
+
acpStopped = true;
|
|
6873
|
+
cleanupBridge();
|
|
6874
|
+
permissionHandler.rejectAll("session stopped");
|
|
6875
|
+
agentBackend.dispose().catch(() => {
|
|
6876
|
+
});
|
|
6420
6877
|
}
|
|
6421
6878
|
};
|
|
6422
|
-
pidToTrackedSession.set(
|
|
6879
|
+
pidToTrackedSession.set(randomUUID$1(), trackedSession);
|
|
6423
6880
|
logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
|
|
6424
6881
|
agentBackend.startSession().then(() => {
|
|
6882
|
+
acpBackendReady = true;
|
|
6425
6883
|
logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
6426
6884
|
}).catch((err) => {
|
|
6427
6885
|
logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
@@ -6430,6 +6888,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6430
6888
|
"event"
|
|
6431
6889
|
);
|
|
6432
6890
|
sessionService.sendSessionEnd();
|
|
6891
|
+
stopSession(sessionId);
|
|
6433
6892
|
});
|
|
6434
6893
|
return {
|
|
6435
6894
|
type: "success",
|
|
@@ -6449,6 +6908,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6449
6908
|
for (const [pid, session] of pidToTrackedSession) {
|
|
6450
6909
|
if (session.svampSessionId === sessionId) {
|
|
6451
6910
|
session.stopped = true;
|
|
6911
|
+
session.onStop?.();
|
|
6452
6912
|
session.hyphaService?.disconnect().catch(() => {
|
|
6453
6913
|
});
|
|
6454
6914
|
if (session.childProcess) {
|
|
@@ -6460,12 +6920,14 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6460
6920
|
session.cleanupCredentials?.().catch(() => {
|
|
6461
6921
|
});
|
|
6462
6922
|
session.cleanupSvampConfig?.();
|
|
6923
|
+
artifactSync.cancelSync(sessionId);
|
|
6463
6924
|
pidToTrackedSession.delete(pid);
|
|
6464
6925
|
deletePersistedSession(sessionId);
|
|
6465
6926
|
logger.log(`Session ${sessionId} stopped`);
|
|
6466
6927
|
return true;
|
|
6467
6928
|
}
|
|
6468
6929
|
}
|
|
6930
|
+
artifactSync.cancelSync(sessionId);
|
|
6469
6931
|
deletePersistedSession(sessionId);
|
|
6470
6932
|
logger.log(`Session ${sessionId} not found in memory, cleaned up persisted state`);
|
|
6471
6933
|
return false;
|
|
@@ -6602,14 +7064,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6602
7064
|
for (const sessionId of sessionsToAutoContinue) {
|
|
6603
7065
|
setTimeout(async () => {
|
|
6604
7066
|
try {
|
|
6605
|
-
const svc = await
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
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
|
+
]);
|
|
6613
7081
|
logger.log(`Auto-continued session ${sessionId}`);
|
|
6614
7082
|
} catch (err) {
|
|
6615
7083
|
logger.log(`Failed to auto-continue session ${sessionId}: ${err.message}`);
|
|
@@ -6623,7 +7091,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6623
7091
|
logger.log(`Resuming Ralph loop for ${sessionsToRalphResume.length} session(s)...`);
|
|
6624
7092
|
for (const { sessionId, directory: sessDir } of sessionsToRalphResume) {
|
|
6625
7093
|
try {
|
|
6626
|
-
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
|
+
]);
|
|
6627
7098
|
const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
|
|
6628
7099
|
if (!rlState) continue;
|
|
6629
7100
|
const initDelayMs = 2e3;
|
|
@@ -6644,17 +7115,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6644
7115
|
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
6645
7116
|
const prompt = buildRalphPrompt(currentState.task, currentState);
|
|
6646
7117
|
const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
|
|
6647
|
-
await
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
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
|
+
]);
|
|
6658
7132
|
logger.log(`Resumed Ralph loop for session ${sessionId} at iteration ${currentState.iteration} (${isFreshMode ? "fresh" : "continue"})`);
|
|
6659
7133
|
} catch (err) {
|
|
6660
7134
|
logger.log(`Failed to resume Ralph loop for session ${sessionId}: ${err.message}`);
|
|
@@ -6740,9 +7214,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6740
7214
|
process.kill(child.pid, 0);
|
|
6741
7215
|
} catch {
|
|
6742
7216
|
logger.log(`Removing stale session (child PID ${child.pid} dead): ${session.svampSessionId}`);
|
|
7217
|
+
session.stopped = true;
|
|
7218
|
+
session.onStop?.();
|
|
6743
7219
|
session.hyphaService?.disconnect().catch(() => {
|
|
6744
7220
|
});
|
|
7221
|
+
session.cleanupCredentials?.().catch(() => {
|
|
7222
|
+
});
|
|
7223
|
+
session.cleanupSvampConfig?.();
|
|
7224
|
+
if (session.svampSessionId) artifactSync.cancelSync(session.svampSessionId);
|
|
6745
7225
|
pidToTrackedSession.delete(key);
|
|
7226
|
+
if (session.svampSessionId) deletePersistedSession(session.svampSessionId);
|
|
6746
7227
|
}
|
|
6747
7228
|
}
|
|
6748
7229
|
}
|
|
@@ -6810,6 +7291,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6810
7291
|
clearInterval(heartbeatInterval);
|
|
6811
7292
|
if (proxyTokenRefreshInterval) clearInterval(proxyTokenRefreshInterval);
|
|
6812
7293
|
if (unhandledRejectionResetTimer) clearTimeout(unhandledRejectionResetTimer);
|
|
7294
|
+
for (const [, session] of pidToTrackedSession) {
|
|
7295
|
+
session.stopped = true;
|
|
7296
|
+
session.onStop?.();
|
|
7297
|
+
}
|
|
6813
7298
|
machineService.updateDaemonState({
|
|
6814
7299
|
...initialDaemonState,
|
|
6815
7300
|
status: "shutting-down",
|
|
@@ -6817,7 +7302,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6817
7302
|
shutdownSource: source
|
|
6818
7303
|
});
|
|
6819
7304
|
await new Promise((r) => setTimeout(r, 200));
|
|
6820
|
-
for (const [
|
|
7305
|
+
for (const [, session] of pidToTrackedSession) {
|
|
6821
7306
|
session.hyphaService?.disconnect().catch(() => {
|
|
6822
7307
|
});
|
|
6823
7308
|
if (session.childProcess) {
|
|
@@ -6834,14 +7319,21 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6834
7319
|
if (shouldMarkStopped) {
|
|
6835
7320
|
try {
|
|
6836
7321
|
const index = loadSessionIndex();
|
|
7322
|
+
let markedCount = 0;
|
|
6837
7323
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
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 {
|
|
6842
7334
|
}
|
|
6843
7335
|
}
|
|
6844
|
-
logger.log(
|
|
7336
|
+
logger.log(`Marked ${markedCount} session(s) as stopped (--cleanup mode)`);
|
|
6845
7337
|
} catch {
|
|
6846
7338
|
}
|
|
6847
7339
|
} else {
|