svamp-cli 0.1.63 → 0.1.65
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/agentCommands-C6iGblcL.mjs +156 -0
- package/dist/{api-Cegey1dh.mjs → api-BRbsyqJ4.mjs} +9 -2
- package/dist/cli.mjs +73 -20
- package/dist/{commands-dm3PSNsk.mjs → commands-CF32XIau.mjs} +3 -3
- package/dist/{commands-3Zu9sCxa.mjs → commands-Dc_6JdE_.mjs} +2 -2
- package/dist/{commands-BRow9cGp.mjs → commands-FAWhkKtz.mjs} +1 -1
- package/dist/{commands-6EyqaoCp.mjs → commands-UFi0_ESV.mjs} +48 -24
- package/dist/index.mjs +1 -1
- package/dist/{package-7U32bRBY.mjs → package-CvCDrFPH.mjs} +2 -2
- package/dist/{run-BaMf-bAo.mjs → run-DPhSmbr1.mjs} +942 -387
- package/dist/{run-COqTRwXb.mjs → run-DTwMJ7dx.mjs} +80 -28
- package/dist/{tunnel-dl6vFKgd.mjs → tunnel-C3UsqTxi.mjs} +53 -65
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os__default from 'os';
|
|
2
|
-
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile, unlink } from 'fs/promises';
|
|
3
|
-
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
2
|
+
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile, rename, unlink } from 'fs/promises';
|
|
3
|
+
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
4
4
|
import path, { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { spawn as spawn$1 } from 'child_process';
|
|
@@ -70,7 +70,7 @@ const ISOLATION_PREFERENCE = ["nono", "docker", "podman"];
|
|
|
70
70
|
|
|
71
71
|
function resolveRoleLevel(sharing, userEmail) {
|
|
72
72
|
let level = -1;
|
|
73
|
-
if (userEmail) {
|
|
73
|
+
if (userEmail && Array.isArray(sharing.allowedUsers)) {
|
|
74
74
|
const sharedUser = sharing.allowedUsers.find(
|
|
75
75
|
(u) => u.email.toLowerCase() === userEmail.toLowerCase()
|
|
76
76
|
);
|
|
@@ -303,7 +303,10 @@ function loadPersistedMachineMetadata(svampHomeDir) {
|
|
|
303
303
|
function savePersistedMachineMetadata(svampHomeDir, data) {
|
|
304
304
|
try {
|
|
305
305
|
mkdirSync(svampHomeDir, { recursive: true });
|
|
306
|
-
|
|
306
|
+
const filePath = getMachineMetadataPath(svampHomeDir);
|
|
307
|
+
const tmpPath = filePath + ".tmp";
|
|
308
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
309
|
+
renameSync(tmpPath, filePath);
|
|
307
310
|
} catch (err) {
|
|
308
311
|
console.error("[HYPHA MACHINE] Failed to persist machine metadata:", err);
|
|
309
312
|
}
|
|
@@ -313,9 +316,36 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
313
316
|
let currentDaemonState = { ...daemonState };
|
|
314
317
|
let metadataVersion = 1;
|
|
315
318
|
let daemonStateVersion = 1;
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
+
const listeners = [];
|
|
320
|
+
const removeListener = (listener, reason) => {
|
|
321
|
+
const idx = listeners.indexOf(listener);
|
|
322
|
+
if (idx >= 0) {
|
|
323
|
+
listeners.splice(idx, 1);
|
|
324
|
+
console.log(`[HYPHA MACHINE] Listener removed (${reason}), remaining: ${listeners.length}`);
|
|
325
|
+
const rintfId = listener._rintf_service_id;
|
|
326
|
+
if (rintfId) {
|
|
327
|
+
server.unregisterService(rintfId).catch(() => {
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const notifyListeners = (update) => {
|
|
333
|
+
const snapshot = [...listeners];
|
|
334
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
335
|
+
const listener = snapshot[i];
|
|
336
|
+
try {
|
|
337
|
+
const result = listener.onUpdate(update);
|
|
338
|
+
if (result && typeof result.catch === "function") {
|
|
339
|
+
result.catch((err) => {
|
|
340
|
+
console.error(`[HYPHA MACHINE] Async listener error:`, err);
|
|
341
|
+
removeListener(listener, "async error");
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error(`[HYPHA MACHINE] Listener error:`, err);
|
|
346
|
+
removeListener(listener, "sync error");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
319
349
|
};
|
|
320
350
|
const serviceInfo = await server.registerService(
|
|
321
351
|
{
|
|
@@ -335,11 +365,14 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
335
365
|
};
|
|
336
366
|
},
|
|
337
367
|
// Heartbeat
|
|
338
|
-
heartbeat: async (context) =>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
heartbeat: async (context) => {
|
|
369
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
370
|
+
return {
|
|
371
|
+
time: Date.now(),
|
|
372
|
+
status: currentDaemonState.status,
|
|
373
|
+
machineId
|
|
374
|
+
};
|
|
375
|
+
},
|
|
343
376
|
// List active sessions on this machine
|
|
344
377
|
listSessions: async (context) => {
|
|
345
378
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
@@ -352,21 +385,22 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
352
385
|
const machineOwner = currentMetadata.sharing?.owner;
|
|
353
386
|
const isSharedUser = callerEmail && machineOwner && callerEmail.toLowerCase() !== machineOwner.toLowerCase();
|
|
354
387
|
if (isSharedUser) {
|
|
355
|
-
const machineUser = currentMetadata.sharing?.allowedUsers?.find(
|
|
356
|
-
(u) => u.email.toLowerCase() === callerEmail.toLowerCase()
|
|
357
|
-
);
|
|
358
|
-
const callerRole = machineUser?.role || "interact";
|
|
359
388
|
const sharing = {
|
|
360
389
|
enabled: true,
|
|
361
|
-
owner:
|
|
390
|
+
owner: callerEmail,
|
|
391
|
+
// spawning user owns their session
|
|
362
392
|
allowedUsers: [
|
|
363
|
-
|
|
393
|
+
// Machine owner gets admin access (can monitor/control sessions on their machine)
|
|
364
394
|
{
|
|
365
|
-
email:
|
|
366
|
-
role:
|
|
395
|
+
email: machineOwner,
|
|
396
|
+
role: "admin",
|
|
367
397
|
addedAt: Date.now(),
|
|
368
398
|
addedBy: "machine-auto"
|
|
369
|
-
}
|
|
399
|
+
},
|
|
400
|
+
// Preserve any explicitly requested allowedUsers (e.g. additional collaborators)
|
|
401
|
+
...(options.sharing?.allowedUsers || []).filter(
|
|
402
|
+
(u) => u.email.toLowerCase() !== machineOwner.toLowerCase()
|
|
403
|
+
)
|
|
370
404
|
]
|
|
371
405
|
};
|
|
372
406
|
options = { ...options, sharing };
|
|
@@ -386,22 +420,17 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
386
420
|
...options,
|
|
387
421
|
securityContext: mergeSecurityContexts(machineCtx, options.securityContext)
|
|
388
422
|
};
|
|
389
|
-
if (machineCtx.role && options.sharing?.enabled) {
|
|
390
|
-
const user = options.sharing.allowedUsers?.find(
|
|
391
|
-
(u) => u.email.toLowerCase() === callerEmail.toLowerCase()
|
|
392
|
-
);
|
|
393
|
-
if (user && !user.role) {
|
|
394
|
-
user.role = machineCtx.role;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
423
|
}
|
|
398
424
|
}
|
|
425
|
+
if (options.injectPlatformGuidance === void 0 && currentMetadata.injectPlatformGuidance !== void 0) {
|
|
426
|
+
options = { ...options, injectPlatformGuidance: currentMetadata.injectPlatformGuidance };
|
|
427
|
+
}
|
|
399
428
|
const result = await handlers.spawnSession({
|
|
400
429
|
...options,
|
|
401
430
|
machineId
|
|
402
431
|
});
|
|
403
432
|
if (result.type === "success" && result.sessionId) {
|
|
404
|
-
|
|
433
|
+
notifyListeners({
|
|
405
434
|
type: "new-session",
|
|
406
435
|
sessionId: result.sessionId,
|
|
407
436
|
machineId
|
|
@@ -414,7 +443,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
414
443
|
stopSession: async (sessionId, context) => {
|
|
415
444
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
416
445
|
const result = handlers.stopSession(sessionId);
|
|
417
|
-
|
|
446
|
+
notifyListeners({
|
|
418
447
|
type: "session-stopped",
|
|
419
448
|
sessionId,
|
|
420
449
|
machineId
|
|
@@ -423,7 +452,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
423
452
|
},
|
|
424
453
|
// Restart agent process in a session (machine-level fallback)
|
|
425
454
|
restartSession: async (sessionId, context) => {
|
|
426
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
455
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
427
456
|
return await handlers.restartSession(sessionId);
|
|
428
457
|
},
|
|
429
458
|
// Stop the daemon
|
|
@@ -453,9 +482,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
453
482
|
metadataVersion++;
|
|
454
483
|
savePersistedMachineMetadata(metadata.svampHomeDir, {
|
|
455
484
|
sharing: currentMetadata.sharing,
|
|
456
|
-
securityContextConfig: currentMetadata.securityContextConfig
|
|
485
|
+
securityContextConfig: currentMetadata.securityContextConfig,
|
|
486
|
+
injectPlatformGuidance: currentMetadata.injectPlatformGuidance
|
|
457
487
|
});
|
|
458
|
-
|
|
488
|
+
notifyListeners({
|
|
459
489
|
type: "update-machine",
|
|
460
490
|
machineId,
|
|
461
491
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -485,7 +515,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
485
515
|
}
|
|
486
516
|
currentDaemonState = newState;
|
|
487
517
|
daemonStateVersion++;
|
|
488
|
-
|
|
518
|
+
notifyListeners({
|
|
489
519
|
type: "update-machine",
|
|
490
520
|
machineId,
|
|
491
521
|
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
@@ -513,9 +543,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
513
543
|
metadataVersion++;
|
|
514
544
|
savePersistedMachineMetadata(metadata.svampHomeDir, {
|
|
515
545
|
sharing: currentMetadata.sharing,
|
|
516
|
-
securityContextConfig: currentMetadata.securityContextConfig
|
|
546
|
+
securityContextConfig: currentMetadata.securityContextConfig,
|
|
547
|
+
injectPlatformGuidance: currentMetadata.injectPlatformGuidance
|
|
517
548
|
});
|
|
518
|
-
|
|
549
|
+
notifyListeners({
|
|
519
550
|
type: "update-machine",
|
|
520
551
|
machineId,
|
|
521
552
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -534,62 +565,21 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
534
565
|
metadataVersion++;
|
|
535
566
|
savePersistedMachineMetadata(metadata.svampHomeDir, {
|
|
536
567
|
sharing: currentMetadata.sharing,
|
|
537
|
-
securityContextConfig: currentMetadata.securityContextConfig
|
|
568
|
+
securityContextConfig: currentMetadata.securityContextConfig,
|
|
569
|
+
injectPlatformGuidance: currentMetadata.injectPlatformGuidance
|
|
538
570
|
});
|
|
539
|
-
|
|
571
|
+
notifyListeners({
|
|
540
572
|
type: "update-machine",
|
|
541
573
|
machineId,
|
|
542
574
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
543
575
|
});
|
|
544
576
|
return { success: true };
|
|
545
577
|
},
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
subscribe: async function* (context) {
|
|
578
|
+
// Register a listener for real-time updates (app calls this with _rintf callback)
|
|
579
|
+
registerListener: async (callback, context) => {
|
|
549
580
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let callerDisconnected = false;
|
|
553
|
-
const callerClientId = context?.from;
|
|
554
|
-
const push = (update) => {
|
|
555
|
-
pending.push(update);
|
|
556
|
-
wake?.();
|
|
557
|
-
};
|
|
558
|
-
const onDisconnect = (event) => {
|
|
559
|
-
if (callerClientId && event.client_id === callerClientId) {
|
|
560
|
-
callerDisconnected = true;
|
|
561
|
-
const w = wake;
|
|
562
|
-
wake = null;
|
|
563
|
-
w?.();
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
server.on("remote_client_disconnected", onDisconnect);
|
|
567
|
-
subscribers.add(push);
|
|
568
|
-
console.log(`[HYPHA MACHINE] subscribe() started (total: ${subscribers.size})`);
|
|
569
|
-
try {
|
|
570
|
-
yield {
|
|
571
|
-
type: "update-machine",
|
|
572
|
-
machineId,
|
|
573
|
-
metadata: { value: currentMetadata, version: metadataVersion },
|
|
574
|
-
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
575
|
-
};
|
|
576
|
-
while (!callerDisconnected) {
|
|
577
|
-
while (pending.length === 0 && !callerDisconnected) {
|
|
578
|
-
await new Promise((r) => {
|
|
579
|
-
wake = r;
|
|
580
|
-
});
|
|
581
|
-
wake = null;
|
|
582
|
-
}
|
|
583
|
-
while (pending.length > 0) {
|
|
584
|
-
yield pending.shift();
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
} finally {
|
|
588
|
-
server.off("remote_client_disconnected", onDisconnect);
|
|
589
|
-
subscribers.delete(push);
|
|
590
|
-
wake?.();
|
|
591
|
-
console.log(`[HYPHA MACHINE] subscribe() ended (remaining: ${subscribers.size})`);
|
|
592
|
-
}
|
|
581
|
+
listeners.push(callback);
|
|
582
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
593
583
|
},
|
|
594
584
|
// Shell access
|
|
595
585
|
bash: async (command, cwd, context) => {
|
|
@@ -615,7 +605,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
615
605
|
const targetPath = resolve(path || homedir());
|
|
616
606
|
const home = homedir();
|
|
617
607
|
const isOwner = !currentMetadata.sharing?.enabled || context?.user?.email && currentMetadata.sharing.owner && context.user.email.toLowerCase() === currentMetadata.sharing.owner.toLowerCase();
|
|
618
|
-
if (!isOwner && !targetPath.startsWith(home)) {
|
|
608
|
+
if (!isOwner && targetPath !== home && !targetPath.startsWith(home + "/")) {
|
|
619
609
|
throw new Error(`Access denied: path must be within ${home}`);
|
|
620
610
|
}
|
|
621
611
|
const showHidden = options?.showHidden ?? false;
|
|
@@ -656,7 +646,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
656
646
|
},
|
|
657
647
|
/** Add and start a new supervised process. */
|
|
658
648
|
processAdd: async (params, context) => {
|
|
659
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
649
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
660
650
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
661
651
|
return handlers.supervisor.add(params.spec);
|
|
662
652
|
},
|
|
@@ -665,7 +655,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
665
655
|
* Returns { action: 'created'|'updated'|'no-change', info: ProcessInfo }
|
|
666
656
|
*/
|
|
667
657
|
processApply: async (params, context) => {
|
|
668
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
658
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
669
659
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
670
660
|
return handlers.supervisor.apply(params.spec);
|
|
671
661
|
},
|
|
@@ -674,7 +664,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
674
664
|
* Returns updated ProcessInfo.
|
|
675
665
|
*/
|
|
676
666
|
processUpdate: async (params, context) => {
|
|
677
|
-
authorizeRequest(context, currentMetadata.sharing, "
|
|
667
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
678
668
|
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
679
669
|
return handlers.supervisor.update(params.idOrName, params.spec);
|
|
680
670
|
},
|
|
@@ -719,7 +709,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
719
709
|
serviceList: async (context) => {
|
|
720
710
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
721
711
|
try {
|
|
722
|
-
const { listServiceGroups } = await import('./api-
|
|
712
|
+
const { listServiceGroups } = await import('./api-BRbsyqJ4.mjs');
|
|
723
713
|
return await listServiceGroups();
|
|
724
714
|
} catch (err) {
|
|
725
715
|
return [];
|
|
@@ -728,13 +718,13 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
728
718
|
/** Get full details of a single service group (includes backends + health). */
|
|
729
719
|
serviceGet: async (params, context) => {
|
|
730
720
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
731
|
-
const { getServiceGroup } = await import('./api-
|
|
721
|
+
const { getServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
732
722
|
return getServiceGroup(params.name);
|
|
733
723
|
},
|
|
734
724
|
/** Delete a service group. */
|
|
735
725
|
serviceDelete: async (params, context) => {
|
|
736
726
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
737
|
-
const { deleteServiceGroup } = await import('./api-
|
|
727
|
+
const { deleteServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
738
728
|
return deleteServiceGroup(params.name);
|
|
739
729
|
},
|
|
740
730
|
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
@@ -745,19 +735,27 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
745
735
|
return { success: false, error: "No OpenAI API key found. Set OPENAI_API_KEY or pass apiKey." };
|
|
746
736
|
}
|
|
747
737
|
try {
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
738
|
+
const wisCtrl = new AbortController();
|
|
739
|
+
const wisTimer = setTimeout(() => wisCtrl.abort(), 15e3);
|
|
740
|
+
let response;
|
|
741
|
+
try {
|
|
742
|
+
response = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
|
|
743
|
+
method: "POST",
|
|
744
|
+
headers: {
|
|
745
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
746
|
+
"Content-Type": "application/json"
|
|
747
|
+
},
|
|
748
|
+
body: JSON.stringify({
|
|
749
|
+
session: {
|
|
750
|
+
type: "realtime",
|
|
751
|
+
model: params.model || "gpt-realtime-mini"
|
|
752
|
+
}
|
|
753
|
+
}),
|
|
754
|
+
signal: wisCtrl.signal
|
|
755
|
+
});
|
|
756
|
+
} finally {
|
|
757
|
+
clearTimeout(wisTimer);
|
|
758
|
+
}
|
|
761
759
|
if (!response.ok) {
|
|
762
760
|
return { success: false, error: `OpenAI API error: ${response.status}` };
|
|
763
761
|
}
|
|
@@ -776,7 +774,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
776
774
|
updateMetadata: (newMetadata) => {
|
|
777
775
|
currentMetadata = newMetadata;
|
|
778
776
|
metadataVersion++;
|
|
779
|
-
|
|
777
|
+
notifyListeners({
|
|
780
778
|
type: "update-machine",
|
|
781
779
|
machineId,
|
|
782
780
|
metadata: { value: currentMetadata, version: metadataVersion }
|
|
@@ -785,13 +783,17 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
785
783
|
updateDaemonState: (newState) => {
|
|
786
784
|
currentDaemonState = newState;
|
|
787
785
|
daemonStateVersion++;
|
|
788
|
-
|
|
786
|
+
notifyListeners({
|
|
789
787
|
type: "update-machine",
|
|
790
788
|
machineId,
|
|
791
789
|
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
792
790
|
});
|
|
793
791
|
},
|
|
794
792
|
disconnect: async () => {
|
|
793
|
+
const toRemove = [...listeners];
|
|
794
|
+
for (const listener of toRemove) {
|
|
795
|
+
removeListener(listener, "disconnect");
|
|
796
|
+
}
|
|
795
797
|
await server.unregisterService(serviceInfo.id);
|
|
796
798
|
}
|
|
797
799
|
};
|
|
@@ -809,17 +811,21 @@ function loadMessages(messagesDir, sessionId) {
|
|
|
809
811
|
} catch {
|
|
810
812
|
}
|
|
811
813
|
}
|
|
812
|
-
return messages.slice(-
|
|
814
|
+
return messages.slice(-1e3);
|
|
813
815
|
} catch {
|
|
814
816
|
return [];
|
|
815
817
|
}
|
|
816
818
|
}
|
|
817
819
|
function appendMessage(messagesDir, sessionId, msg) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
820
|
+
try {
|
|
821
|
+
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
822
|
+
if (!existsSync(messagesDir)) {
|
|
823
|
+
mkdirSync$1(messagesDir, { recursive: true });
|
|
824
|
+
}
|
|
825
|
+
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
826
|
+
} catch (err) {
|
|
827
|
+
console.error(`[HYPHA SESSION ${sessionId}] Failed to persist message: ${err?.message ?? err}`);
|
|
821
828
|
}
|
|
822
|
-
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
823
829
|
}
|
|
824
830
|
async function registerSessionService(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
825
831
|
const messages = options?.messagesDir ? loadMessages(options.messagesDir) : [];
|
|
@@ -834,9 +840,36 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
834
840
|
mode: "remote",
|
|
835
841
|
time: Date.now()
|
|
836
842
|
};
|
|
837
|
-
const
|
|
838
|
-
const
|
|
839
|
-
|
|
843
|
+
const listeners = [];
|
|
844
|
+
const removeListener = (listener, reason) => {
|
|
845
|
+
const idx = listeners.indexOf(listener);
|
|
846
|
+
if (idx >= 0) {
|
|
847
|
+
listeners.splice(idx, 1);
|
|
848
|
+
console.log(`[HYPHA SESSION ${sessionId}] Listener removed (${reason}), remaining: ${listeners.length}`);
|
|
849
|
+
const rintfId = listener._rintf_service_id;
|
|
850
|
+
if (rintfId) {
|
|
851
|
+
server.unregisterService(rintfId).catch(() => {
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const notifyListeners = (update) => {
|
|
857
|
+
const snapshot = [...listeners];
|
|
858
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
859
|
+
const listener = snapshot[i];
|
|
860
|
+
try {
|
|
861
|
+
const result = listener.onUpdate(update);
|
|
862
|
+
if (result && typeof result.catch === "function") {
|
|
863
|
+
result.catch((err) => {
|
|
864
|
+
console.error(`[HYPHA SESSION ${sessionId}] Async listener error:`, err);
|
|
865
|
+
removeListener(listener, "async error");
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.error(`[HYPHA SESSION ${sessionId}] Listener error:`, err);
|
|
870
|
+
removeListener(listener, "sync error");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
840
873
|
};
|
|
841
874
|
const pushMessage = (content, role = "agent") => {
|
|
842
875
|
let wrappedContent;
|
|
@@ -867,7 +900,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
867
900
|
if (options?.messagesDir) {
|
|
868
901
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
869
902
|
}
|
|
870
|
-
|
|
903
|
+
notifyListeners({
|
|
871
904
|
type: "new-message",
|
|
872
905
|
sessionId,
|
|
873
906
|
message: msg
|
|
@@ -928,7 +961,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
928
961
|
if (options?.messagesDir) {
|
|
929
962
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
930
963
|
}
|
|
931
|
-
|
|
964
|
+
notifyListeners({
|
|
932
965
|
type: "new-message",
|
|
933
966
|
sessionId,
|
|
934
967
|
message: msg
|
|
@@ -955,7 +988,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
955
988
|
}
|
|
956
989
|
metadata = newMetadata;
|
|
957
990
|
metadataVersion++;
|
|
958
|
-
|
|
991
|
+
notifyListeners({
|
|
959
992
|
type: "update-session",
|
|
960
993
|
sessionId,
|
|
961
994
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -973,7 +1006,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
973
1006
|
* Null values remove keys from the config.
|
|
974
1007
|
*/
|
|
975
1008
|
updateConfig: async (patch, context) => {
|
|
976
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1009
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
977
1010
|
callbacks.onUpdateConfig?.(patch);
|
|
978
1011
|
return { success: true };
|
|
979
1012
|
},
|
|
@@ -996,7 +1029,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
996
1029
|
}
|
|
997
1030
|
agentState = newState;
|
|
998
1031
|
agentStateVersion++;
|
|
999
|
-
|
|
1032
|
+
notifyListeners({
|
|
1000
1033
|
type: "update-session",
|
|
1001
1034
|
sessionId,
|
|
1002
1035
|
agentState: { value: agentState, version: agentStateVersion }
|
|
@@ -1019,7 +1052,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1019
1052
|
return { success: true };
|
|
1020
1053
|
},
|
|
1021
1054
|
switchMode: async (mode, context) => {
|
|
1022
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1055
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1023
1056
|
callbacks.onSwitchMode(mode);
|
|
1024
1057
|
return { success: true };
|
|
1025
1058
|
},
|
|
@@ -1034,16 +1067,18 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1034
1067
|
},
|
|
1035
1068
|
// ── Activity ──
|
|
1036
1069
|
keepAlive: async (thinking, mode, context) => {
|
|
1070
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1037
1071
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1038
|
-
|
|
1072
|
+
notifyListeners({
|
|
1039
1073
|
type: "activity",
|
|
1040
1074
|
sessionId,
|
|
1041
1075
|
...lastActivity
|
|
1042
1076
|
});
|
|
1043
1077
|
},
|
|
1044
1078
|
sessionEnd: async (context) => {
|
|
1079
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1045
1080
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1046
|
-
|
|
1081
|
+
notifyListeners({
|
|
1047
1082
|
type: "activity",
|
|
1048
1083
|
sessionId,
|
|
1049
1084
|
...lastActivity
|
|
@@ -1104,6 +1139,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1104
1139
|
},
|
|
1105
1140
|
/** Returns the caller's effective role (null if no access). Does not throw. */
|
|
1106
1141
|
getEffectiveRole: async (context) => {
|
|
1142
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1107
1143
|
const role = getEffectiveRole(context, metadata.sharing);
|
|
1108
1144
|
return { role };
|
|
1109
1145
|
},
|
|
@@ -1117,7 +1153,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1117
1153
|
}
|
|
1118
1154
|
metadata = { ...metadata, sharing: newSharing };
|
|
1119
1155
|
metadataVersion++;
|
|
1120
|
-
|
|
1156
|
+
notifyListeners({
|
|
1121
1157
|
type: "update-session",
|
|
1122
1158
|
sessionId,
|
|
1123
1159
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1135,7 +1171,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1135
1171
|
}
|
|
1136
1172
|
metadata = { ...metadata, securityContext: newSecurityContext };
|
|
1137
1173
|
metadataVersion++;
|
|
1138
|
-
|
|
1174
|
+
notifyListeners({
|
|
1139
1175
|
type: "update-session",
|
|
1140
1176
|
sessionId,
|
|
1141
1177
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1150,67 +1186,69 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1150
1186
|
}
|
|
1151
1187
|
return await callbacks.onApplySystemPrompt(prompt);
|
|
1152
1188
|
},
|
|
1153
|
-
// ──
|
|
1154
|
-
|
|
1155
|
-
// Returns an async generator that yields real-time updates for this session.
|
|
1156
|
-
// hypha-rpc proxies the generator across the RPC boundary — the frontend
|
|
1157
|
-
// iterates with `for await (const update of service.subscribe())`.
|
|
1158
|
-
//
|
|
1159
|
-
// Initial state is replayed as the first batch of yields so the frontend
|
|
1160
|
-
// can reconstruct full session state without a separate RPC call.
|
|
1161
|
-
// Cleanup is automatic: when the frontend disconnects, hypha-rpc calls the
|
|
1162
|
-
// generator's close method, triggering the finally block which removes the
|
|
1163
|
-
// subscriber. No reverse `_rintf` service is registered.
|
|
1164
|
-
subscribe: async function* (context) {
|
|
1189
|
+
// ── Listener Registration ──
|
|
1190
|
+
registerListener: async (callback, context) => {
|
|
1165
1191
|
authorizeRequest(context, metadata.sharing, "view");
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1192
|
+
listeners.push(callback);
|
|
1193
|
+
const replayMessages = messages.slice(-50);
|
|
1194
|
+
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
1195
|
+
for (const msg of replayMessages) {
|
|
1196
|
+
if (listeners.indexOf(callback) < 0) break;
|
|
1197
|
+
try {
|
|
1198
|
+
const result = callback.onUpdate({
|
|
1199
|
+
type: "new-message",
|
|
1200
|
+
sessionId,
|
|
1201
|
+
message: msg
|
|
1202
|
+
});
|
|
1203
|
+
if (result && typeof result.catch === "function") {
|
|
1204
|
+
try {
|
|
1205
|
+
await Promise.race([
|
|
1206
|
+
result,
|
|
1207
|
+
new Promise(
|
|
1208
|
+
(_, reject) => setTimeout(() => reject(new Error("Replay message timeout")), REPLAY_MESSAGE_TIMEOUT_MS)
|
|
1209
|
+
)
|
|
1210
|
+
]);
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1213
|
+
removeListener(callback, "replay error");
|
|
1214
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1219
|
+
removeListener(callback, "replay error");
|
|
1220
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1180
1221
|
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1222
|
+
}
|
|
1223
|
+
if (listeners.indexOf(callback) < 0) {
|
|
1224
|
+
return { success: false, error: "Listener was removed during replay" };
|
|
1225
|
+
}
|
|
1185
1226
|
try {
|
|
1186
|
-
|
|
1227
|
+
const result = callback.onUpdate({
|
|
1187
1228
|
type: "update-session",
|
|
1188
1229
|
sessionId,
|
|
1189
1230
|
metadata: { value: metadata, version: metadataVersion },
|
|
1190
1231
|
agentState: { value: agentState, version: agentStateVersion }
|
|
1191
|
-
};
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1232
|
+
});
|
|
1233
|
+
if (result && typeof result.catch === "function") {
|
|
1234
|
+
result.catch(() => {
|
|
1235
|
+
});
|
|
1195
1236
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const result = callback.onUpdate({
|
|
1241
|
+
type: "activity",
|
|
1242
|
+
sessionId,
|
|
1243
|
+
...lastActivity
|
|
1244
|
+
});
|
|
1245
|
+
if (result && typeof result.catch === "function") {
|
|
1246
|
+
result.catch(() => {
|
|
1247
|
+
});
|
|
1207
1248
|
}
|
|
1208
|
-
}
|
|
1209
|
-
server.off("remote_client_disconnected", onDisconnect);
|
|
1210
|
-
subscribers.delete(push);
|
|
1211
|
-
wake?.();
|
|
1212
|
-
console.log(`[HYPHA SESSION ${sessionId}] subscribe() ended (remaining: ${subscribers.size})`);
|
|
1249
|
+
} catch {
|
|
1213
1250
|
}
|
|
1251
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
1214
1252
|
}
|
|
1215
1253
|
},
|
|
1216
1254
|
{ overwrite: true }
|
|
@@ -1225,7 +1263,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1225
1263
|
updateMetadata: (newMetadata) => {
|
|
1226
1264
|
metadata = newMetadata;
|
|
1227
1265
|
metadataVersion++;
|
|
1228
|
-
|
|
1266
|
+
notifyListeners({
|
|
1229
1267
|
type: "update-session",
|
|
1230
1268
|
sessionId,
|
|
1231
1269
|
metadata: { value: metadata, version: metadataVersion }
|
|
@@ -1234,7 +1272,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1234
1272
|
updateAgentState: (newAgentState) => {
|
|
1235
1273
|
agentState = newAgentState;
|
|
1236
1274
|
agentStateVersion++;
|
|
1237
|
-
|
|
1275
|
+
notifyListeners({
|
|
1238
1276
|
type: "update-session",
|
|
1239
1277
|
sessionId,
|
|
1240
1278
|
agentState: { value: agentState, version: agentStateVersion }
|
|
@@ -1242,7 +1280,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1242
1280
|
},
|
|
1243
1281
|
sendKeepAlive: (thinking, mode) => {
|
|
1244
1282
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1245
|
-
|
|
1283
|
+
notifyListeners({
|
|
1246
1284
|
type: "activity",
|
|
1247
1285
|
sessionId,
|
|
1248
1286
|
...lastActivity
|
|
@@ -1250,7 +1288,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1250
1288
|
},
|
|
1251
1289
|
sendSessionEnd: () => {
|
|
1252
1290
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1253
|
-
|
|
1291
|
+
notifyListeners({
|
|
1254
1292
|
type: "activity",
|
|
1255
1293
|
sessionId,
|
|
1256
1294
|
...lastActivity
|
|
@@ -1266,12 +1304,16 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1266
1304
|
} catch {
|
|
1267
1305
|
}
|
|
1268
1306
|
}
|
|
1269
|
-
|
|
1307
|
+
notifyListeners({
|
|
1270
1308
|
type: "clear-messages",
|
|
1271
1309
|
sessionId
|
|
1272
1310
|
});
|
|
1273
1311
|
},
|
|
1274
1312
|
disconnect: async () => {
|
|
1313
|
+
const toRemove = [...listeners];
|
|
1314
|
+
for (const listener of toRemove) {
|
|
1315
|
+
removeListener(listener, "disconnect");
|
|
1316
|
+
}
|
|
1275
1317
|
await server.unregisterService(serviceInfo.id);
|
|
1276
1318
|
}
|
|
1277
1319
|
};
|
|
@@ -1406,6 +1448,19 @@ class SessionArtifactSync {
|
|
|
1406
1448
|
this.log(`[ARTIFACT SYNC] Created new collection: ${this.collectionId}`);
|
|
1407
1449
|
}
|
|
1408
1450
|
}
|
|
1451
|
+
/**
|
|
1452
|
+
* fetch() with an AbortSignal-based timeout to prevent indefinite hangs
|
|
1453
|
+
* on slow/stalled presigned URL servers.
|
|
1454
|
+
*/
|
|
1455
|
+
async fetchWithTimeout(url, options = {}, timeoutMs = 6e4) {
|
|
1456
|
+
const controller = new AbortController();
|
|
1457
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1458
|
+
try {
|
|
1459
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
1460
|
+
} finally {
|
|
1461
|
+
clearTimeout(timer);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1409
1464
|
/**
|
|
1410
1465
|
* Upload a file to an artifact using the presigned URL pattern:
|
|
1411
1466
|
* 1. put_file() returns a presigned upload URL
|
|
@@ -1420,11 +1475,11 @@ class SessionArtifactSync {
|
|
|
1420
1475
|
if (!putUrl || typeof putUrl !== "string") {
|
|
1421
1476
|
throw new Error(`put_file returned invalid URL for ${filePath}: ${putUrl}`);
|
|
1422
1477
|
}
|
|
1423
|
-
const resp = await
|
|
1478
|
+
const resp = await this.fetchWithTimeout(putUrl, {
|
|
1424
1479
|
method: "PUT",
|
|
1425
1480
|
body: content,
|
|
1426
1481
|
headers: { "Content-Type": "application/octet-stream" }
|
|
1427
|
-
});
|
|
1482
|
+
}, 12e4);
|
|
1428
1483
|
if (!resp.ok) {
|
|
1429
1484
|
throw new Error(`Upload failed for ${filePath}: ${resp.status} ${resp.statusText}`);
|
|
1430
1485
|
}
|
|
@@ -1441,7 +1496,7 @@ class SessionArtifactSync {
|
|
|
1441
1496
|
_rkwargs: true
|
|
1442
1497
|
});
|
|
1443
1498
|
if (!getUrl || typeof getUrl !== "string") return null;
|
|
1444
|
-
const resp = await
|
|
1499
|
+
const resp = await this.fetchWithTimeout(getUrl, {}, 6e4);
|
|
1445
1500
|
if (!resp.ok) return null;
|
|
1446
1501
|
return await resp.text();
|
|
1447
1502
|
}
|
|
@@ -1459,16 +1514,27 @@ class SessionArtifactSync {
|
|
|
1459
1514
|
const artifactAlias = `session-${sessionId}`;
|
|
1460
1515
|
const sessionJsonPath = join$1(sessionsDir, "session.json");
|
|
1461
1516
|
const messagesPath = join$1(sessionsDir, "messages.jsonl");
|
|
1462
|
-
|
|
1517
|
+
let sessionData = null;
|
|
1518
|
+
if (existsSync(sessionJsonPath)) {
|
|
1519
|
+
try {
|
|
1520
|
+
sessionData = JSON.parse(readFileSync(sessionJsonPath, "utf-8"));
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1463
1524
|
const messagesExist = existsSync(messagesPath);
|
|
1464
1525
|
const messageCount = messagesExist ? readFileSync(messagesPath, "utf-8").split("\n").filter((l) => l.trim()).length : 0;
|
|
1465
1526
|
let artifactId;
|
|
1527
|
+
let existingArtifactId = null;
|
|
1466
1528
|
try {
|
|
1467
1529
|
const existing = await this.artifactManager.read({
|
|
1468
1530
|
artifact_id: artifactAlias,
|
|
1469
1531
|
_rkwargs: true
|
|
1470
1532
|
});
|
|
1471
|
-
|
|
1533
|
+
existingArtifactId = existing.id;
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
if (existingArtifactId) {
|
|
1537
|
+
artifactId = existingArtifactId;
|
|
1472
1538
|
await this.artifactManager.edit({
|
|
1473
1539
|
artifact_id: artifactId,
|
|
1474
1540
|
manifest: {
|
|
@@ -1482,7 +1548,7 @@ class SessionArtifactSync {
|
|
|
1482
1548
|
stage: true,
|
|
1483
1549
|
_rkwargs: true
|
|
1484
1550
|
});
|
|
1485
|
-
}
|
|
1551
|
+
} else {
|
|
1486
1552
|
const artifact = await this.artifactManager.create({
|
|
1487
1553
|
alias: artifactAlias,
|
|
1488
1554
|
parent_id: this.collectionId,
|
|
@@ -1536,6 +1602,16 @@ class SessionArtifactSync {
|
|
|
1536
1602
|
}, delayMs);
|
|
1537
1603
|
this.syncTimers.set(sessionId, timer);
|
|
1538
1604
|
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Cancel any pending debounced sync for a session (e.g., when session is stopped).
|
|
1607
|
+
*/
|
|
1608
|
+
cancelSync(sessionId) {
|
|
1609
|
+
const existing = this.syncTimers.get(sessionId);
|
|
1610
|
+
if (existing) {
|
|
1611
|
+
clearTimeout(existing);
|
|
1612
|
+
this.syncTimers.delete(sessionId);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1539
1615
|
/**
|
|
1540
1616
|
* Download a session from artifact store to local disk.
|
|
1541
1617
|
*/
|
|
@@ -1903,6 +1979,7 @@ function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
|
1903
1979
|
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1904
1980
|
ctx.activeToolCalls.delete(toolCallId);
|
|
1905
1981
|
ctx.toolCallStartTimes.delete(toolCallId);
|
|
1982
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
1906
1983
|
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1907
1984
|
if (timeout) {
|
|
1908
1985
|
clearTimeout(timeout);
|
|
@@ -1912,7 +1989,12 @@ function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
|
1912
1989
|
ctx.emit({ type: "tool-result", toolName: toolKindStr, result: content, callId: toolCallId });
|
|
1913
1990
|
if (ctx.activeToolCalls.size === 0) {
|
|
1914
1991
|
ctx.clearIdleTimeout();
|
|
1915
|
-
ctx.
|
|
1992
|
+
const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
1993
|
+
ctx.setIdleTimeout(() => {
|
|
1994
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
1995
|
+
ctx.emitIdleStatus();
|
|
1996
|
+
}
|
|
1997
|
+
}, idleTimeoutMs);
|
|
1916
1998
|
}
|
|
1917
1999
|
}
|
|
1918
2000
|
function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
@@ -1921,6 +2003,7 @@ function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
|
1921
2003
|
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1922
2004
|
ctx.activeToolCalls.delete(toolCallId);
|
|
1923
2005
|
ctx.toolCallStartTimes.delete(toolCallId);
|
|
2006
|
+
ctx.toolCallIdToNameMap.delete(toolCallId);
|
|
1924
2007
|
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1925
2008
|
if (timeout) {
|
|
1926
2009
|
clearTimeout(timeout);
|
|
@@ -1936,7 +2019,12 @@ function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
|
1936
2019
|
});
|
|
1937
2020
|
if (ctx.activeToolCalls.size === 0) {
|
|
1938
2021
|
ctx.clearIdleTimeout();
|
|
1939
|
-
ctx.
|
|
2022
|
+
const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
2023
|
+
ctx.setIdleTimeout(() => {
|
|
2024
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
2025
|
+
ctx.emitIdleStatus();
|
|
2026
|
+
}
|
|
2027
|
+
}, idleTimeoutMs);
|
|
1940
2028
|
}
|
|
1941
2029
|
}
|
|
1942
2030
|
function handleToolCallUpdate(update, ctx) {
|
|
@@ -2185,10 +2273,22 @@ class AcpBackend {
|
|
|
2185
2273
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2186
2274
|
});
|
|
2187
2275
|
this.process.on("exit", (code, signal) => {
|
|
2188
|
-
if (
|
|
2276
|
+
if (this.disposed) return;
|
|
2277
|
+
if (code !== 0 && code !== null) {
|
|
2189
2278
|
signalStartupFailure(new Error(`Exit code: ${code}`));
|
|
2190
2279
|
this.log(`[ACP] Process exited: code=${code}, signal=${signal}`);
|
|
2191
2280
|
this.emit({ type: "status", status: "stopped", detail: `Exit code: ${code}` });
|
|
2281
|
+
} else if (code === null && this.waitingForResponse) {
|
|
2282
|
+
this.log(`[ACP] Process killed by signal: ${signal} (mid-turn)`);
|
|
2283
|
+
this.waitingForResponse = false;
|
|
2284
|
+
if (this.idleResolver) {
|
|
2285
|
+
this.idleResolver();
|
|
2286
|
+
this.idleResolver = null;
|
|
2287
|
+
}
|
|
2288
|
+
this.emit({ type: "status", status: "stopped", detail: `Process killed by signal: ${signal}` });
|
|
2289
|
+
} else if (code === 0 && this.waitingForResponse) {
|
|
2290
|
+
this.log(`[ACP] Process exited cleanly but response was pending \u2014 emitting idle`);
|
|
2291
|
+
this.emitIdleStatus();
|
|
2192
2292
|
}
|
|
2193
2293
|
});
|
|
2194
2294
|
const streams = nodeToWebStreams(this.process.stdin, this.process.stdout);
|
|
@@ -2343,12 +2443,14 @@ class AcpBackend {
|
|
|
2343
2443
|
const maybeErr = error;
|
|
2344
2444
|
if (startupFailure && error === startupFailure) return true;
|
|
2345
2445
|
if (maybeErr.code === "ENOENT" || maybeErr.code === "EACCES" || maybeErr.code === "EPIPE") return true;
|
|
2446
|
+
if (maybeErr.code === "DISPOSED") return true;
|
|
2346
2447
|
const msg = error.message.toLowerCase();
|
|
2347
2448
|
if (msg.includes("api key") || msg.includes("not configured") || msg.includes("401") || msg.includes("403")) return true;
|
|
2348
2449
|
return false;
|
|
2349
2450
|
};
|
|
2350
2451
|
await withRetry(
|
|
2351
2452
|
async () => {
|
|
2453
|
+
if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
|
|
2352
2454
|
let timeoutHandle = null;
|
|
2353
2455
|
try {
|
|
2354
2456
|
const result = await Promise.race([
|
|
@@ -2399,6 +2501,7 @@ class AcpBackend {
|
|
|
2399
2501
|
this.log(`[ACP] Creating new session...`);
|
|
2400
2502
|
const sessionResponse = await withRetry(
|
|
2401
2503
|
async () => {
|
|
2504
|
+
if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
|
|
2402
2505
|
let timeoutHandle = null;
|
|
2403
2506
|
try {
|
|
2404
2507
|
const result = await Promise.race([
|
|
@@ -2548,9 +2651,16 @@ class AcpBackend {
|
|
|
2548
2651
|
handleThinkingUpdate(update, ctx);
|
|
2549
2652
|
}
|
|
2550
2653
|
emitIdleStatus() {
|
|
2654
|
+
const resolver = this.idleResolver;
|
|
2655
|
+
this.idleResolver = null;
|
|
2656
|
+
this.waitingForResponse = false;
|
|
2551
2657
|
this.emit({ type: "status", status: "idle" });
|
|
2552
|
-
if (
|
|
2553
|
-
this.
|
|
2658
|
+
if (resolver) {
|
|
2659
|
+
const newPromptInFlight = this.waitingForResponse;
|
|
2660
|
+
resolver();
|
|
2661
|
+
if (newPromptInFlight) {
|
|
2662
|
+
this.waitingForResponse = true;
|
|
2663
|
+
}
|
|
2554
2664
|
}
|
|
2555
2665
|
}
|
|
2556
2666
|
async sendPrompt(sessionId, prompt) {
|
|
@@ -2604,9 +2714,14 @@ class AcpBackend {
|
|
|
2604
2714
|
}
|
|
2605
2715
|
async cancel(sessionId) {
|
|
2606
2716
|
if (!this.connection || !this.acpSessionId) return;
|
|
2717
|
+
this.waitingForResponse = false;
|
|
2718
|
+
if (this.idleResolver) {
|
|
2719
|
+
this.idleResolver();
|
|
2720
|
+
this.idleResolver = null;
|
|
2721
|
+
}
|
|
2607
2722
|
try {
|
|
2608
2723
|
await this.connection.cancel({ sessionId: this.acpSessionId });
|
|
2609
|
-
this.emit({ type: "status", status: "
|
|
2724
|
+
this.emit({ type: "status", status: "cancelled", detail: "Cancelled by user" });
|
|
2610
2725
|
} catch (error) {
|
|
2611
2726
|
this.log("[ACP] Error cancelling:", error);
|
|
2612
2727
|
}
|
|
@@ -2629,16 +2744,24 @@ class AcpBackend {
|
|
|
2629
2744
|
}
|
|
2630
2745
|
}
|
|
2631
2746
|
if (this.process) {
|
|
2632
|
-
|
|
2747
|
+
try {
|
|
2748
|
+
this.process.kill("SIGTERM");
|
|
2749
|
+
} catch {
|
|
2750
|
+
}
|
|
2633
2751
|
await new Promise((resolve) => {
|
|
2634
2752
|
const timeout = setTimeout(() => {
|
|
2635
|
-
|
|
2753
|
+
try {
|
|
2754
|
+
if (this.process) this.process.kill("SIGKILL");
|
|
2755
|
+
} catch {
|
|
2756
|
+
}
|
|
2636
2757
|
resolve();
|
|
2637
2758
|
}, 1e3);
|
|
2638
|
-
|
|
2759
|
+
const done = () => {
|
|
2639
2760
|
clearTimeout(timeout);
|
|
2640
2761
|
resolve();
|
|
2641
|
-
}
|
|
2762
|
+
};
|
|
2763
|
+
this.process?.once("exit", done);
|
|
2764
|
+
this.process?.once("close", done);
|
|
2642
2765
|
});
|
|
2643
2766
|
this.process = null;
|
|
2644
2767
|
}
|
|
@@ -2653,6 +2776,7 @@ class AcpBackend {
|
|
|
2653
2776
|
for (const timeout of this.toolCallTimeouts.values()) clearTimeout(timeout);
|
|
2654
2777
|
this.toolCallTimeouts.clear();
|
|
2655
2778
|
this.toolCallStartTimes.clear();
|
|
2779
|
+
this.toolCallIdToNameMap.clear();
|
|
2656
2780
|
}
|
|
2657
2781
|
}
|
|
2658
2782
|
|
|
@@ -2718,6 +2842,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2718
2842
|
let pendingText = "";
|
|
2719
2843
|
let turnText = "";
|
|
2720
2844
|
let flushTimer = null;
|
|
2845
|
+
let bridgeStopped = false;
|
|
2721
2846
|
function flushText() {
|
|
2722
2847
|
if (pendingText) {
|
|
2723
2848
|
sessionService.pushMessage({
|
|
@@ -2732,6 +2857,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2732
2857
|
}
|
|
2733
2858
|
}
|
|
2734
2859
|
backend.onMessage((msg) => {
|
|
2860
|
+
if (bridgeStopped) return;
|
|
2735
2861
|
switch (msg.type) {
|
|
2736
2862
|
case "model-output": {
|
|
2737
2863
|
if (msg.textDelta) {
|
|
@@ -2763,6 +2889,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2763
2889
|
setMetadata((m) => ({ ...m, lifecycleState: "running" }));
|
|
2764
2890
|
} else if (msg.status === "error") {
|
|
2765
2891
|
flushText();
|
|
2892
|
+
turnText = "";
|
|
2766
2893
|
sessionService.pushMessage(
|
|
2767
2894
|
{ type: "message", message: `Agent process exited unexpectedly: ${msg.detail || "Unknown error"}` },
|
|
2768
2895
|
"event"
|
|
@@ -2771,8 +2898,12 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2771
2898
|
setMetadata((m) => ({ ...m, lifecycleState: "error" }));
|
|
2772
2899
|
} else if (msg.status === "stopped") {
|
|
2773
2900
|
flushText();
|
|
2901
|
+
turnText = "";
|
|
2774
2902
|
sessionService.sendSessionEnd();
|
|
2775
2903
|
setMetadata((m) => ({ ...m, lifecycleState: "stopped" }));
|
|
2904
|
+
} else if (msg.status === "cancelled") {
|
|
2905
|
+
flushText();
|
|
2906
|
+
turnText = "";
|
|
2776
2907
|
}
|
|
2777
2908
|
break;
|
|
2778
2909
|
}
|
|
@@ -2853,6 +2984,14 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
2853
2984
|
}
|
|
2854
2985
|
}
|
|
2855
2986
|
});
|
|
2987
|
+
return () => {
|
|
2988
|
+
bridgeStopped = true;
|
|
2989
|
+
if (flushTimer) {
|
|
2990
|
+
clearTimeout(flushTimer);
|
|
2991
|
+
flushTimer = null;
|
|
2992
|
+
}
|
|
2993
|
+
pendingText = "";
|
|
2994
|
+
};
|
|
2856
2995
|
}
|
|
2857
2996
|
class HyphaPermissionHandler {
|
|
2858
2997
|
constructor(shouldAutoAllow, log) {
|
|
@@ -2918,6 +3057,10 @@ class CodexMcpBackend {
|
|
|
2918
3057
|
client;
|
|
2919
3058
|
transport = null;
|
|
2920
3059
|
disposed = false;
|
|
3060
|
+
turnCancelled = false;
|
|
3061
|
+
// set by cancel() to suppress 'idle' in sendPrompt() finally
|
|
3062
|
+
turnId = 0;
|
|
3063
|
+
// monotonically increasing; each sendPrompt() captures its own ID
|
|
2921
3064
|
codexSessionId = null;
|
|
2922
3065
|
conversationId = null;
|
|
2923
3066
|
svampSessionId = null;
|
|
@@ -2969,7 +3112,10 @@ class CodexMcpBackend {
|
|
|
2969
3112
|
}
|
|
2970
3113
|
async sendPrompt(sessionId, prompt) {
|
|
2971
3114
|
if (!this.connected) throw new Error("Codex not connected");
|
|
3115
|
+
this.turnCancelled = false;
|
|
3116
|
+
const myTurnId = ++this.turnId;
|
|
2972
3117
|
this.emit({ type: "status", status: "running" });
|
|
3118
|
+
let hadError = false;
|
|
2973
3119
|
try {
|
|
2974
3120
|
let response;
|
|
2975
3121
|
if (this.codexSessionId) {
|
|
@@ -2990,16 +3136,20 @@ class CodexMcpBackend {
|
|
|
2990
3136
|
}
|
|
2991
3137
|
}
|
|
2992
3138
|
} catch (err) {
|
|
3139
|
+
hadError = true;
|
|
2993
3140
|
this.log(`[Codex] Error in sendPrompt: ${err.message}`);
|
|
2994
3141
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2995
3142
|
throw err;
|
|
2996
3143
|
} finally {
|
|
2997
|
-
this.
|
|
3144
|
+
if (!this.turnCancelled && !hadError && this.turnId === myTurnId) {
|
|
3145
|
+
this.emit({ type: "status", status: "idle" });
|
|
3146
|
+
}
|
|
2998
3147
|
}
|
|
2999
3148
|
}
|
|
3000
3149
|
async cancel(_sessionId) {
|
|
3001
3150
|
this.log("[Codex] Cancel requested");
|
|
3002
|
-
this.
|
|
3151
|
+
this.turnCancelled = true;
|
|
3152
|
+
this.emit({ type: "status", status: "cancelled" });
|
|
3003
3153
|
}
|
|
3004
3154
|
async respondToPermission(requestId, approved) {
|
|
3005
3155
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -3194,8 +3344,8 @@ class CodexMcpBackend {
|
|
|
3194
3344
|
this.emit({ type: "status", status: "running" });
|
|
3195
3345
|
break;
|
|
3196
3346
|
case "task_complete":
|
|
3347
|
+
break;
|
|
3197
3348
|
case "turn_aborted":
|
|
3198
|
-
this.emit({ type: "status", status: "idle" });
|
|
3199
3349
|
break;
|
|
3200
3350
|
case "agent_message": {
|
|
3201
3351
|
const content = event.content;
|
|
@@ -3603,13 +3753,17 @@ async function verifyNonoIsolation(binaryPath) {
|
|
|
3603
3753
|
"-s",
|
|
3604
3754
|
"--allow",
|
|
3605
3755
|
workDir,
|
|
3606
|
-
|
|
3756
|
+
// NOTE: Do NOT add --allow-cwd here. If the daemon's CWD happens to be
|
|
3757
|
+
// $HOME (common when started interactively), --allow-cwd would grant
|
|
3758
|
+
// access to $HOME, allowing the probe file write to succeed and making
|
|
3759
|
+
// verification incorrectly fail ("file leaked to host filesystem").
|
|
3760
|
+
// We already grant --allow workDir explicitly, so --allow-cwd is redundant.
|
|
3607
3761
|
"--trust-override",
|
|
3608
3762
|
"--",
|
|
3609
3763
|
"sh",
|
|
3610
3764
|
"-c",
|
|
3611
3765
|
testScript
|
|
3612
|
-
], { timeout: 15e3 });
|
|
3766
|
+
], { timeout: 15e3, cwd: workDir });
|
|
3613
3767
|
return parseIsolationTestOutput(stdout, probeFile);
|
|
3614
3768
|
} catch (e) {
|
|
3615
3769
|
return { passed: false, error: e.message };
|
|
@@ -3827,7 +3981,10 @@ class ProcessSupervisor {
|
|
|
3827
3981
|
/** Start a stopped/failed process by id or name. */
|
|
3828
3982
|
async start(idOrName) {
|
|
3829
3983
|
const entry = this.require(idOrName);
|
|
3830
|
-
if (entry.child
|
|
3984
|
+
if (entry.child) {
|
|
3985
|
+
if (entry.stopping) throw new Error(`Process '${entry.spec.name}' is being stopped, try again shortly`);
|
|
3986
|
+
throw new Error(`Process '${entry.spec.name}' is already running`);
|
|
3987
|
+
}
|
|
3831
3988
|
entry.stopping = false;
|
|
3832
3989
|
await this.startEntry(entry, false);
|
|
3833
3990
|
}
|
|
@@ -3847,15 +4004,21 @@ class ProcessSupervisor {
|
|
|
3847
4004
|
/** Restart a process (stop if running, then start again). */
|
|
3848
4005
|
async restart(idOrName) {
|
|
3849
4006
|
const entry = this.require(idOrName);
|
|
3850
|
-
if (entry.
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
4007
|
+
if (entry.restarting) return;
|
|
4008
|
+
entry.restarting = true;
|
|
4009
|
+
try {
|
|
4010
|
+
if (entry.child) {
|
|
4011
|
+
entry.stopping = true;
|
|
4012
|
+
this.clearTimers(entry);
|
|
4013
|
+
await this.killChild(entry.child);
|
|
4014
|
+
entry.child = void 0;
|
|
4015
|
+
}
|
|
4016
|
+
entry.stopping = false;
|
|
4017
|
+
entry.state.restartCount++;
|
|
4018
|
+
await this.startEntry(entry, false);
|
|
4019
|
+
} finally {
|
|
4020
|
+
entry.restarting = false;
|
|
3855
4021
|
}
|
|
3856
|
-
entry.stopping = false;
|
|
3857
|
-
entry.state.restartCount++;
|
|
3858
|
-
await this.startEntry(entry, false);
|
|
3859
4022
|
}
|
|
3860
4023
|
/** Stop the process and remove it from supervision (deletes persisted spec). */
|
|
3861
4024
|
async remove(idOrName) {
|
|
@@ -3989,7 +4152,9 @@ class ProcessSupervisor {
|
|
|
3989
4152
|
}
|
|
3990
4153
|
async persistSpec(spec) {
|
|
3991
4154
|
const filePath = path.join(this.persistDir, `${spec.id}.json`);
|
|
3992
|
-
|
|
4155
|
+
const tmpPath = filePath + ".tmp";
|
|
4156
|
+
await writeFile(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4157
|
+
await rename(tmpPath, filePath);
|
|
3993
4158
|
}
|
|
3994
4159
|
async deleteSpec(id) {
|
|
3995
4160
|
try {
|
|
@@ -4063,7 +4228,13 @@ class ProcessSupervisor {
|
|
|
4063
4228
|
};
|
|
4064
4229
|
child.stdout?.on("data", appendLog);
|
|
4065
4230
|
child.stderr?.on("data", appendLog);
|
|
4066
|
-
child.on("
|
|
4231
|
+
child.on("error", (err) => {
|
|
4232
|
+
console.error(`[SUPERVISOR] Process '${spec.name}' error: ${err.message}`);
|
|
4233
|
+
});
|
|
4234
|
+
child.on("close", (code, signal) => {
|
|
4235
|
+
if (entry.child !== child) return;
|
|
4236
|
+
this.onProcessExit(entry, code, signal);
|
|
4237
|
+
});
|
|
4067
4238
|
if (spec.probe) this.setupProbe(entry);
|
|
4068
4239
|
if (spec.ttl !== void 0) this.setupTTL(entry);
|
|
4069
4240
|
console.log(`[SUPERVISOR] Started '${spec.name}' pid=${child.pid}`);
|
|
@@ -4151,6 +4322,8 @@ class ProcessSupervisor {
|
|
|
4151
4322
|
}
|
|
4152
4323
|
}
|
|
4153
4324
|
async triggerProbeRestart(entry) {
|
|
4325
|
+
if (entry.restarting) return;
|
|
4326
|
+
if (entry.stopping) return;
|
|
4154
4327
|
console.warn(`[SUPERVISOR] Restarting '${entry.spec.name}' due to probe failures`);
|
|
4155
4328
|
entry.state.consecutiveProbeFailures = 0;
|
|
4156
4329
|
this.clearTimers(entry);
|
|
@@ -4175,6 +4348,7 @@ class ProcessSupervisor {
|
|
|
4175
4348
|
console.log(`[SUPERVISOR] Process '${entry.spec.name}' TTL expired`);
|
|
4176
4349
|
entry.state.status = "expired";
|
|
4177
4350
|
entry.stopping = true;
|
|
4351
|
+
this.clearTimers(entry);
|
|
4178
4352
|
const cleanup = async () => {
|
|
4179
4353
|
if (entry.child) await this.killChild(entry.child);
|
|
4180
4354
|
this.entries.delete(entry.spec.id);
|
|
@@ -4186,13 +4360,36 @@ class ProcessSupervisor {
|
|
|
4186
4360
|
// ── Process kill helper ───────────────────────────────────────────────────
|
|
4187
4361
|
killChild(child) {
|
|
4188
4362
|
return new Promise((resolve) => {
|
|
4189
|
-
|
|
4363
|
+
let resolved = false;
|
|
4364
|
+
let forceKillTimer;
|
|
4365
|
+
let hardDeadlineTimer;
|
|
4366
|
+
const done = () => {
|
|
4367
|
+
if (!resolved) {
|
|
4368
|
+
resolved = true;
|
|
4369
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
4370
|
+
if (hardDeadlineTimer) clearTimeout(hardDeadlineTimer);
|
|
4371
|
+
resolve();
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4190
4374
|
child.once("exit", done);
|
|
4191
|
-
child.
|
|
4192
|
-
|
|
4193
|
-
child.kill("
|
|
4375
|
+
child.once("close", done);
|
|
4376
|
+
try {
|
|
4377
|
+
child.kill("SIGTERM");
|
|
4378
|
+
} catch {
|
|
4379
|
+
}
|
|
4380
|
+
forceKillTimer = setTimeout(() => {
|
|
4381
|
+
try {
|
|
4382
|
+
child.kill("SIGKILL");
|
|
4383
|
+
} catch {
|
|
4384
|
+
}
|
|
4385
|
+
hardDeadlineTimer = setTimeout(() => {
|
|
4386
|
+
if (!resolved) {
|
|
4387
|
+
resolved = true;
|
|
4388
|
+
console.warn(`[SUPERVISOR] Process pid=${child.pid} did not exit after SIGKILL \u2014 forcing resolution`);
|
|
4389
|
+
resolve();
|
|
4390
|
+
}
|
|
4391
|
+
}, 2e3);
|
|
4194
4392
|
}, 5e3);
|
|
4195
|
-
child.once("exit", () => clearTimeout(forceKill));
|
|
4196
4393
|
});
|
|
4197
4394
|
}
|
|
4198
4395
|
// ── Timer cleanup ─────────────────────────────────────────────────────────
|
|
@@ -4214,6 +4411,83 @@ class ProcessSupervisor {
|
|
|
4214
4411
|
|
|
4215
4412
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
4216
4413
|
const __dirname$1 = dirname(__filename$1);
|
|
4414
|
+
const CLAUDE_SKILLS_DIR = join(os__default.homedir(), ".claude", "skills");
|
|
4415
|
+
async function installSkillFromEndpoint(name, baseUrl) {
|
|
4416
|
+
const resp = await fetch(baseUrl, { signal: AbortSignal.timeout(15e3) });
|
|
4417
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${baseUrl}`);
|
|
4418
|
+
const index = await resp.json();
|
|
4419
|
+
const files = index.files || [];
|
|
4420
|
+
if (files.length === 0) throw new Error(`Skill index at ${baseUrl} has no files`);
|
|
4421
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, name);
|
|
4422
|
+
mkdirSync(targetDir, { recursive: true });
|
|
4423
|
+
for (const filePath of files) {
|
|
4424
|
+
if (!filePath) continue;
|
|
4425
|
+
const url = `${baseUrl}${filePath}`;
|
|
4426
|
+
const fileResp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
4427
|
+
if (!fileResp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${fileResp.status}`);
|
|
4428
|
+
const content = await fileResp.text();
|
|
4429
|
+
const localPath = join(targetDir, filePath);
|
|
4430
|
+
if (!localPath.startsWith(targetDir + "/")) continue;
|
|
4431
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
4432
|
+
writeFileSync(localPath, content, "utf-8");
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
async function installSkillFromMarketplace(name) {
|
|
4436
|
+
const BASE = `https://hypha.aicell.io/hypha-cloud/artifacts/${name}`;
|
|
4437
|
+
async function collectFiles(dir = "") {
|
|
4438
|
+
const url = dir ? `${BASE}/files/${dir}` : `${BASE}/files/`;
|
|
4439
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
4440
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} listing files`);
|
|
4441
|
+
const data = await resp.json();
|
|
4442
|
+
const items = Array.isArray(data) ? data : data.items || [];
|
|
4443
|
+
const result = [];
|
|
4444
|
+
for (const item of items) {
|
|
4445
|
+
const itemPath = dir ? `${dir}/${item.name}` : item.name;
|
|
4446
|
+
if (item.type === "directory") {
|
|
4447
|
+
result.push(...await collectFiles(itemPath));
|
|
4448
|
+
} else {
|
|
4449
|
+
result.push(itemPath);
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
return result;
|
|
4453
|
+
}
|
|
4454
|
+
const files = await collectFiles();
|
|
4455
|
+
if (files.length === 0) throw new Error(`Skill ${name} has no files in marketplace`);
|
|
4456
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, name);
|
|
4457
|
+
mkdirSync(targetDir, { recursive: true });
|
|
4458
|
+
for (const filePath of files) {
|
|
4459
|
+
const url = `${BASE}/files/${filePath}`;
|
|
4460
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
4461
|
+
if (!resp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${resp.status}`);
|
|
4462
|
+
const content = await resp.text();
|
|
4463
|
+
const localPath = join(targetDir, filePath);
|
|
4464
|
+
if (!localPath.startsWith(targetDir + "/")) continue;
|
|
4465
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
4466
|
+
writeFileSync(localPath, content, "utf-8");
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
async function ensureAutoInstalledSkills(logger) {
|
|
4470
|
+
const tasks = [
|
|
4471
|
+
{
|
|
4472
|
+
name: "svamp",
|
|
4473
|
+
install: () => installSkillFromMarketplace("svamp")
|
|
4474
|
+
},
|
|
4475
|
+
{
|
|
4476
|
+
name: "hypha",
|
|
4477
|
+
install: () => installSkillFromEndpoint("hypha", "https://hypha.aicell.io/ws/agent-skills/")
|
|
4478
|
+
}
|
|
4479
|
+
];
|
|
4480
|
+
for (const task of tasks) {
|
|
4481
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, task.name);
|
|
4482
|
+
if (existsSync$1(targetDir)) continue;
|
|
4483
|
+
try {
|
|
4484
|
+
await task.install();
|
|
4485
|
+
logger.log(`[skills] Auto-installed: ${task.name}`);
|
|
4486
|
+
} catch (err) {
|
|
4487
|
+
logger.log(`[skills] Auto-install of "${task.name}" failed (non-fatal): ${err.message}`);
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4217
4491
|
function loadEnvFile(path) {
|
|
4218
4492
|
if (!existsSync$1(path)) return false;
|
|
4219
4493
|
const lines = readFileSync$1(path, "utf-8").split("\n");
|
|
@@ -4290,7 +4564,9 @@ function readSvampConfig(configPath) {
|
|
|
4290
4564
|
function writeSvampConfig(configPath, config) {
|
|
4291
4565
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
4292
4566
|
const content = JSON.stringify(config, null, 2);
|
|
4293
|
-
|
|
4567
|
+
const tmpPath = configPath + ".tmp";
|
|
4568
|
+
writeFileSync(tmpPath, content);
|
|
4569
|
+
renameSync(tmpPath, configPath);
|
|
4294
4570
|
return content;
|
|
4295
4571
|
}
|
|
4296
4572
|
function getRalphStateFilePath(directory, sessionId) {
|
|
@@ -4344,7 +4620,9 @@ started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResu
|
|
|
4344
4620
|
|
|
4345
4621
|
${state.task}
|
|
4346
4622
|
`;
|
|
4347
|
-
|
|
4623
|
+
const tmpPath = `${filePath}.tmp`;
|
|
4624
|
+
writeFileSync(tmpPath, content);
|
|
4625
|
+
renameSync(tmpPath, filePath);
|
|
4348
4626
|
}
|
|
4349
4627
|
function removeRalphState(filePath) {
|
|
4350
4628
|
try {
|
|
@@ -4433,20 +4711,17 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
4433
4711
|
ralphSystemPrompt: ralphSysPrompt
|
|
4434
4712
|
}]
|
|
4435
4713
|
}));
|
|
4436
|
-
sessionService.updateMetadata(getMetadata());
|
|
4437
4714
|
sessionService.pushMessage(
|
|
4438
|
-
{ type: "message", message: buildIterationStatus(1, state.max_iterations, state.completion_promise) },
|
|
4715
|
+
{ type: "message", message: buildIterationStatus(state.iteration + 1, state.max_iterations, state.completion_promise) },
|
|
4439
4716
|
"event"
|
|
4440
4717
|
);
|
|
4441
|
-
logger.log(`[svampConfig] Ralph loop started: "${state.task.slice(0, 50)}..."`);
|
|
4718
|
+
logger.log(`[svampConfig] Ralph loop started/resumed at iteration ${state.iteration + 1}: "${state.task.slice(0, 50)}..."`);
|
|
4442
4719
|
onRalphLoopActivated?.();
|
|
4443
4720
|
} else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
|
|
4444
4721
|
setMetadata((m) => ({ ...m, ralphLoop }));
|
|
4445
|
-
sessionService.updateMetadata(getMetadata());
|
|
4446
4722
|
}
|
|
4447
4723
|
} else if (prevRalph?.active) {
|
|
4448
4724
|
setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
|
|
4449
|
-
sessionService.updateMetadata(getMetadata());
|
|
4450
4725
|
sessionService.pushMessage(
|
|
4451
4726
|
{ type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
|
|
4452
4727
|
"event"
|
|
@@ -4679,18 +4954,19 @@ function loadSessionIndex() {
|
|
|
4679
4954
|
}
|
|
4680
4955
|
}
|
|
4681
4956
|
function saveSessionIndex(index) {
|
|
4682
|
-
|
|
4957
|
+
const tmp = SESSION_INDEX_FILE + ".tmp";
|
|
4958
|
+
writeFileSync(tmp, JSON.stringify(index, null, 2), "utf-8");
|
|
4959
|
+
renameSync(tmp, SESSION_INDEX_FILE);
|
|
4683
4960
|
}
|
|
4684
4961
|
function saveSession(session) {
|
|
4685
4962
|
const sessionDir = getSessionDir(session.directory, session.sessionId);
|
|
4686
4963
|
if (!existsSync$1(sessionDir)) {
|
|
4687
4964
|
mkdirSync(sessionDir, { recursive: true });
|
|
4688
4965
|
}
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
);
|
|
4966
|
+
const filePath = getSessionFilePath(session.directory, session.sessionId);
|
|
4967
|
+
const tmpPath = filePath + ".tmp";
|
|
4968
|
+
writeFileSync(tmpPath, JSON.stringify(session, null, 2), "utf-8");
|
|
4969
|
+
renameSync(tmpPath, filePath);
|
|
4694
4970
|
const index = loadSessionIndex();
|
|
4695
4971
|
index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
|
|
4696
4972
|
saveSessionIndex(index);
|
|
@@ -4714,6 +4990,16 @@ function deletePersistedSession(sessionId) {
|
|
|
4714
4990
|
if (existsSync$1(configFile)) unlinkSync(configFile);
|
|
4715
4991
|
} catch {
|
|
4716
4992
|
}
|
|
4993
|
+
const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
|
|
4994
|
+
try {
|
|
4995
|
+
if (existsSync$1(ralphStateFile)) unlinkSync(ralphStateFile);
|
|
4996
|
+
} catch {
|
|
4997
|
+
}
|
|
4998
|
+
const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
|
|
4999
|
+
try {
|
|
5000
|
+
if (existsSync$1(ralphProgressFile)) unlinkSync(ralphProgressFile);
|
|
5001
|
+
} catch {
|
|
5002
|
+
}
|
|
4717
5003
|
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
4718
5004
|
try {
|
|
4719
5005
|
rmdirSync(sessionDir);
|
|
@@ -4779,7 +5065,9 @@ function createLogger() {
|
|
|
4779
5065
|
}
|
|
4780
5066
|
function writeDaemonStateFile(state) {
|
|
4781
5067
|
ensureHomeDir();
|
|
4782
|
-
|
|
5068
|
+
const tmpPath = DAEMON_STATE_FILE + ".tmp";
|
|
5069
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
5070
|
+
renameSync(tmpPath, DAEMON_STATE_FILE);
|
|
4783
5071
|
}
|
|
4784
5072
|
function readDaemonStateFile() {
|
|
4785
5073
|
try {
|
|
@@ -4964,6 +5252,8 @@ async function startDaemon(options) {
|
|
|
4964
5252
|
let server = null;
|
|
4965
5253
|
const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
|
|
4966
5254
|
await supervisor.init();
|
|
5255
|
+
ensureAutoInstalledSkills(logger).catch(() => {
|
|
5256
|
+
});
|
|
4967
5257
|
try {
|
|
4968
5258
|
logger.log("Connecting to Hypha server...");
|
|
4969
5259
|
server = await connectToHypha({
|
|
@@ -5022,6 +5312,7 @@ async function startDaemon(options) {
|
|
|
5022
5312
|
if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
|
|
5023
5313
|
return await spawnAgentSession(sessionId, directory, agentName, options2, resumeSessionId);
|
|
5024
5314
|
}
|
|
5315
|
+
let stagedCredentials = null;
|
|
5025
5316
|
try {
|
|
5026
5317
|
let parseBashPermission2 = function(permission) {
|
|
5027
5318
|
if (permission === "Bash") return;
|
|
@@ -5055,17 +5346,23 @@ async function startDaemon(options) {
|
|
|
5055
5346
|
resolve2();
|
|
5056
5347
|
return;
|
|
5057
5348
|
}
|
|
5349
|
+
let settled = false;
|
|
5350
|
+
const done = () => {
|
|
5351
|
+
if (settled) return;
|
|
5352
|
+
settled = true;
|
|
5353
|
+
clearTimeout(timeout);
|
|
5354
|
+
proc.off("exit", exitHandler);
|
|
5355
|
+
resolve2();
|
|
5356
|
+
};
|
|
5058
5357
|
const timeout = setTimeout(() => {
|
|
5059
5358
|
try {
|
|
5060
5359
|
proc.kill("SIGKILL");
|
|
5061
5360
|
} catch {
|
|
5062
5361
|
}
|
|
5063
|
-
|
|
5362
|
+
done();
|
|
5064
5363
|
}, timeoutMs);
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
resolve2();
|
|
5068
|
-
});
|
|
5364
|
+
const exitHandler = () => done();
|
|
5365
|
+
proc.on("exit", exitHandler);
|
|
5069
5366
|
if (!proc.killed) {
|
|
5070
5367
|
proc.kill(signal);
|
|
5071
5368
|
}
|
|
@@ -5113,7 +5410,8 @@ async function startDaemon(options) {
|
|
|
5113
5410
|
sharing: options2.sharing,
|
|
5114
5411
|
securityContext: options2.securityContext,
|
|
5115
5412
|
tags: options2.tags,
|
|
5116
|
-
parentSessionId: options2.parentSessionId
|
|
5413
|
+
parentSessionId: options2.parentSessionId,
|
|
5414
|
+
...options2.injectPlatformGuidance !== void 0 && { injectPlatformGuidance: options2.injectPlatformGuidance }
|
|
5117
5415
|
};
|
|
5118
5416
|
let claudeProcess = null;
|
|
5119
5417
|
const allPersisted = loadPersistedSessions();
|
|
@@ -5128,6 +5426,9 @@ async function startDaemon(options) {
|
|
|
5128
5426
|
const newState = processing ? "running" : "idle";
|
|
5129
5427
|
if (sessionMetadata.lifecycleState !== newState) {
|
|
5130
5428
|
sessionMetadata = { ...sessionMetadata, lifecycleState: newState };
|
|
5429
|
+
if (!processing) {
|
|
5430
|
+
sessionMetadata = { ...sessionMetadata, unread: true };
|
|
5431
|
+
}
|
|
5131
5432
|
sessionService.updateMetadata(sessionMetadata);
|
|
5132
5433
|
}
|
|
5133
5434
|
};
|
|
@@ -5159,6 +5460,8 @@ async function startDaemon(options) {
|
|
|
5159
5460
|
let userMessagePending = false;
|
|
5160
5461
|
let turnInitiatedByUser = true;
|
|
5161
5462
|
let isKillingClaude = false;
|
|
5463
|
+
let isRestartingClaude = false;
|
|
5464
|
+
let isSwitchingMode = false;
|
|
5162
5465
|
let checkSvampConfig;
|
|
5163
5466
|
let cleanupSvampConfig;
|
|
5164
5467
|
const CLAUDE_PERMISSION_MODE_MAP = {
|
|
@@ -5166,7 +5469,6 @@ async function startDaemon(options) {
|
|
|
5166
5469
|
};
|
|
5167
5470
|
const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
|
|
5168
5471
|
let isolationCleanupFiles = [];
|
|
5169
|
-
let stagedCredentials = null;
|
|
5170
5472
|
const spawnClaude = (initialMessage, meta) => {
|
|
5171
5473
|
const effectiveMeta = { ...lastSpawnMeta, ...meta };
|
|
5172
5474
|
let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
@@ -5234,12 +5536,17 @@ async function startDaemon(options) {
|
|
|
5234
5536
|
});
|
|
5235
5537
|
claudeProcess = child;
|
|
5236
5538
|
logger.log(`[Session ${sessionId}] Claude PID: ${child.pid}, stdin: ${!!child.stdin}, stdout: ${!!child.stdout}, stderr: ${!!child.stderr}`);
|
|
5539
|
+
child.stdin?.on("error", (err) => {
|
|
5540
|
+
logger.log(`[Session ${sessionId}] Claude stdin error: ${err.message}`);
|
|
5541
|
+
});
|
|
5237
5542
|
child.on("error", (err) => {
|
|
5238
5543
|
logger.log(`[Session ${sessionId}] Claude process error: ${err.message}`);
|
|
5239
5544
|
sessionService.pushMessage(
|
|
5240
5545
|
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure Claude Code CLI is installed.` },
|
|
5241
5546
|
"event"
|
|
5242
5547
|
);
|
|
5548
|
+
sessionWasProcessing = false;
|
|
5549
|
+
claudeProcess = null;
|
|
5243
5550
|
signalProcessing(false);
|
|
5244
5551
|
sessionService.sendSessionEnd();
|
|
5245
5552
|
});
|
|
@@ -5351,7 +5658,7 @@ async function startDaemon(options) {
|
|
|
5351
5658
|
}
|
|
5352
5659
|
const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
5353
5660
|
if (textBlocks.length > 0) {
|
|
5354
|
-
lastAssistantText
|
|
5661
|
+
lastAssistantText += textBlocks.join("\n");
|
|
5355
5662
|
}
|
|
5356
5663
|
}
|
|
5357
5664
|
if (msg.type === "result") {
|
|
@@ -5389,9 +5696,12 @@ async function startDaemon(options) {
|
|
|
5389
5696
|
turnInitiatedByUser = true;
|
|
5390
5697
|
continue;
|
|
5391
5698
|
}
|
|
5699
|
+
if (msg.session_id) {
|
|
5700
|
+
claudeResumeId = msg.session_id;
|
|
5701
|
+
}
|
|
5392
5702
|
signalProcessing(false);
|
|
5393
5703
|
sessionWasProcessing = false;
|
|
5394
|
-
if (claudeResumeId) {
|
|
5704
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
5395
5705
|
saveSession({
|
|
5396
5706
|
sessionId,
|
|
5397
5707
|
directory,
|
|
@@ -5411,7 +5721,7 @@ async function startDaemon(options) {
|
|
|
5411
5721
|
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
5412
5722
|
}
|
|
5413
5723
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5414
|
-
if (queueLen > 0 && claudeResumeId) {
|
|
5724
|
+
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5415
5725
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
5416
5726
|
} else if (claudeResumeId) {
|
|
5417
5727
|
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
@@ -5435,6 +5745,7 @@ async function startDaemon(options) {
|
|
|
5435
5745
|
logger.log(`[Session ${sessionId}] ${reason}`);
|
|
5436
5746
|
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
5437
5747
|
if (isFreshMode && rlState.original_resume_id) {
|
|
5748
|
+
claudeResumeId = rlState.original_resume_id;
|
|
5438
5749
|
(async () => {
|
|
5439
5750
|
try {
|
|
5440
5751
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
@@ -5442,7 +5753,8 @@ async function startDaemon(options) {
|
|
|
5442
5753
|
await killAndWaitForExit2(claudeProcess);
|
|
5443
5754
|
isKillingClaude = false;
|
|
5444
5755
|
}
|
|
5445
|
-
|
|
5756
|
+
if (trackedSession.stopped) return;
|
|
5757
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5446
5758
|
const progressPath = getRalphProgressFilePath(directory, sessionId);
|
|
5447
5759
|
let resumeMessage;
|
|
5448
5760
|
try {
|
|
@@ -5482,7 +5794,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5482
5794
|
if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
|
|
5483
5795
|
updatedRlState.original_resume_id = claudeResumeId;
|
|
5484
5796
|
}
|
|
5485
|
-
|
|
5797
|
+
try {
|
|
5798
|
+
writeRalphState(getRalphStateFilePath(directory, sessionId), updatedRlState);
|
|
5799
|
+
} catch (writeErr) {
|
|
5800
|
+
logger.log(`[Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
5801
|
+
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
5802
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
5803
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
5804
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
5805
|
+
break;
|
|
5806
|
+
}
|
|
5486
5807
|
const ralphLoop = {
|
|
5487
5808
|
active: true,
|
|
5488
5809
|
task: rlState.task,
|
|
@@ -5502,13 +5823,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5502
5823
|
const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
|
|
5503
5824
|
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
5504
5825
|
if (isFreshMode) {
|
|
5826
|
+
isKillingClaude = true;
|
|
5505
5827
|
setTimeout(async () => {
|
|
5506
5828
|
try {
|
|
5507
5829
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5508
|
-
isKillingClaude = true;
|
|
5509
5830
|
await killAndWaitForExit2(claudeProcess);
|
|
5510
|
-
isKillingClaude = false;
|
|
5511
5831
|
}
|
|
5832
|
+
isKillingClaude = false;
|
|
5833
|
+
if (trackedSession.stopped) return;
|
|
5834
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5512
5835
|
claudeResumeId = void 0;
|
|
5513
5836
|
userMessagePending = true;
|
|
5514
5837
|
turnInitiatedByUser = true;
|
|
@@ -5520,25 +5843,34 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5520
5843
|
} catch (err) {
|
|
5521
5844
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
|
|
5522
5845
|
isKillingClaude = false;
|
|
5846
|
+
sessionWasProcessing = false;
|
|
5523
5847
|
signalProcessing(false);
|
|
5524
5848
|
}
|
|
5525
5849
|
}, cooldownMs);
|
|
5526
5850
|
} else {
|
|
5527
5851
|
setTimeout(() => {
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5852
|
+
if (trackedSession.stopped) return;
|
|
5853
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5854
|
+
try {
|
|
5855
|
+
userMessagePending = true;
|
|
5856
|
+
turnInitiatedByUser = true;
|
|
5857
|
+
sessionWasProcessing = true;
|
|
5858
|
+
signalProcessing(true);
|
|
5859
|
+
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
5860
|
+
sessionService.pushMessage(rlState.task, "user");
|
|
5861
|
+
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5862
|
+
const stdinMsg = JSON.stringify({
|
|
5863
|
+
type: "user",
|
|
5864
|
+
message: { role: "user", content: prompt }
|
|
5865
|
+
});
|
|
5866
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
5867
|
+
} else {
|
|
5868
|
+
spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
|
|
5869
|
+
}
|
|
5870
|
+
} catch (err) {
|
|
5871
|
+
logger.log(`[Session ${sessionId}] Error in continue Ralph iteration: ${err.message}`);
|
|
5872
|
+
sessionWasProcessing = false;
|
|
5873
|
+
signalProcessing(false);
|
|
5542
5874
|
}
|
|
5543
5875
|
}, cooldownMs);
|
|
5544
5876
|
}
|
|
@@ -5553,10 +5885,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5553
5885
|
}
|
|
5554
5886
|
}
|
|
5555
5887
|
sessionService.pushMessage(msg, "agent");
|
|
5556
|
-
if (msg.session_id) {
|
|
5557
|
-
claudeResumeId = msg.session_id;
|
|
5558
|
-
}
|
|
5559
5888
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
5889
|
+
lastAssistantText = "";
|
|
5560
5890
|
if (!userMessagePending) {
|
|
5561
5891
|
turnInitiatedByUser = false;
|
|
5562
5892
|
logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
|
|
@@ -5567,18 +5897,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5567
5897
|
claudeResumeId = msg.session_id;
|
|
5568
5898
|
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
5569
5899
|
sessionService.updateMetadata(sessionMetadata);
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5900
|
+
if (!trackedSession.stopped) {
|
|
5901
|
+
saveSession({
|
|
5902
|
+
sessionId,
|
|
5903
|
+
directory,
|
|
5904
|
+
claudeResumeId,
|
|
5905
|
+
permissionMode: currentPermissionMode,
|
|
5906
|
+
spawnMeta: lastSpawnMeta,
|
|
5907
|
+
metadata: sessionMetadata,
|
|
5908
|
+
createdAt: Date.now(),
|
|
5909
|
+
machineId,
|
|
5910
|
+
wasProcessing: sessionWasProcessing
|
|
5911
|
+
});
|
|
5912
|
+
artifactSync.scheduleDebouncedSync(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId);
|
|
5913
|
+
}
|
|
5582
5914
|
if (isConversationClear) {
|
|
5583
5915
|
logger.log(`[Session ${sessionId}] Conversation cleared (/clear) \u2014 new Claude session: ${msg.session_id}`);
|
|
5584
5916
|
sessionService.clearMessages();
|
|
@@ -5604,6 +5936,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5604
5936
|
}
|
|
5605
5937
|
}
|
|
5606
5938
|
});
|
|
5939
|
+
child.stdout?.on("close", () => {
|
|
5940
|
+
const remaining = stdoutBuffer.trim();
|
|
5941
|
+
if (remaining) {
|
|
5942
|
+
logger.log(`[Session ${sessionId}] stdout close with remaining buffer (${remaining.length} chars): ${remaining.slice(0, 200)}`);
|
|
5943
|
+
try {
|
|
5944
|
+
const msg = JSON.parse(remaining);
|
|
5945
|
+
sessionService.pushMessage(msg, "agent");
|
|
5946
|
+
} catch {
|
|
5947
|
+
logger.log(`[Session ${sessionId}] Discarding non-JSON stdout remainder on close`);
|
|
5948
|
+
}
|
|
5949
|
+
stdoutBuffer = "";
|
|
5950
|
+
}
|
|
5951
|
+
});
|
|
5607
5952
|
let stderrBuffer = "";
|
|
5608
5953
|
child.stderr?.on("data", (chunk) => {
|
|
5609
5954
|
const text = chunk.toString();
|
|
@@ -5633,7 +5978,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5633
5978
|
sessionService.updateMetadata(sessionMetadata);
|
|
5634
5979
|
sessionWasProcessing = false;
|
|
5635
5980
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5636
|
-
if (queueLen > 0 && claudeResumeId) {
|
|
5981
|
+
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5637
5982
|
signalProcessing(false);
|
|
5638
5983
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
5639
5984
|
} else {
|
|
@@ -5667,6 +6012,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5667
6012
|
};
|
|
5668
6013
|
const restartClaudeHandler = async () => {
|
|
5669
6014
|
logger.log(`[Session ${sessionId}] Restart Claude requested`);
|
|
6015
|
+
if (isRestartingClaude || isSwitchingMode) {
|
|
6016
|
+
return { success: false, message: "Restart already in progress." };
|
|
6017
|
+
}
|
|
6018
|
+
isRestartingClaude = true;
|
|
5670
6019
|
try {
|
|
5671
6020
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
5672
6021
|
isKillingClaude = true;
|
|
@@ -5675,6 +6024,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5675
6024
|
await killAndWaitForExit2(claudeProcess);
|
|
5676
6025
|
isKillingClaude = false;
|
|
5677
6026
|
}
|
|
6027
|
+
if (trackedSession?.stopped) {
|
|
6028
|
+
return { success: false, message: "Session was stopped during restart." };
|
|
6029
|
+
}
|
|
5678
6030
|
if (claudeResumeId) {
|
|
5679
6031
|
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
5680
6032
|
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
@@ -5687,9 +6039,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5687
6039
|
isKillingClaude = false;
|
|
5688
6040
|
logger.log(`[Session ${sessionId}] Restart failed: ${err.message}`);
|
|
5689
6041
|
return { success: false, message: `Restart failed: ${err.message}` };
|
|
6042
|
+
} finally {
|
|
6043
|
+
isRestartingClaude = false;
|
|
5690
6044
|
}
|
|
5691
6045
|
};
|
|
5692
|
-
if (sessionMetadata.sharing?.enabled
|
|
6046
|
+
if (sessionMetadata.sharing?.enabled) {
|
|
5693
6047
|
try {
|
|
5694
6048
|
stagedCredentials = await stageCredentialsForSharing(sessionId);
|
|
5695
6049
|
logger.log(`[Session ${sessionId}] Credentials staged at ${stagedCredentials.homePath}`);
|
|
@@ -5705,6 +6059,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5705
6059
|
{ controlledByUser: false },
|
|
5706
6060
|
{
|
|
5707
6061
|
onUserMessage: (content, meta) => {
|
|
6062
|
+
if (trackedSession?.stopped) return;
|
|
5708
6063
|
logger.log(`[Session ${sessionId}] User message received`);
|
|
5709
6064
|
userMessagePending = true;
|
|
5710
6065
|
turnInitiatedByUser = true;
|
|
@@ -5731,7 +6086,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5731
6086
|
if (msgMeta) {
|
|
5732
6087
|
lastSpawnMeta = { ...lastSpawnMeta, ...msgMeta };
|
|
5733
6088
|
}
|
|
5734
|
-
if (isKillingClaude) {
|
|
6089
|
+
if (isKillingClaude || isRestartingClaude || isSwitchingMode) {
|
|
5735
6090
|
logger.log(`[Session ${sessionId}] Message received while restarting Claude, queuing to prevent loss`);
|
|
5736
6091
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
5737
6092
|
sessionMetadata = {
|
|
@@ -5785,6 +6140,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5785
6140
|
if (params.mode) {
|
|
5786
6141
|
currentPermissionMode = toClaudePermissionMode(params.mode);
|
|
5787
6142
|
logger.log(`[Session ${sessionId}] Permission mode changed to: ${currentPermissionMode}`);
|
|
6143
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
6144
|
+
saveSession({
|
|
6145
|
+
sessionId,
|
|
6146
|
+
directory,
|
|
6147
|
+
claudeResumeId,
|
|
6148
|
+
permissionMode: currentPermissionMode,
|
|
6149
|
+
spawnMeta: lastSpawnMeta,
|
|
6150
|
+
metadata: sessionMetadata,
|
|
6151
|
+
createdAt: Date.now(),
|
|
6152
|
+
machineId,
|
|
6153
|
+
wasProcessing: sessionWasProcessing
|
|
6154
|
+
});
|
|
6155
|
+
}
|
|
5788
6156
|
}
|
|
5789
6157
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
5790
6158
|
for (const tool of params.allowTools) {
|
|
@@ -5815,12 +6183,23 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5815
6183
|
},
|
|
5816
6184
|
onSwitchMode: async (mode) => {
|
|
5817
6185
|
logger.log(`[Session ${sessionId}] Switch mode: ${mode}`);
|
|
6186
|
+
if (isRestartingClaude || isSwitchingMode) {
|
|
6187
|
+
logger.log(`[Session ${sessionId}] Switch mode deferred \u2014 restart/switch already in progress`);
|
|
6188
|
+
return;
|
|
6189
|
+
}
|
|
5818
6190
|
currentPermissionMode = mode;
|
|
5819
6191
|
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
6192
|
+
isSwitchingMode = true;
|
|
5820
6193
|
isKillingClaude = true;
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
6194
|
+
try {
|
|
6195
|
+
await killAndWaitForExit2(claudeProcess);
|
|
6196
|
+
isKillingClaude = false;
|
|
6197
|
+
if (trackedSession?.stopped) return;
|
|
6198
|
+
spawnClaude(void 0, { permissionMode: mode });
|
|
6199
|
+
} finally {
|
|
6200
|
+
isKillingClaude = false;
|
|
6201
|
+
isSwitchingMode = false;
|
|
6202
|
+
}
|
|
5824
6203
|
}
|
|
5825
6204
|
},
|
|
5826
6205
|
onRestartClaude: restartClaudeHandler,
|
|
@@ -5841,11 +6220,15 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5841
6220
|
onMetadataUpdate: (newMeta) => {
|
|
5842
6221
|
sessionMetadata = {
|
|
5843
6222
|
...newMeta,
|
|
6223
|
+
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6224
|
+
lifecycleState: sessionMetadata.lifecycleState,
|
|
6225
|
+
// Preserve claudeSessionId set by 'system init' (frontend may not have it)
|
|
6226
|
+
...sessionMetadata.claudeSessionId ? { claudeSessionId: sessionMetadata.claudeSessionId } : {},
|
|
5844
6227
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
5845
6228
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
5846
6229
|
};
|
|
5847
6230
|
const queue = newMeta.messageQueue;
|
|
5848
|
-
if (queue && queue.length > 0 && !sessionWasProcessing) {
|
|
6231
|
+
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
5849
6232
|
setTimeout(() => {
|
|
5850
6233
|
processMessageQueueRef?.();
|
|
5851
6234
|
}, 200);
|
|
@@ -5882,7 +6265,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5882
6265
|
},
|
|
5883
6266
|
onReadFile: async (path) => {
|
|
5884
6267
|
const resolvedPath = resolve(directory, path);
|
|
5885
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6268
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
5886
6269
|
throw new Error("Path outside working directory");
|
|
5887
6270
|
}
|
|
5888
6271
|
const buffer = await fs.readFile(resolvedPath);
|
|
@@ -5890,7 +6273,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5890
6273
|
},
|
|
5891
6274
|
onWriteFile: async (path, content) => {
|
|
5892
6275
|
const resolvedPath = resolve(directory, path);
|
|
5893
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6276
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
5894
6277
|
throw new Error("Path outside working directory");
|
|
5895
6278
|
}
|
|
5896
6279
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -5898,7 +6281,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5898
6281
|
},
|
|
5899
6282
|
onListDirectory: async (path) => {
|
|
5900
6283
|
const resolvedDir = resolve(directory, path || ".");
|
|
5901
|
-
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
6284
|
+
if (resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
|
|
5902
6285
|
throw new Error("Path outside working directory");
|
|
5903
6286
|
}
|
|
5904
6287
|
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
@@ -5937,6 +6320,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5937
6320
|
}
|
|
5938
6321
|
}
|
|
5939
6322
|
const resolvedPath = resolve(directory, treePath);
|
|
6323
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6324
|
+
throw new Error("Path outside working directory");
|
|
6325
|
+
}
|
|
5940
6326
|
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
5941
6327
|
return { success: !!tree, tree };
|
|
5942
6328
|
}
|
|
@@ -5953,13 +6339,18 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5953
6339
|
},
|
|
5954
6340
|
sessionService,
|
|
5955
6341
|
logger,
|
|
5956
|
-
() =>
|
|
6342
|
+
() => {
|
|
6343
|
+
if (!trackedSession?.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
6344
|
+
}
|
|
5957
6345
|
);
|
|
5958
6346
|
checkSvampConfig = svampConfig.check;
|
|
5959
6347
|
cleanupSvampConfig = svampConfig.cleanup;
|
|
5960
6348
|
const writeSvampConfigPatch = svampConfig.writeConfig;
|
|
5961
6349
|
processMessageQueueRef = () => {
|
|
5962
6350
|
if (sessionWasProcessing) return;
|
|
6351
|
+
if (trackedSession?.stopped) return;
|
|
6352
|
+
if (isKillingClaude) return;
|
|
6353
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5963
6354
|
const queue = sessionMetadata.messageQueue;
|
|
5964
6355
|
if (queue && queue.length > 0) {
|
|
5965
6356
|
const next = queue[0];
|
|
@@ -5988,22 +6379,33 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5988
6379
|
isKillingClaude = true;
|
|
5989
6380
|
await killAndWaitForExit2(claudeProcess);
|
|
5990
6381
|
isKillingClaude = false;
|
|
6382
|
+
if (trackedSession?.stopped) return;
|
|
6383
|
+
if (isRestartingClaude || isSwitchingMode) return;
|
|
5991
6384
|
claudeResumeId = void 0;
|
|
5992
6385
|
spawnClaude(next.text, queueMeta);
|
|
5993
6386
|
} catch (err) {
|
|
5994
6387
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
5995
6388
|
isKillingClaude = false;
|
|
6389
|
+
sessionWasProcessing = false;
|
|
5996
6390
|
signalProcessing(false);
|
|
5997
6391
|
}
|
|
5998
6392
|
})();
|
|
5999
|
-
} else if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6000
|
-
spawnClaude(next.text, queueMeta);
|
|
6001
6393
|
} else {
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6394
|
+
try {
|
|
6395
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6396
|
+
spawnClaude(next.text, queueMeta);
|
|
6397
|
+
} else {
|
|
6398
|
+
const stdinMsg = JSON.stringify({
|
|
6399
|
+
type: "user",
|
|
6400
|
+
message: { role: "user", content: next.text }
|
|
6401
|
+
});
|
|
6402
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6403
|
+
}
|
|
6404
|
+
} catch (err) {
|
|
6405
|
+
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
6406
|
+
sessionWasProcessing = false;
|
|
6407
|
+
signalProcessing(false);
|
|
6408
|
+
}
|
|
6007
6409
|
}
|
|
6008
6410
|
}
|
|
6009
6411
|
};
|
|
@@ -6022,7 +6424,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6022
6424
|
return claudeProcess || void 0;
|
|
6023
6425
|
}
|
|
6024
6426
|
};
|
|
6025
|
-
pidToTrackedSession.set(
|
|
6427
|
+
pidToTrackedSession.set(randomUUID$1(), trackedSession);
|
|
6026
6428
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6027
6429
|
sessionService.updateMetadata(sessionMetadata);
|
|
6028
6430
|
logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
|
|
@@ -6033,6 +6435,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6033
6435
|
};
|
|
6034
6436
|
} catch (err) {
|
|
6035
6437
|
logger.error(`Failed to spawn session: ${err?.message || err}`, err?.stack);
|
|
6438
|
+
if (stagedCredentials) {
|
|
6439
|
+
stagedCredentials.cleanup().catch(() => {
|
|
6440
|
+
});
|
|
6441
|
+
}
|
|
6036
6442
|
return {
|
|
6037
6443
|
type: "error",
|
|
6038
6444
|
errorMessage: `Failed to register session service: ${err?.message || String(err)}`
|
|
@@ -6100,6 +6506,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6100
6506
|
{ controlledByUser: false },
|
|
6101
6507
|
{
|
|
6102
6508
|
onUserMessage: (content, meta) => {
|
|
6509
|
+
if (acpStopped) return;
|
|
6103
6510
|
logger.log(`[${agentName} Session ${sessionId}] User message received`);
|
|
6104
6511
|
let text;
|
|
6105
6512
|
let msgMeta = meta;
|
|
@@ -6120,8 +6527,35 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6120
6527
|
if (msgMeta?.permissionMode) {
|
|
6121
6528
|
currentPermissionMode = msgMeta.permissionMode;
|
|
6122
6529
|
}
|
|
6530
|
+
if (!acpBackendReady) {
|
|
6531
|
+
logger.log(`[${agentName} Session ${sessionId}] Backend not ready \u2014 queuing message`);
|
|
6532
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
6533
|
+
sessionMetadata = {
|
|
6534
|
+
...sessionMetadata,
|
|
6535
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
6536
|
+
};
|
|
6537
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
if (sessionMetadata.lifecycleState === "running") {
|
|
6541
|
+
logger.log(`[${agentName} Session ${sessionId}] Agent busy \u2014 queuing message`);
|
|
6542
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
6543
|
+
sessionMetadata = {
|
|
6544
|
+
...sessionMetadata,
|
|
6545
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
6546
|
+
};
|
|
6547
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6548
|
+
return;
|
|
6549
|
+
}
|
|
6550
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6551
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6123
6552
|
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
6124
6553
|
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
6554
|
+
if (!acpStopped) {
|
|
6555
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6556
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6557
|
+
sessionService.sendSessionEnd();
|
|
6558
|
+
}
|
|
6125
6559
|
});
|
|
6126
6560
|
},
|
|
6127
6561
|
onAbort: () => {
|
|
@@ -6175,18 +6609,27 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6175
6609
|
onMetadataUpdate: (newMeta) => {
|
|
6176
6610
|
sessionMetadata = {
|
|
6177
6611
|
...newMeta,
|
|
6612
|
+
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6613
|
+
lifecycleState: sessionMetadata.lifecycleState,
|
|
6178
6614
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6179
6615
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6180
6616
|
};
|
|
6617
|
+
if (acpStopped) return;
|
|
6181
6618
|
const queue = newMeta.messageQueue;
|
|
6182
6619
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
6183
6620
|
const next = queue[0];
|
|
6184
6621
|
const remaining = queue.slice(1);
|
|
6185
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0 };
|
|
6622
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6186
6623
|
sessionService.updateMetadata(sessionMetadata);
|
|
6187
6624
|
logger.log(`[Session ${sessionId}] Processing queued message from metadata update: "${next.text.slice(0, 50)}..."`);
|
|
6625
|
+
sessionService.sendKeepAlive(true);
|
|
6188
6626
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6189
6627
|
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6628
|
+
if (!acpStopped) {
|
|
6629
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6630
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6631
|
+
sessionService.sendSessionEnd();
|
|
6632
|
+
}
|
|
6190
6633
|
});
|
|
6191
6634
|
}
|
|
6192
6635
|
},
|
|
@@ -6220,7 +6663,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6220
6663
|
},
|
|
6221
6664
|
onReadFile: async (path) => {
|
|
6222
6665
|
const resolvedPath = resolve(directory, path);
|
|
6223
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6666
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6224
6667
|
throw new Error("Path outside working directory");
|
|
6225
6668
|
}
|
|
6226
6669
|
const buffer = await fs.readFile(resolvedPath);
|
|
@@ -6228,7 +6671,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6228
6671
|
},
|
|
6229
6672
|
onWriteFile: async (path, content) => {
|
|
6230
6673
|
const resolvedPath = resolve(directory, path);
|
|
6231
|
-
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
6674
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6232
6675
|
throw new Error("Path outside working directory");
|
|
6233
6676
|
}
|
|
6234
6677
|
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -6236,7 +6679,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6236
6679
|
},
|
|
6237
6680
|
onListDirectory: async (path) => {
|
|
6238
6681
|
const resolvedDir = resolve(directory, path || ".");
|
|
6239
|
-
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
6682
|
+
if (resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
|
|
6240
6683
|
throw new Error("Path outside working directory");
|
|
6241
6684
|
}
|
|
6242
6685
|
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
@@ -6275,12 +6718,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6275
6718
|
}
|
|
6276
6719
|
}
|
|
6277
6720
|
const resolvedPath = resolve(directory, treePath);
|
|
6721
|
+
if (resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
6722
|
+
throw new Error("Path outside working directory");
|
|
6723
|
+
}
|
|
6278
6724
|
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
6279
6725
|
return { success: !!tree, tree };
|
|
6280
6726
|
}
|
|
6281
6727
|
},
|
|
6282
6728
|
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
6283
6729
|
);
|
|
6730
|
+
let insideOnTurnEnd = false;
|
|
6284
6731
|
const svampConfigChecker = createSvampConfigChecker(
|
|
6285
6732
|
directory,
|
|
6286
6733
|
sessionId,
|
|
@@ -6292,6 +6739,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6292
6739
|
sessionService,
|
|
6293
6740
|
logger,
|
|
6294
6741
|
() => {
|
|
6742
|
+
if (acpStopped) return;
|
|
6743
|
+
if (insideOnTurnEnd) return;
|
|
6295
6744
|
const queue = sessionMetadata.messageQueue;
|
|
6296
6745
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
6297
6746
|
const next = queue[0];
|
|
@@ -6302,6 +6751,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6302
6751
|
sessionService.sendKeepAlive(true);
|
|
6303
6752
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6304
6753
|
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6754
|
+
if (!acpStopped) {
|
|
6755
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6756
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6757
|
+
sessionService.sendSessionEnd();
|
|
6758
|
+
}
|
|
6305
6759
|
});
|
|
6306
6760
|
}
|
|
6307
6761
|
}
|
|
@@ -6355,71 +6809,129 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6355
6809
|
isolationConfig: agentIsoConfig
|
|
6356
6810
|
});
|
|
6357
6811
|
}
|
|
6812
|
+
let acpStopped = false;
|
|
6813
|
+
let acpBackendReady = false;
|
|
6358
6814
|
const onTurnEnd = (lastAssistantText) => {
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
promiseFulfilled =
|
|
6815
|
+
if (acpStopped) return;
|
|
6816
|
+
insideOnTurnEnd = true;
|
|
6817
|
+
try {
|
|
6818
|
+
checkSvampConfig?.();
|
|
6819
|
+
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6820
|
+
if (rlState) {
|
|
6821
|
+
let promiseFulfilled = false;
|
|
6822
|
+
if (rlState.completion_promise) {
|
|
6823
|
+
const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
|
|
6824
|
+
promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
|
|
6825
|
+
}
|
|
6826
|
+
const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
|
|
6827
|
+
if (promiseFulfilled || maxReached) {
|
|
6828
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6829
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
6830
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6831
|
+
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.`;
|
|
6832
|
+
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
6833
|
+
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
6834
|
+
} else {
|
|
6835
|
+
const pendingQueue = sessionMetadata.messageQueue;
|
|
6836
|
+
if (pendingQueue && pendingQueue.length > 0) {
|
|
6837
|
+
const next = pendingQueue[0];
|
|
6838
|
+
const remaining = pendingQueue.slice(1);
|
|
6839
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6840
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6841
|
+
sessionService.sendKeepAlive(true);
|
|
6842
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6843
|
+
logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
|
|
6844
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6845
|
+
logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
|
|
6846
|
+
if (!acpStopped) {
|
|
6847
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6848
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6849
|
+
sessionService.sendSessionEnd();
|
|
6850
|
+
}
|
|
6851
|
+
});
|
|
6852
|
+
return;
|
|
6853
|
+
}
|
|
6854
|
+
const nextIteration = rlState.iteration + 1;
|
|
6855
|
+
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6856
|
+
try {
|
|
6857
|
+
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
6858
|
+
} catch (writeErr) {
|
|
6859
|
+
logger.log(`[${agentName} Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
6860
|
+
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
6861
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6862
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
6863
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6864
|
+
return;
|
|
6865
|
+
}
|
|
6866
|
+
const ralphLoop = {
|
|
6867
|
+
active: true,
|
|
6868
|
+
task: rlState.task,
|
|
6869
|
+
completionPromise: rlState.completion_promise ?? "none",
|
|
6870
|
+
maxIterations: rlState.max_iterations,
|
|
6871
|
+
currentIteration: nextIteration,
|
|
6872
|
+
startedAt: rlState.started_at,
|
|
6873
|
+
cooldownSeconds: rlState.cooldown_seconds,
|
|
6874
|
+
contextMode: rlState.context_mode || "fresh",
|
|
6875
|
+
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6876
|
+
};
|
|
6877
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop, lifecycleState: "running" };
|
|
6878
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6879
|
+
logger.log(`[${agentName} Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning`);
|
|
6880
|
+
const updatedState = { ...rlState, iteration: nextIteration, context_mode: "continue" };
|
|
6881
|
+
const prompt = buildRalphPrompt(rlState.task, updatedState);
|
|
6882
|
+
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
6883
|
+
setTimeout(() => {
|
|
6884
|
+
if (acpStopped) return;
|
|
6885
|
+
const liveRlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6886
|
+
if (!liveRlState) {
|
|
6887
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6888
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6889
|
+
sessionService.sendKeepAlive(false);
|
|
6890
|
+
sessionService.sendSessionEnd();
|
|
6891
|
+
return;
|
|
6892
|
+
}
|
|
6893
|
+
sessionService.sendKeepAlive(true);
|
|
6894
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6895
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6896
|
+
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
6897
|
+
sessionService.pushMessage(rlState.task, "user");
|
|
6898
|
+
agentBackend.sendPrompt(sessionId, prompt).catch((err) => {
|
|
6899
|
+
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
6900
|
+
if (!acpStopped) {
|
|
6901
|
+
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6902
|
+
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
6903
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6904
|
+
sessionService.sendSessionEnd();
|
|
6905
|
+
sessionService.pushMessage({ type: "message", message: `Ralph loop error: agent failed to start turn \u2014 loop stopped. (${err.message})` }, "event");
|
|
6906
|
+
}
|
|
6907
|
+
});
|
|
6908
|
+
}, cooldownMs);
|
|
6909
|
+
return;
|
|
6910
|
+
}
|
|
6366
6911
|
}
|
|
6367
|
-
const
|
|
6368
|
-
if (
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
|
|
6373
|
-
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
6374
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
6375
|
-
} else {
|
|
6376
|
-
const nextIteration = rlState.iteration + 1;
|
|
6377
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6378
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
6379
|
-
const ralphLoop = {
|
|
6380
|
-
active: true,
|
|
6381
|
-
task: rlState.task,
|
|
6382
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
6383
|
-
maxIterations: rlState.max_iterations,
|
|
6384
|
-
currentIteration: nextIteration,
|
|
6385
|
-
startedAt: rlState.started_at,
|
|
6386
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
6387
|
-
contextMode: rlState.context_mode || "fresh",
|
|
6388
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6389
|
-
};
|
|
6390
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop };
|
|
6912
|
+
const queue = sessionMetadata.messageQueue;
|
|
6913
|
+
if (queue && queue.length > 0) {
|
|
6914
|
+
const next = queue[0];
|
|
6915
|
+
const remaining = queue.slice(1);
|
|
6916
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6391
6917
|
sessionService.updateMetadata(sessionMetadata);
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
6404
|
-
});
|
|
6405
|
-
}, cooldownMs);
|
|
6406
|
-
return;
|
|
6918
|
+
sessionService.sendKeepAlive(true);
|
|
6919
|
+
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
6920
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6921
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6922
|
+
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6923
|
+
if (!acpStopped) {
|
|
6924
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
6925
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6926
|
+
sessionService.sendSessionEnd();
|
|
6927
|
+
}
|
|
6928
|
+
});
|
|
6407
6929
|
}
|
|
6408
|
-
}
|
|
6409
|
-
|
|
6410
|
-
if (queue && queue.length > 0) {
|
|
6411
|
-
const next = queue[0];
|
|
6412
|
-
const remaining = queue.slice(1);
|
|
6413
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
6414
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
6415
|
-
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
6416
|
-
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6417
|
-
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
6418
|
-
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
6419
|
-
});
|
|
6930
|
+
} finally {
|
|
6931
|
+
insideOnTurnEnd = false;
|
|
6420
6932
|
}
|
|
6421
6933
|
};
|
|
6422
|
-
bridgeAcpToSession(
|
|
6934
|
+
const cleanupBridge = bridgeAcpToSession(
|
|
6423
6935
|
agentBackend,
|
|
6424
6936
|
sessionService,
|
|
6425
6937
|
() => sessionMetadata,
|
|
@@ -6441,11 +6953,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6441
6953
|
resumeSessionId,
|
|
6442
6954
|
get childProcess() {
|
|
6443
6955
|
return agentBackend.getProcess?.() || void 0;
|
|
6956
|
+
},
|
|
6957
|
+
onStop: () => {
|
|
6958
|
+
acpStopped = true;
|
|
6959
|
+
cleanupBridge();
|
|
6960
|
+
permissionHandler.rejectAll("session stopped");
|
|
6961
|
+
agentBackend.dispose().catch(() => {
|
|
6962
|
+
});
|
|
6444
6963
|
}
|
|
6445
6964
|
};
|
|
6446
|
-
pidToTrackedSession.set(
|
|
6965
|
+
pidToTrackedSession.set(randomUUID$1(), trackedSession);
|
|
6447
6966
|
logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
|
|
6448
6967
|
agentBackend.startSession().then(() => {
|
|
6968
|
+
acpBackendReady = true;
|
|
6449
6969
|
logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
6450
6970
|
}).catch((err) => {
|
|
6451
6971
|
logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
@@ -6454,6 +6974,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6454
6974
|
"event"
|
|
6455
6975
|
);
|
|
6456
6976
|
sessionService.sendSessionEnd();
|
|
6977
|
+
stopSession(sessionId);
|
|
6457
6978
|
});
|
|
6458
6979
|
return {
|
|
6459
6980
|
type: "success",
|
|
@@ -6473,6 +6994,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6473
6994
|
for (const [pid, session] of pidToTrackedSession) {
|
|
6474
6995
|
if (session.svampSessionId === sessionId) {
|
|
6475
6996
|
session.stopped = true;
|
|
6997
|
+
session.onStop?.();
|
|
6476
6998
|
session.hyphaService?.disconnect().catch(() => {
|
|
6477
6999
|
});
|
|
6478
7000
|
if (session.childProcess) {
|
|
@@ -6484,12 +7006,14 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6484
7006
|
session.cleanupCredentials?.().catch(() => {
|
|
6485
7007
|
});
|
|
6486
7008
|
session.cleanupSvampConfig?.();
|
|
7009
|
+
artifactSync.cancelSync(sessionId);
|
|
6487
7010
|
pidToTrackedSession.delete(pid);
|
|
6488
7011
|
deletePersistedSession(sessionId);
|
|
6489
7012
|
logger.log(`Session ${sessionId} stopped`);
|
|
6490
7013
|
return true;
|
|
6491
7014
|
}
|
|
6492
7015
|
}
|
|
7016
|
+
artifactSync.cancelSync(sessionId);
|
|
6493
7017
|
deletePersistedSession(sessionId);
|
|
6494
7018
|
logger.log(`Session ${sessionId} not found in memory, cleaned up persisted state`);
|
|
6495
7019
|
return false;
|
|
@@ -6516,7 +7040,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6516
7040
|
const defaultHomeDir = existsSync$1("/data") ? "/data" : os__default.homedir();
|
|
6517
7041
|
const persistedMachineMeta = loadPersistedMachineMetadata(SVAMP_HOME);
|
|
6518
7042
|
if (persistedMachineMeta) {
|
|
6519
|
-
logger.log(`Restored machine metadata (sharing=${!!persistedMachineMeta.sharing}, securityContextConfig=${!!persistedMachineMeta.securityContextConfig})`);
|
|
7043
|
+
logger.log(`Restored machine metadata (sharing=${!!persistedMachineMeta.sharing}, securityContextConfig=${!!persistedMachineMeta.securityContextConfig}, injectPlatformGuidance=${persistedMachineMeta.injectPlatformGuidance})`);
|
|
6520
7044
|
}
|
|
6521
7045
|
const machineMetadata = {
|
|
6522
7046
|
host: os__default.hostname(),
|
|
@@ -6527,9 +7051,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6527
7051
|
svampLibDir: join(__dirname$1, ".."),
|
|
6528
7052
|
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
6529
7053
|
isolationCapabilities,
|
|
6530
|
-
// Restore persisted sharing
|
|
7054
|
+
// Restore persisted sharing, security context config, and platform guidance flag
|
|
6531
7055
|
...persistedMachineMeta?.sharing && { sharing: persistedMachineMeta.sharing },
|
|
6532
|
-
...persistedMachineMeta?.securityContextConfig && { securityContextConfig: persistedMachineMeta.securityContextConfig }
|
|
7056
|
+
...persistedMachineMeta?.securityContextConfig && { securityContextConfig: persistedMachineMeta.securityContextConfig },
|
|
7057
|
+
...persistedMachineMeta?.injectPlatformGuidance !== void 0 && { injectPlatformGuidance: persistedMachineMeta.injectPlatformGuidance }
|
|
6533
7058
|
};
|
|
6534
7059
|
const initialDaemonState = {
|
|
6535
7060
|
status: "running",
|
|
@@ -6626,14 +7151,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6626
7151
|
for (const sessionId of sessionsToAutoContinue) {
|
|
6627
7152
|
setTimeout(async () => {
|
|
6628
7153
|
try {
|
|
6629
|
-
const svc = await
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
7154
|
+
const svc = await Promise.race([
|
|
7155
|
+
server.getService(`svamp-session-${sessionId}`),
|
|
7156
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("getService timeout")), 1e4))
|
|
7157
|
+
]);
|
|
7158
|
+
await Promise.race([
|
|
7159
|
+
svc.sendMessage(
|
|
7160
|
+
JSON.stringify({
|
|
7161
|
+
role: "user",
|
|
7162
|
+
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.' },
|
|
7163
|
+
meta: { sentFrom: "svamp-daemon-auto-continue" }
|
|
7164
|
+
})
|
|
7165
|
+
),
|
|
7166
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("sendMessage timeout")), 3e4))
|
|
7167
|
+
]);
|
|
6637
7168
|
logger.log(`Auto-continued session ${sessionId}`);
|
|
6638
7169
|
} catch (err) {
|
|
6639
7170
|
logger.log(`Failed to auto-continue session ${sessionId}: ${err.message}`);
|
|
@@ -6647,7 +7178,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6647
7178
|
logger.log(`Resuming Ralph loop for ${sessionsToRalphResume.length} session(s)...`);
|
|
6648
7179
|
for (const { sessionId, directory: sessDir } of sessionsToRalphResume) {
|
|
6649
7180
|
try {
|
|
6650
|
-
const svc = await
|
|
7181
|
+
const svc = await Promise.race([
|
|
7182
|
+
server.getService(`svamp-session-${sessionId}`),
|
|
7183
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("getService timeout")), 1e4))
|
|
7184
|
+
]);
|
|
6651
7185
|
const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
|
|
6652
7186
|
if (!rlState) continue;
|
|
6653
7187
|
const initDelayMs = 2e3;
|
|
@@ -6668,17 +7202,20 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6668
7202
|
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
6669
7203
|
const prompt = buildRalphPrompt(currentState.task, currentState);
|
|
6670
7204
|
const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
|
|
6671
|
-
await
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
7205
|
+
await Promise.race([
|
|
7206
|
+
svc.sendMessage(
|
|
7207
|
+
JSON.stringify({
|
|
7208
|
+
role: "user",
|
|
7209
|
+
content: { type: "text", text: prompt },
|
|
7210
|
+
meta: {
|
|
7211
|
+
sentFrom: "svamp-daemon-ralph-resume",
|
|
7212
|
+
appendSystemPrompt: ralphSysPrompt,
|
|
7213
|
+
...isFreshMode ? { ralphFreshContext: true } : {}
|
|
7214
|
+
}
|
|
7215
|
+
})
|
|
7216
|
+
),
|
|
7217
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("sendMessage timeout")), 3e4))
|
|
7218
|
+
]);
|
|
6682
7219
|
logger.log(`Resumed Ralph loop for session ${sessionId} at iteration ${currentState.iteration} (${isFreshMode ? "fresh" : "continue"})`);
|
|
6683
7220
|
} catch (err) {
|
|
6684
7221
|
logger.log(`Failed to resume Ralph loop for session ${sessionId}: ${err.message}`);
|
|
@@ -6764,9 +7301,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6764
7301
|
process.kill(child.pid, 0);
|
|
6765
7302
|
} catch {
|
|
6766
7303
|
logger.log(`Removing stale session (child PID ${child.pid} dead): ${session.svampSessionId}`);
|
|
7304
|
+
session.stopped = true;
|
|
7305
|
+
session.onStop?.();
|
|
6767
7306
|
session.hyphaService?.disconnect().catch(() => {
|
|
6768
7307
|
});
|
|
7308
|
+
session.cleanupCredentials?.().catch(() => {
|
|
7309
|
+
});
|
|
7310
|
+
session.cleanupSvampConfig?.();
|
|
7311
|
+
if (session.svampSessionId) artifactSync.cancelSync(session.svampSessionId);
|
|
6769
7312
|
pidToTrackedSession.delete(key);
|
|
7313
|
+
if (session.svampSessionId) deletePersistedSession(session.svampSessionId);
|
|
6770
7314
|
}
|
|
6771
7315
|
}
|
|
6772
7316
|
}
|
|
@@ -6834,6 +7378,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6834
7378
|
clearInterval(heartbeatInterval);
|
|
6835
7379
|
if (proxyTokenRefreshInterval) clearInterval(proxyTokenRefreshInterval);
|
|
6836
7380
|
if (unhandledRejectionResetTimer) clearTimeout(unhandledRejectionResetTimer);
|
|
7381
|
+
for (const [, session] of pidToTrackedSession) {
|
|
7382
|
+
session.stopped = true;
|
|
7383
|
+
session.onStop?.();
|
|
7384
|
+
}
|
|
6837
7385
|
machineService.updateDaemonState({
|
|
6838
7386
|
...initialDaemonState,
|
|
6839
7387
|
status: "shutting-down",
|
|
@@ -6841,7 +7389,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6841
7389
|
shutdownSource: source
|
|
6842
7390
|
});
|
|
6843
7391
|
await new Promise((r) => setTimeout(r, 200));
|
|
6844
|
-
for (const [
|
|
7392
|
+
for (const [, session] of pidToTrackedSession) {
|
|
6845
7393
|
session.hyphaService?.disconnect().catch(() => {
|
|
6846
7394
|
});
|
|
6847
7395
|
if (session.childProcess) {
|
|
@@ -6858,14 +7406,21 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6858
7406
|
if (shouldMarkStopped) {
|
|
6859
7407
|
try {
|
|
6860
7408
|
const index = loadSessionIndex();
|
|
7409
|
+
let markedCount = 0;
|
|
6861
7410
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
7411
|
+
try {
|
|
7412
|
+
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
7413
|
+
if (existsSync$1(filePath)) {
|
|
7414
|
+
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
7415
|
+
const tmpPath = filePath + ".tmp";
|
|
7416
|
+
writeFileSync(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
7417
|
+
renameSync(tmpPath, filePath);
|
|
7418
|
+
markedCount++;
|
|
7419
|
+
}
|
|
7420
|
+
} catch {
|
|
6866
7421
|
}
|
|
6867
7422
|
}
|
|
6868
|
-
logger.log(
|
|
7423
|
+
logger.log(`Marked ${markedCount} session(s) as stopped (--cleanup mode)`);
|
|
6869
7424
|
} catch {
|
|
6870
7425
|
}
|
|
6871
7426
|
} else {
|