svamp-cli 0.1.74 → 0.1.76
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-NVZzP_Vo.mjs +298 -0
- package/dist/cli.mjs +188 -164
- package/dist/{commands-dKkUOvUb.mjs → commands-7Iw1nFwf.mjs} +3 -3
- package/dist/{commands-BLjcT1Vl.mjs → commands-CADr1mQg.mjs} +66 -2
- package/dist/{commands-UFi0_ESV.mjs → commands-DJoYOM_1.mjs} +25 -25
- package/dist/{commands-ClMc3nSg.mjs → commands-lJ8V7MJE.mjs} +77 -141
- package/dist/index.mjs +1 -1
- package/dist/{package-CCjeil_X.mjs → package-Dpz1MLO4.mjs} +4 -3
- package/dist/{run-BgwhZgkT.mjs → run-B29grSMh.mjs} +1 -1
- package/dist/{run-6QgabuQN.mjs → run-BnFGIK0c.mjs} +321 -329
- package/dist/staticServer-B-S9sl6E.mjs +198 -0
- package/package.json +4 -3
- package/dist/agentCommands-7GGmL2zY.mjs +0 -157
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os__default from 'os';
|
|
2
2
|
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
|
|
3
3
|
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
4
|
-
import
|
|
4
|
+
import path__default, { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { spawn as spawn$1 } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
@@ -294,6 +294,33 @@ function applySecurityContext(baseConfig, context) {
|
|
|
294
294
|
return config;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
const terminalSessions = /* @__PURE__ */ new Map();
|
|
298
|
+
let ptyModule = null;
|
|
299
|
+
async function getPtyModule() {
|
|
300
|
+
if (!ptyModule) {
|
|
301
|
+
try {
|
|
302
|
+
ptyModule = await import('node-pty');
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error("node-pty is not available. Install it with: npm install node-pty");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return ptyModule;
|
|
308
|
+
}
|
|
309
|
+
function generateTerminalId() {
|
|
310
|
+
return `term-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
311
|
+
}
|
|
312
|
+
const ALLOWED_CONTROL_CHARS = /* @__PURE__ */ new Set([7, 8, 9, 10, 11, 12, 13, 27]);
|
|
313
|
+
function filterForXterm(text) {
|
|
314
|
+
if (!text) return text;
|
|
315
|
+
let result = "";
|
|
316
|
+
for (let i = 0; i < text.length; i++) {
|
|
317
|
+
const code = text.charCodeAt(i);
|
|
318
|
+
if (code >= 32 && code <= 126 || ALLOWED_CONTROL_CHARS.has(code) || code > 127) {
|
|
319
|
+
result += text[i];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
297
324
|
function getMachineMetadataPath(svampHomeDir) {
|
|
298
325
|
return join(svampHomeDir, "machine-metadata.json");
|
|
299
326
|
}
|
|
@@ -667,6 +694,125 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
667
694
|
});
|
|
668
695
|
});
|
|
669
696
|
},
|
|
697
|
+
// ── Terminal PTY RPC ──────────────────────────────────────────────
|
|
698
|
+
/** Start a new terminal PTY session. Returns { sessionId, cols, rows }. */
|
|
699
|
+
terminalStart: async (params = {}, context) => {
|
|
700
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
701
|
+
const pty = await getPtyModule();
|
|
702
|
+
const { homedir: getHomedir } = await import('os');
|
|
703
|
+
const cols = params.cols || 80;
|
|
704
|
+
const rows = params.rows || 24;
|
|
705
|
+
const cwd = params.cwd || getHomedir();
|
|
706
|
+
const shell = params.shell || process.env.SHELL || "bash";
|
|
707
|
+
const sessionId = generateTerminalId();
|
|
708
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
709
|
+
name: "xterm-256color",
|
|
710
|
+
cols,
|
|
711
|
+
rows,
|
|
712
|
+
cwd,
|
|
713
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
714
|
+
});
|
|
715
|
+
const session = {
|
|
716
|
+
id: sessionId,
|
|
717
|
+
pty: ptyProcess,
|
|
718
|
+
outputBuffer: [],
|
|
719
|
+
exited: false,
|
|
720
|
+
cols,
|
|
721
|
+
rows,
|
|
722
|
+
createdAt: Date.now(),
|
|
723
|
+
cwd
|
|
724
|
+
};
|
|
725
|
+
terminalSessions.set(sessionId, session);
|
|
726
|
+
ptyProcess.onData((data) => {
|
|
727
|
+
const filtered = filterForXterm(data);
|
|
728
|
+
if (!filtered) return;
|
|
729
|
+
session.outputBuffer.push(filtered);
|
|
730
|
+
if (session.outputBuffer.length > 1e3) {
|
|
731
|
+
session.outputBuffer.splice(0, session.outputBuffer.length - 500);
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
server.emit({
|
|
735
|
+
type: "svamp:terminal-output",
|
|
736
|
+
data: { type: "output", content: filtered, sessionId },
|
|
737
|
+
to: "*"
|
|
738
|
+
});
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
743
|
+
session.exited = true;
|
|
744
|
+
session.exitCode = exitCode;
|
|
745
|
+
session.exitSignal = signal;
|
|
746
|
+
try {
|
|
747
|
+
server.emit({
|
|
748
|
+
type: "svamp:terminal-output",
|
|
749
|
+
data: { type: "exit", content: "", sessionId, exitCode, signal },
|
|
750
|
+
to: "*"
|
|
751
|
+
});
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
return { sessionId, cols, rows };
|
|
756
|
+
},
|
|
757
|
+
/** Write data (keystrokes) to a terminal session. */
|
|
758
|
+
terminalWrite: async (params, context) => {
|
|
759
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
760
|
+
const session = terminalSessions.get(params.sessionId);
|
|
761
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
762
|
+
session.pty.write(params.data);
|
|
763
|
+
return { success: true };
|
|
764
|
+
},
|
|
765
|
+
/** Resize a terminal session. */
|
|
766
|
+
terminalResize: async (params, context) => {
|
|
767
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
768
|
+
const session = terminalSessions.get(params.sessionId);
|
|
769
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
770
|
+
session.pty.resize(params.cols, params.rows);
|
|
771
|
+
session.cols = params.cols;
|
|
772
|
+
session.rows = params.rows;
|
|
773
|
+
return { success: true };
|
|
774
|
+
},
|
|
775
|
+
/**
|
|
776
|
+
* Read buffered output from a terminal session (polling).
|
|
777
|
+
* Returns { output, exited, exitCode? }. Drains the buffer.
|
|
778
|
+
*/
|
|
779
|
+
terminalRead: async (params, context) => {
|
|
780
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
781
|
+
const session = terminalSessions.get(params.sessionId);
|
|
782
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
783
|
+
const output = session.outputBuffer.splice(0).join("");
|
|
784
|
+
const result = {
|
|
785
|
+
output,
|
|
786
|
+
exited: session.exited
|
|
787
|
+
};
|
|
788
|
+
if (session.exited) {
|
|
789
|
+
result.exitCode = session.exitCode;
|
|
790
|
+
result.exitSignal = session.exitSignal;
|
|
791
|
+
if (!output) terminalSessions.delete(params.sessionId);
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
},
|
|
795
|
+
/** Stop (kill) a terminal session. */
|
|
796
|
+
terminalStop: async (params, context) => {
|
|
797
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
798
|
+
const session = terminalSessions.get(params.sessionId);
|
|
799
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
800
|
+
session.pty.kill();
|
|
801
|
+
terminalSessions.delete(params.sessionId);
|
|
802
|
+
return { success: true };
|
|
803
|
+
},
|
|
804
|
+
/** List active terminal sessions. */
|
|
805
|
+
terminalList: async (context) => {
|
|
806
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
807
|
+
return Array.from(terminalSessions.values()).map((s) => ({
|
|
808
|
+
sessionId: s.id,
|
|
809
|
+
cols: s.cols,
|
|
810
|
+
rows: s.rows,
|
|
811
|
+
cwd: s.cwd,
|
|
812
|
+
createdAt: s.createdAt,
|
|
813
|
+
exited: s.exited
|
|
814
|
+
}));
|
|
815
|
+
},
|
|
670
816
|
// Machine-level directory listing (read-only, view role)
|
|
671
817
|
listDirectory: async (path, options, context) => {
|
|
672
818
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
@@ -874,20 +1020,21 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
874
1020
|
function isStructuredMessage(msg) {
|
|
875
1021
|
return !!(msg.from && msg.subject);
|
|
876
1022
|
}
|
|
1023
|
+
function escapeXml(s) {
|
|
1024
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1025
|
+
}
|
|
877
1026
|
function formatInboxMessageXml(msg) {
|
|
878
1027
|
if (!isStructuredMessage(msg)) return msg.body;
|
|
879
|
-
const attrs = [
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (msg.cc && msg.cc.length > 0) attrs.push(`cc="${msg.cc.join(",")}"`);
|
|
890
|
-
if (msg.threadId) attrs.push(`thread-id="${msg.threadId}"`);
|
|
1028
|
+
const attrs = [`message-id="${escapeXml(msg.messageId)}"`];
|
|
1029
|
+
if (msg.from) attrs.push(`from="${escapeXml(msg.from)}"`);
|
|
1030
|
+
if (msg.fromSession) attrs.push(`from-session="${escapeXml(msg.fromSession)}"`);
|
|
1031
|
+
if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
|
|
1032
|
+
if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
|
|
1033
|
+
if (msg.urgency) attrs.push(`urgency="${msg.urgency}"`);
|
|
1034
|
+
if (msg.replyTo) attrs.push(`reply-to="${escapeXml(msg.replyTo)}"`);
|
|
1035
|
+
if (msg.cc && msg.cc.length > 0) attrs.push(`cc="${msg.cc.map(escapeXml).join(",")}"`);
|
|
1036
|
+
if (msg.threadId) attrs.push(`thread-id="${escapeXml(msg.threadId)}"`);
|
|
1037
|
+
attrs.push(`timestamp="${msg.timestamp}"`);
|
|
891
1038
|
return `<svamp-message ${attrs.join(" ")}>
|
|
892
1039
|
${msg.body}
|
|
893
1040
|
</svamp-message>`;
|
|
@@ -933,6 +1080,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
933
1080
|
mode: "remote",
|
|
934
1081
|
time: Date.now()
|
|
935
1082
|
};
|
|
1083
|
+
const inbox = [];
|
|
1084
|
+
const INBOX_MAX = 100;
|
|
936
1085
|
const listeners = [];
|
|
937
1086
|
const removeListener = (listener, reason) => {
|
|
938
1087
|
const idx = listeners.indexOf(listener);
|
|
@@ -1268,24 +1417,6 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1268
1417
|
});
|
|
1269
1418
|
return await callbacks.onUpdateSecurityContext(newSecurityContext);
|
|
1270
1419
|
},
|
|
1271
|
-
/** Toggle isolation (nono/docker/podman) on or off — triggers agent restart */
|
|
1272
|
-
updateIsolation: async (enabled, context) => {
|
|
1273
|
-
authorizeRequest(context, metadata.sharing, "admin");
|
|
1274
|
-
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1275
|
-
throw new Error("Only the session owner can change isolation settings");
|
|
1276
|
-
}
|
|
1277
|
-
if (!callbacks.onUpdateIsolation) {
|
|
1278
|
-
throw new Error("Isolation updates are not supported for this session");
|
|
1279
|
-
}
|
|
1280
|
-
metadata = { ...metadata, forceIsolation: enabled };
|
|
1281
|
-
metadataVersion++;
|
|
1282
|
-
notifyListeners({
|
|
1283
|
-
type: "update-session",
|
|
1284
|
-
sessionId,
|
|
1285
|
-
metadata: { value: metadata, version: metadataVersion }
|
|
1286
|
-
});
|
|
1287
|
-
return await callbacks.onUpdateIsolation(enabled);
|
|
1288
|
-
},
|
|
1289
1420
|
/** Apply a new system prompt and restart the agent process */
|
|
1290
1421
|
applySystemPrompt: async (prompt, context) => {
|
|
1291
1422
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
@@ -1294,85 +1425,42 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1294
1425
|
}
|
|
1295
1426
|
return await callbacks.onApplySystemPrompt(prompt);
|
|
1296
1427
|
},
|
|
1297
|
-
// ── Inbox
|
|
1428
|
+
// ── Inbox ──
|
|
1298
1429
|
sendInboxMessage: async (message, context) => {
|
|
1299
1430
|
authorizeRequest(context, metadata.sharing, "interact");
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
const inbox = (metadata.inbox || []).slice();
|
|
1304
|
-
inbox.push({ ...message, read: false });
|
|
1305
|
-
if (inbox.length > 100) {
|
|
1306
|
-
const readIdx = inbox.findIndex((m) => m.read);
|
|
1307
|
-
if (readIdx >= 0) {
|
|
1308
|
-
inbox.splice(readIdx, 1);
|
|
1309
|
-
} else {
|
|
1310
|
-
inbox.shift();
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
metadata = { ...metadata, inbox };
|
|
1314
|
-
metadataVersion++;
|
|
1315
|
-
notifyListeners({
|
|
1316
|
-
type: "update-session",
|
|
1317
|
-
sessionId,
|
|
1318
|
-
metadata: { value: metadata, version: metadataVersion }
|
|
1319
|
-
});
|
|
1431
|
+
inbox.push(message);
|
|
1432
|
+
while (inbox.length > INBOX_MAX) inbox.shift();
|
|
1320
1433
|
callbacks.onInboxMessage?.(message);
|
|
1321
|
-
|
|
1434
|
+
notifyListeners({ type: "inbox-update", sessionId, message });
|
|
1435
|
+
return { success: true, messageId: message.messageId };
|
|
1322
1436
|
},
|
|
1323
1437
|
getInbox: async (opts, context) => {
|
|
1324
1438
|
authorizeRequest(context, metadata.sharing, "view");
|
|
1325
|
-
let
|
|
1326
|
-
if (opts?.
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
inbox = inbox.slice(-opts.limit);
|
|
1331
|
-
}
|
|
1332
|
-
return { messages: inbox, total: (metadata.inbox || []).length };
|
|
1439
|
+
let result = [...inbox];
|
|
1440
|
+
if (opts?.unread) result = result.filter((m) => !m.read);
|
|
1441
|
+
result.sort((a, b) => b.timestamp - a.timestamp);
|
|
1442
|
+
if (opts?.limit && opts.limit > 0) result = result.slice(0, opts.limit);
|
|
1443
|
+
return { messages: result, total: inbox.length };
|
|
1333
1444
|
},
|
|
1334
|
-
markInboxRead: async (
|
|
1445
|
+
markInboxRead: async (messageId, context) => {
|
|
1335
1446
|
authorizeRequest(context, metadata.sharing, "interact");
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
return m;
|
|
1343
|
-
});
|
|
1344
|
-
if (updated > 0) {
|
|
1345
|
-
metadata = { ...metadata, inbox };
|
|
1346
|
-
metadataVersion++;
|
|
1347
|
-
notifyListeners({
|
|
1348
|
-
type: "update-session",
|
|
1349
|
-
sessionId,
|
|
1350
|
-
metadata: { value: metadata, version: metadataVersion }
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
return { updated };
|
|
1447
|
+
const msg = inbox.find((m) => m.messageId === messageId);
|
|
1448
|
+
if (!msg) return { success: false, error: "Message not found" };
|
|
1449
|
+
msg.read = true;
|
|
1450
|
+
notifyListeners({ type: "inbox-update", sessionId, message: msg });
|
|
1451
|
+
return { success: true };
|
|
1354
1452
|
},
|
|
1355
|
-
clearInbox: async (
|
|
1356
|
-
authorizeRequest(context, metadata.sharing, "
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
if (messageIds && messageIds.length > 0) {
|
|
1360
|
-
const newInbox = currentInbox.filter((m) => !messageIds.includes(m.messageId));
|
|
1361
|
-
removed = currentInbox.length - newInbox.length;
|
|
1362
|
-
metadata = { ...metadata, inbox: newInbox.length > 0 ? newInbox : void 0 };
|
|
1453
|
+
clearInbox: async (opts, context) => {
|
|
1454
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1455
|
+
if (opts?.all) {
|
|
1456
|
+
inbox.length = 0;
|
|
1363
1457
|
} else {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
if (removed > 0) {
|
|
1368
|
-
metadataVersion++;
|
|
1369
|
-
notifyListeners({
|
|
1370
|
-
type: "update-session",
|
|
1371
|
-
sessionId,
|
|
1372
|
-
metadata: { value: metadata, version: metadataVersion }
|
|
1373
|
-
});
|
|
1458
|
+
for (let i = inbox.length - 1; i >= 0; i--) {
|
|
1459
|
+
if (inbox[i].read) inbox.splice(i, 1);
|
|
1460
|
+
}
|
|
1374
1461
|
}
|
|
1375
|
-
|
|
1462
|
+
notifyListeners({ type: "inbox-update", sessionId, cleared: true });
|
|
1463
|
+
return { success: true, remaining: inbox.length };
|
|
1376
1464
|
},
|
|
1377
1465
|
// ── Listener Registration ──
|
|
1378
1466
|
registerListener: async (callback, context) => {
|
|
@@ -4158,6 +4246,8 @@ function sanitizeEnvForSharing(env) {
|
|
|
4158
4246
|
const DEFAULT_PROBE_INTERVAL_S = 10;
|
|
4159
4247
|
const DEFAULT_PROBE_TIMEOUT_S = 5;
|
|
4160
4248
|
const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
|
|
4249
|
+
const MAX_RESTART_DELAY_S = 300;
|
|
4250
|
+
const BACKOFF_RESET_WINDOW_MS = 6e4;
|
|
4161
4251
|
const MAX_LOG_LINES = 300;
|
|
4162
4252
|
class ProcessSupervisor {
|
|
4163
4253
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -4355,7 +4445,7 @@ class ProcessSupervisor {
|
|
|
4355
4445
|
for (const file of files) {
|
|
4356
4446
|
if (!file.endsWith(".json")) continue;
|
|
4357
4447
|
try {
|
|
4358
|
-
const raw = await readFile(
|
|
4448
|
+
const raw = await readFile(path__default.join(this.persistDir, file), "utf-8");
|
|
4359
4449
|
const spec = JSON.parse(raw);
|
|
4360
4450
|
const entry = this.makeEntry(spec);
|
|
4361
4451
|
this.entries.set(spec.id, entry);
|
|
@@ -4376,14 +4466,14 @@ class ProcessSupervisor {
|
|
|
4376
4466
|
}
|
|
4377
4467
|
}
|
|
4378
4468
|
async persistSpec(spec) {
|
|
4379
|
-
const filePath =
|
|
4469
|
+
const filePath = path__default.join(this.persistDir, `${spec.id}.json`);
|
|
4380
4470
|
const tmpPath = filePath + ".tmp";
|
|
4381
4471
|
await writeFile$1(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4382
4472
|
await rename(tmpPath, filePath);
|
|
4383
4473
|
}
|
|
4384
4474
|
async deleteSpec(id) {
|
|
4385
4475
|
try {
|
|
4386
|
-
await unlink(
|
|
4476
|
+
await unlink(path__default.join(this.persistDir, `${id}.json`));
|
|
4387
4477
|
} catch {
|
|
4388
4478
|
}
|
|
4389
4479
|
}
|
|
@@ -4489,8 +4579,19 @@ class ProcessSupervisor {
|
|
|
4489
4579
|
state.status = "failed";
|
|
4490
4580
|
return;
|
|
4491
4581
|
}
|
|
4492
|
-
const
|
|
4493
|
-
|
|
4582
|
+
const uptime = state.startedAt ? Date.now() - state.startedAt : 0;
|
|
4583
|
+
const baseDelay = spec.restartDelay * 1e3;
|
|
4584
|
+
let delayMs;
|
|
4585
|
+
if (uptime > BACKOFF_RESET_WINDOW_MS) {
|
|
4586
|
+
state.restartCount = 0;
|
|
4587
|
+
delayMs = baseDelay;
|
|
4588
|
+
} else {
|
|
4589
|
+
const backoffExponent = Math.min(state.restartCount, 10);
|
|
4590
|
+
delayMs = Math.min(baseDelay * Math.pow(2, backoffExponent), MAX_RESTART_DELAY_S * 1e3);
|
|
4591
|
+
const jitter = (Math.random() * 0.2 - 0.1) * delayMs;
|
|
4592
|
+
delayMs = Math.max(baseDelay, Math.round(delayMs + jitter));
|
|
4593
|
+
}
|
|
4594
|
+
console.log(`[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (restart #${state.restartCount + 1}, uptime=${Math.round(uptime / 1e3)}s)`);
|
|
4494
4595
|
entry.restartTimer = setTimeout(() => {
|
|
4495
4596
|
if (entry.stopping) return;
|
|
4496
4597
|
state.restartCount++;
|
|
@@ -4971,16 +5072,15 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
4971
5072
|
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
4972
5073
|
const prompt = buildRalphPrompt(state.task, state);
|
|
4973
5074
|
const ralphSysPrompt = buildRalphSystemPrompt(state, progressRelPath);
|
|
4974
|
-
const
|
|
5075
|
+
const existingQueue = getMetadata().messageQueue || [];
|
|
4975
5076
|
setMetadata((m) => ({
|
|
4976
5077
|
...m,
|
|
4977
5078
|
ralphLoop,
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
5079
|
+
messageQueue: [...existingQueue, {
|
|
5080
|
+
id: randomUUID$1(),
|
|
5081
|
+
text: prompt,
|
|
4981
5082
|
displayText: state.task,
|
|
4982
|
-
|
|
4983
|
-
read: false,
|
|
5083
|
+
createdAt: Date.now(),
|
|
4984
5084
|
ralphSystemPrompt: ralphSysPrompt
|
|
4985
5085
|
}]
|
|
4986
5086
|
}));
|
|
@@ -5648,8 +5748,7 @@ async function startDaemon(options) {
|
|
|
5648
5748
|
}
|
|
5649
5749
|
});
|
|
5650
5750
|
}, buildIsolationConfig2 = function(dir) {
|
|
5651
|
-
|
|
5652
|
-
if (!forceIsolation && !sessionMetadata.sharing?.enabled) return null;
|
|
5751
|
+
if (!options2.forceIsolation && !sessionMetadata.sharing?.enabled) return null;
|
|
5653
5752
|
const method = isolationCapabilities.preferred;
|
|
5654
5753
|
if (!method) return null;
|
|
5655
5754
|
const detail = isolationCapabilities.details[method];
|
|
@@ -5690,7 +5789,6 @@ async function startDaemon(options) {
|
|
|
5690
5789
|
lifecycleState: resumeSessionId ? "idle" : "starting",
|
|
5691
5790
|
sharing: options2.sharing,
|
|
5692
5791
|
securityContext: options2.securityContext,
|
|
5693
|
-
forceIsolation: options2.forceIsolation || void 0,
|
|
5694
5792
|
tags: options2.tags,
|
|
5695
5793
|
parentSessionId: options2.parentSessionId,
|
|
5696
5794
|
...options2.parentSessionId && (() => {
|
|
@@ -5766,8 +5864,7 @@ async function startDaemon(options) {
|
|
|
5766
5864
|
startupNonJsonLines = [];
|
|
5767
5865
|
startupRetryMessage = initialMessage;
|
|
5768
5866
|
let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
5769
|
-
|
|
5770
|
-
if (forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
5867
|
+
if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
5771
5868
|
rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
|
|
5772
5869
|
}
|
|
5773
5870
|
if (sessionMetadata.ralphLoop?.active) {
|
|
@@ -5804,7 +5901,7 @@ async function startDaemon(options) {
|
|
|
5804
5901
|
if (wrapped.cleanupFiles) isolationCleanupFiles = wrapped.cleanupFiles;
|
|
5805
5902
|
sessionMetadata = { ...sessionMetadata, isolationMethod: isoConfig.method };
|
|
5806
5903
|
logger.log(`[Session ${sessionId}] Isolation: ${isoConfig.method} (binary: ${isoConfig.binaryPath})`);
|
|
5807
|
-
} else if (forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
5904
|
+
} else if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
5808
5905
|
logger.log(`[Session ${sessionId}] WARNING: No isolation runtime (nono/docker/podman) available. Session is NOT sandboxed.`);
|
|
5809
5906
|
sessionMetadata = { ...sessionMetadata, isolationMethod: void 0 };
|
|
5810
5907
|
} else {
|
|
@@ -6029,7 +6126,7 @@ async function startDaemon(options) {
|
|
|
6029
6126
|
logger.log(`[Session ${sessionId}] ${taskInfo}`);
|
|
6030
6127
|
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
6031
6128
|
}
|
|
6032
|
-
const queueLen = sessionMetadata.
|
|
6129
|
+
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
6033
6130
|
if (msg.is_error) {
|
|
6034
6131
|
const rlStateForError = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
6035
6132
|
if (rlStateForError) {
|
|
@@ -6330,11 +6427,12 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6330
6427
|
logger.log(`[Session ${sessionId}] Retrying startup without --resume`);
|
|
6331
6428
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6332
6429
|
sessionService.updateMetadata(sessionMetadata);
|
|
6430
|
+
sessionService.sendKeepAlive(true);
|
|
6333
6431
|
spawnClaude(startupRetryMessage);
|
|
6334
6432
|
return;
|
|
6335
6433
|
}
|
|
6336
6434
|
}
|
|
6337
|
-
const queueLen = sessionMetadata.
|
|
6435
|
+
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
6338
6436
|
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
6339
6437
|
signalProcessing(false);
|
|
6340
6438
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
@@ -6445,10 +6543,10 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6445
6543
|
}
|
|
6446
6544
|
if (isKillingClaude || isRestartingClaude || isSwitchingMode) {
|
|
6447
6545
|
logger.log(`[Session ${sessionId}] Message received while restarting Claude, queuing to prevent loss`);
|
|
6448
|
-
const
|
|
6546
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
6449
6547
|
sessionMetadata = {
|
|
6450
6548
|
...sessionMetadata,
|
|
6451
|
-
|
|
6549
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
6452
6550
|
};
|
|
6453
6551
|
sessionService.updateMetadata(sessionMetadata);
|
|
6454
6552
|
signalProcessing(false);
|
|
@@ -6483,7 +6581,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6483
6581
|
}
|
|
6484
6582
|
signalProcessing(false);
|
|
6485
6583
|
sessionWasProcessing = false;
|
|
6486
|
-
const queueLen = sessionMetadata.
|
|
6584
|
+
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
6487
6585
|
const abortMsg = queueLen > 0 ? `Aborted by user. ${queueLen} queued message(s) will be processed next.` : "Aborted by user";
|
|
6488
6586
|
sessionService.pushMessage(
|
|
6489
6587
|
{ type: "message", message: abortMsg },
|
|
@@ -6565,24 +6663,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6565
6663
|
sessionMetadata = { ...sessionMetadata, securityContext: newSecurityContext };
|
|
6566
6664
|
return await restartClaudeHandler();
|
|
6567
6665
|
},
|
|
6568
|
-
onUpdateIsolation: async (enabled) => {
|
|
6569
|
-
logger.log(`[Session ${sessionId}] Isolation ${enabled ? "enabled" : "disabled"} \u2014 restarting agent`);
|
|
6570
|
-
sessionMetadata = { ...sessionMetadata, forceIsolation: enabled };
|
|
6571
|
-
if (!trackedSession.stopped) {
|
|
6572
|
-
saveSession({
|
|
6573
|
-
sessionId,
|
|
6574
|
-
directory,
|
|
6575
|
-
claudeResumeId,
|
|
6576
|
-
permissionMode: currentPermissionMode,
|
|
6577
|
-
spawnMeta: lastSpawnMeta,
|
|
6578
|
-
metadata: sessionMetadata,
|
|
6579
|
-
createdAt: Date.now(),
|
|
6580
|
-
machineId,
|
|
6581
|
-
wasProcessing: sessionWasProcessing
|
|
6582
|
-
});
|
|
6583
|
-
}
|
|
6584
|
-
return await restartClaudeHandler();
|
|
6585
|
-
},
|
|
6586
6666
|
onSharingUpdate: (newSharing) => {
|
|
6587
6667
|
logger.log(`[Session ${sessionId}] Sharing config updated \u2014 persisting to disk`);
|
|
6588
6668
|
sessionMetadata = { ...sessionMetadata, sharing: newSharing };
|
|
@@ -6605,72 +6685,33 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6605
6685
|
lastSpawnMeta = { ...lastSpawnMeta, appendSystemPrompt: prompt };
|
|
6606
6686
|
return await restartClaudeHandler();
|
|
6607
6687
|
},
|
|
6688
|
+
onKillSession: () => {
|
|
6689
|
+
logger.log(`[Session ${sessionId}] Kill session requested`);
|
|
6690
|
+
stopSession(sessionId);
|
|
6691
|
+
},
|
|
6608
6692
|
onInboxMessage: (message) => {
|
|
6609
|
-
|
|
6693
|
+
if (trackedSession?.stopped) return;
|
|
6694
|
+
logger.log(`[Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
|
|
6610
6695
|
if (message.urgency === "urgent") {
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
if (
|
|
6614
|
-
|
|
6615
|
-
sessionMetadata = { ...sessionMetadata, inbox: [{ messageId: message.messageId, body: text, displayText: `[Inbox] ${message.subject}`, timestamp: Date.now(), read: false, from: message.from, fromSession: message.fromSession, subject: message.subject, urgency: "urgent" }, ...existingInbox] };
|
|
6616
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
6696
|
+
const formatted = formatInboxMessageXml(message);
|
|
6697
|
+
logger.log(`[Session ${sessionId}] Delivering urgent inbox message to agent`);
|
|
6698
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6699
|
+
spawnClaude(formatted);
|
|
6617
6700
|
} else {
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
try {
|
|
6624
|
-
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6625
|
-
spawnClaude(text);
|
|
6626
|
-
} else {
|
|
6627
|
-
const stdinMsg = JSON.stringify({
|
|
6628
|
-
type: "user",
|
|
6629
|
-
message: { role: "user", content: text }
|
|
6630
|
-
});
|
|
6631
|
-
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6632
|
-
}
|
|
6633
|
-
} catch (err) {
|
|
6634
|
-
logger.log(`[Session ${sessionId}] Error injecting urgent inbox message: ${err.message}`);
|
|
6635
|
-
sessionWasProcessing = false;
|
|
6636
|
-
signalProcessing(false);
|
|
6637
|
-
}
|
|
6638
|
-
}
|
|
6639
|
-
const inbox = (sessionMetadata.inbox || []).map(
|
|
6640
|
-
(m) => m.messageId === message.messageId ? { ...m, read: true } : m
|
|
6641
|
-
);
|
|
6642
|
-
sessionMetadata = { ...sessionMetadata, inbox };
|
|
6643
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
6644
|
-
} else {
|
|
6645
|
-
logger.log(`[Session ${sessionId}] Normal inbox message from ${message.from}: ${message.subject}`);
|
|
6646
|
-
if (!sessionWasProcessing && !trackedSession.stopped) {
|
|
6647
|
-
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
6701
|
+
const stdinMsg = JSON.stringify({
|
|
6702
|
+
type: "user",
|
|
6703
|
+
message: { role: "user", content: formatted }
|
|
6704
|
+
});
|
|
6705
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6648
6706
|
}
|
|
6707
|
+
signalProcessing(true);
|
|
6708
|
+
sessionWasProcessing = true;
|
|
6649
6709
|
}
|
|
6650
6710
|
},
|
|
6651
|
-
onKillSession: () => {
|
|
6652
|
-
logger.log(`[Session ${sessionId}] Kill session requested`);
|
|
6653
|
-
stopSession(sessionId);
|
|
6654
|
-
},
|
|
6655
6711
|
onMetadataUpdate: (newMeta) => {
|
|
6656
6712
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6657
|
-
const legacyQueue = newMeta.messageQueue;
|
|
6658
|
-
let migratedInbox = newMeta.inbox || [];
|
|
6659
|
-
if (legacyQueue && legacyQueue.length > 0) {
|
|
6660
|
-
const converted = legacyQueue.map((q) => ({
|
|
6661
|
-
messageId: q.id,
|
|
6662
|
-
body: q.text,
|
|
6663
|
-
displayText: q.displayText,
|
|
6664
|
-
timestamp: q.createdAt,
|
|
6665
|
-
read: false
|
|
6666
|
-
}));
|
|
6667
|
-
migratedInbox = [...migratedInbox || [], ...converted];
|
|
6668
|
-
}
|
|
6669
6713
|
sessionMetadata = {
|
|
6670
6714
|
...newMeta,
|
|
6671
|
-
inbox: migratedInbox.length > 0 ? migratedInbox : newMeta.inbox,
|
|
6672
|
-
// Clear legacy messageQueue — daemon uses inbox only
|
|
6673
|
-
...legacyQueue && legacyQueue.length > 0 ? { messageQueue: void 0 } : {},
|
|
6674
6715
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6675
6716
|
lifecycleState: sessionMetadata.lifecycleState,
|
|
6676
6717
|
// Preserve claudeSessionId set by 'system init' (frontend may not have it)
|
|
@@ -6686,11 +6727,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6686
6727
|
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
6687
6728
|
sessionService.updateMetadata(sessionMetadata);
|
|
6688
6729
|
}
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
}
|
|
6692
|
-
const unreadInbox = sessionMetadata.inbox?.filter((m) => !m.read);
|
|
6693
|
-
if (unreadInbox && unreadInbox.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
6730
|
+
const queue = newMeta.messageQueue;
|
|
6731
|
+
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
6694
6732
|
setTimeout(() => {
|
|
6695
6733
|
processMessageQueueRef?.();
|
|
6696
6734
|
}, 200);
|
|
@@ -6813,18 +6851,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6813
6851
|
if (trackedSession?.stopped) return;
|
|
6814
6852
|
if (isKillingClaude) return;
|
|
6815
6853
|
if (isRestartingClaude || isSwitchingMode) return;
|
|
6816
|
-
const
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
const
|
|
6820
|
-
|
|
6821
|
-
sessionMetadata = { ...sessionMetadata, inbox: updatedInbox };
|
|
6854
|
+
const queue = sessionMetadata.messageQueue;
|
|
6855
|
+
if (queue && queue.length > 0) {
|
|
6856
|
+
const next = queue[0];
|
|
6857
|
+
const remaining = queue.slice(1);
|
|
6858
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0 };
|
|
6822
6859
|
sessionService.updateMetadata(sessionMetadata);
|
|
6823
|
-
|
|
6824
|
-
logger.log(`[Session ${sessionId}] Processing inbox message: "${next.body.slice(0, 50)}..."`);
|
|
6860
|
+
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
6825
6861
|
sessionWasProcessing = true;
|
|
6826
6862
|
signalProcessing(true);
|
|
6827
|
-
sessionService.pushMessage(next.displayText || next.
|
|
6863
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
6828
6864
|
userMessagePending = true;
|
|
6829
6865
|
turnInitiatedByUser = true;
|
|
6830
6866
|
const queueMeta = next.ralphSystemPrompt ? { appendSystemPrompt: next.ralphSystemPrompt } : void 0;
|
|
@@ -6846,7 +6882,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6846
6882
|
if (trackedSession?.stopped) return;
|
|
6847
6883
|
if (isRestartingClaude || isSwitchingMode) return;
|
|
6848
6884
|
claudeResumeId = void 0;
|
|
6849
|
-
spawnClaude(
|
|
6885
|
+
spawnClaude(next.text, queueMeta);
|
|
6850
6886
|
} catch (err) {
|
|
6851
6887
|
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
6852
6888
|
isKillingClaude = false;
|
|
@@ -6857,11 +6893,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6857
6893
|
} else {
|
|
6858
6894
|
try {
|
|
6859
6895
|
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6860
|
-
spawnClaude(
|
|
6896
|
+
spawnClaude(next.text, queueMeta);
|
|
6861
6897
|
} else {
|
|
6862
6898
|
const stdinMsg = JSON.stringify({
|
|
6863
6899
|
type: "user",
|
|
6864
|
-
message: { role: "user", content:
|
|
6900
|
+
message: { role: "user", content: next.text }
|
|
6865
6901
|
});
|
|
6866
6902
|
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6867
6903
|
}
|
|
@@ -6994,26 +7030,27 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6994
7030
|
}
|
|
6995
7031
|
if (!acpBackendReady) {
|
|
6996
7032
|
logger.log(`[${agentName} Session ${sessionId}] Backend not ready \u2014 queuing message`);
|
|
6997
|
-
const
|
|
7033
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
6998
7034
|
sessionMetadata = {
|
|
6999
7035
|
...sessionMetadata,
|
|
7000
|
-
|
|
7036
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
7001
7037
|
};
|
|
7002
7038
|
sessionService.updateMetadata(sessionMetadata);
|
|
7003
7039
|
return;
|
|
7004
7040
|
}
|
|
7005
7041
|
if (sessionMetadata.lifecycleState === "running") {
|
|
7006
7042
|
logger.log(`[${agentName} Session ${sessionId}] Agent busy \u2014 queuing message`);
|
|
7007
|
-
const
|
|
7043
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
7008
7044
|
sessionMetadata = {
|
|
7009
7045
|
...sessionMetadata,
|
|
7010
|
-
|
|
7046
|
+
messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
|
|
7011
7047
|
};
|
|
7012
7048
|
sessionService.updateMetadata(sessionMetadata);
|
|
7013
7049
|
return;
|
|
7014
7050
|
}
|
|
7015
7051
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
7016
7052
|
sessionService.updateMetadata(sessionMetadata);
|
|
7053
|
+
sessionService.sendKeepAlive(true);
|
|
7017
7054
|
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
7018
7055
|
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
7019
7056
|
if (!acpStopped) {
|
|
@@ -7064,10 +7101,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7064
7101
|
sessionMetadata = { ...sessionMetadata, securityContext: newSecurityContext };
|
|
7065
7102
|
return { success: false, message: "Security context updates with restart are not yet supported for this agent type." };
|
|
7066
7103
|
},
|
|
7067
|
-
onUpdateIsolation: async (enabled) => {
|
|
7068
|
-
sessionMetadata = { ...sessionMetadata, forceIsolation: enabled };
|
|
7069
|
-
return { success: false, message: "Isolation changes with restart are not yet supported for this agent type." };
|
|
7070
|
-
},
|
|
7071
7104
|
onSharingUpdate: (newSharing) => {
|
|
7072
7105
|
logger.log(`[${agentName} Session ${sessionId}] Sharing config updated \u2014 persisting in-memory`);
|
|
7073
7106
|
sessionMetadata = { ...sessionMetadata, sharing: newSharing };
|
|
@@ -7075,59 +7108,33 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7075
7108
|
onApplySystemPrompt: async () => {
|
|
7076
7109
|
return { success: false, message: "System prompt updates with restart are not yet supported for this agent type." };
|
|
7077
7110
|
},
|
|
7078
|
-
onInboxMessage: (message) => {
|
|
7079
|
-
const xmlText = formatInboxMessageXml(message);
|
|
7080
|
-
if (message.urgency === "urgent") {
|
|
7081
|
-
logger.log(`[${agentName} Session ${sessionId}] Urgent inbox message from ${message.from}: ${message.subject}`);
|
|
7082
|
-
if (sessionMetadata.lifecycleState === "running" || !acpBackendReady) {
|
|
7083
|
-
const existingInbox = sessionMetadata.inbox || [];
|
|
7084
|
-
sessionMetadata = { ...sessionMetadata, inbox: [{ messageId: message.messageId, body: xmlText, displayText: `[Inbox] ${message.subject}`, timestamp: Date.now(), read: false, from: message.from, fromSession: message.fromSession, subject: message.subject, urgency: "urgent" }, ...existingInbox] };
|
|
7085
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
7086
|
-
} else {
|
|
7087
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
7088
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
7089
|
-
sessionService.pushMessage(`[Inbox] ${message.subject}`, "user");
|
|
7090
|
-
agentBackend.sendPrompt(sessionId, xmlText).catch((err) => {
|
|
7091
|
-
logger.error(`[${agentName} Session ${sessionId}] Error sending urgent inbox: ${err.message}`);
|
|
7092
|
-
if (!acpStopped) {
|
|
7093
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7094
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
7095
|
-
sessionService.sendSessionEnd();
|
|
7096
|
-
}
|
|
7097
|
-
});
|
|
7098
|
-
}
|
|
7099
|
-
const inbox = (sessionMetadata.inbox || []).map(
|
|
7100
|
-
(m) => m.messageId === message.messageId ? { ...m, read: true } : m
|
|
7101
|
-
);
|
|
7102
|
-
sessionMetadata = { ...sessionMetadata, inbox };
|
|
7103
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
7104
|
-
} else {
|
|
7105
|
-
logger.log(`[${agentName} Session ${sessionId}] Normal inbox message from ${message.from}: ${message.subject}`);
|
|
7106
|
-
}
|
|
7107
|
-
},
|
|
7108
7111
|
onKillSession: () => {
|
|
7109
7112
|
logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
|
|
7110
7113
|
stopSession(sessionId);
|
|
7111
7114
|
},
|
|
7115
|
+
onInboxMessage: (message) => {
|
|
7116
|
+
if (acpStopped) return;
|
|
7117
|
+
logger.log(`[${agentName} Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
|
|
7118
|
+
if (message.urgency === "urgent" && acpBackendReady) {
|
|
7119
|
+
const formatted = formatInboxMessageXml(message);
|
|
7120
|
+
logger.log(`[${agentName} Session ${sessionId}] Delivering urgent inbox message to agent`);
|
|
7121
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
7122
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
7123
|
+
sessionService.sendKeepAlive(true);
|
|
7124
|
+
agentBackend.sendPrompt(sessionId, formatted).catch((err) => {
|
|
7125
|
+
logger.error(`[${agentName} Session ${sessionId}] Error delivering urgent inbox message: ${err.message}`);
|
|
7126
|
+
if (!acpStopped) {
|
|
7127
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7128
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
7129
|
+
sessionService.sendSessionEnd();
|
|
7130
|
+
}
|
|
7131
|
+
});
|
|
7132
|
+
}
|
|
7133
|
+
},
|
|
7112
7134
|
onMetadataUpdate: (newMeta) => {
|
|
7113
7135
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
7114
|
-
const legacyQueue = newMeta.messageQueue;
|
|
7115
|
-
let migratedInbox = newMeta.inbox || [];
|
|
7116
|
-
if (legacyQueue && legacyQueue.length > 0) {
|
|
7117
|
-
const converted = legacyQueue.map((q) => ({
|
|
7118
|
-
messageId: q.id,
|
|
7119
|
-
body: q.text,
|
|
7120
|
-
displayText: q.displayText,
|
|
7121
|
-
timestamp: q.createdAt,
|
|
7122
|
-
read: false
|
|
7123
|
-
}));
|
|
7124
|
-
migratedInbox = [...migratedInbox || [], ...converted];
|
|
7125
|
-
}
|
|
7126
7136
|
sessionMetadata = {
|
|
7127
7137
|
...newMeta,
|
|
7128
|
-
inbox: migratedInbox.length > 0 ? migratedInbox : newMeta.inbox,
|
|
7129
|
-
// Clear legacy messageQueue — daemon uses inbox only
|
|
7130
|
-
...legacyQueue && legacyQueue.length > 0 ? { messageQueue: void 0 } : {},
|
|
7131
7138
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
7132
7139
|
lifecycleState: sessionMetadata.lifecycleState,
|
|
7133
7140
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
@@ -7141,24 +7148,17 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7141
7148
|
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
7142
7149
|
sessionService.updateMetadata(sessionMetadata);
|
|
7143
7150
|
}
|
|
7144
|
-
if (legacyQueue && legacyQueue.length > 0) {
|
|
7145
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
7146
|
-
}
|
|
7147
7151
|
if (acpStopped) return;
|
|
7148
|
-
const
|
|
7149
|
-
if (
|
|
7150
|
-
const next =
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
inbox: (sessionMetadata.inbox || []).map((m) => m.messageId === next.messageId ? { ...m, read: true } : m),
|
|
7154
|
-
lifecycleState: "running"
|
|
7155
|
-
};
|
|
7152
|
+
const queue = newMeta.messageQueue;
|
|
7153
|
+
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
7154
|
+
const next = queue[0];
|
|
7155
|
+
const remaining = queue.slice(1);
|
|
7156
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
7156
7157
|
sessionService.updateMetadata(sessionMetadata);
|
|
7157
|
-
|
|
7158
|
-
logger.log(`[Session ${sessionId}] Processing inbox message from metadata update: "${next.body.slice(0, 50)}..."`);
|
|
7158
|
+
logger.log(`[Session ${sessionId}] Processing queued message from metadata update: "${next.text.slice(0, 50)}..."`);
|
|
7159
7159
|
sessionService.sendKeepAlive(true);
|
|
7160
|
-
agentBackend.sendPrompt(sessionId,
|
|
7161
|
-
logger.error(`[Session ${sessionId}] Error processing
|
|
7160
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
7161
|
+
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
7162
7162
|
if (!acpStopped) {
|
|
7163
7163
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7164
7164
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -7275,20 +7275,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7275
7275
|
() => {
|
|
7276
7276
|
if (acpStopped) return;
|
|
7277
7277
|
if (insideOnTurnEnd) return;
|
|
7278
|
-
const
|
|
7279
|
-
if (
|
|
7280
|
-
const next =
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
inbox: (sessionMetadata.inbox || []).map((m) => m.messageId === next.messageId ? { ...m, read: true } : m),
|
|
7284
|
-
lifecycleState: "running"
|
|
7285
|
-
};
|
|
7278
|
+
const queue = sessionMetadata.messageQueue;
|
|
7279
|
+
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
7280
|
+
const next = queue[0];
|
|
7281
|
+
const remaining = queue.slice(1);
|
|
7282
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
7286
7283
|
sessionService.updateMetadata(sessionMetadata);
|
|
7287
|
-
|
|
7288
|
-
logger.log(`[Session ${sessionId}] Processing inbox message (ACP ralph activation): "${next.body.slice(0, 50)}..."`);
|
|
7284
|
+
logger.log(`[Session ${sessionId}] Processing queued message (ACP ralph activation): "${next.text.slice(0, 50)}..."`);
|
|
7289
7285
|
sessionService.sendKeepAlive(true);
|
|
7290
|
-
agentBackend.sendPrompt(sessionId,
|
|
7291
|
-
logger.error(`[Session ${sessionId}] Error processing
|
|
7286
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
7287
|
+
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
7292
7288
|
if (!acpStopped) {
|
|
7293
7289
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7294
7290
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -7302,7 +7298,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7302
7298
|
const writeSvampConfigPatchAcp = svampConfigChecker.writeConfig;
|
|
7303
7299
|
const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
|
|
7304
7300
|
let agentIsoConfig;
|
|
7305
|
-
if ((
|
|
7301
|
+
if ((options2.forceIsolation || sessionMetadata.sharing?.enabled) && isolationCapabilities.preferred) {
|
|
7306
7302
|
const method = isolationCapabilities.preferred;
|
|
7307
7303
|
const detail = isolationCapabilities.details[method];
|
|
7308
7304
|
if (detail.found && detail.verified !== false) {
|
|
@@ -7370,21 +7366,17 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7370
7366
|
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
7371
7367
|
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
7372
7368
|
} else {
|
|
7373
|
-
const
|
|
7374
|
-
if (
|
|
7375
|
-
const next =
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
inbox: (sessionMetadata.inbox || []).map((m) => m.messageId === next.messageId ? { ...m, read: true } : m),
|
|
7379
|
-
lifecycleState: "running"
|
|
7380
|
-
};
|
|
7369
|
+
const pendingQueue = sessionMetadata.messageQueue;
|
|
7370
|
+
if (pendingQueue && pendingQueue.length > 0) {
|
|
7371
|
+
const next = pendingQueue[0];
|
|
7372
|
+
const remaining = pendingQueue.slice(1);
|
|
7373
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
7381
7374
|
sessionService.updateMetadata(sessionMetadata);
|
|
7382
7375
|
sessionService.sendKeepAlive(true);
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
logger.error(`[${agentName} Session ${sessionId}] Error processing inbox message (Ralph): ${err.message}`);
|
|
7376
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
7377
|
+
logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
|
|
7378
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
7379
|
+
logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
|
|
7388
7380
|
if (!acpStopped) {
|
|
7389
7381
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7390
7382
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -7451,21 +7443,17 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7451
7443
|
return;
|
|
7452
7444
|
}
|
|
7453
7445
|
}
|
|
7454
|
-
const
|
|
7455
|
-
if (
|
|
7456
|
-
const next =
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
inbox: (sessionMetadata.inbox || []).map((m) => m.messageId === next.messageId ? { ...m, read: true } : m),
|
|
7460
|
-
lifecycleState: "running"
|
|
7461
|
-
};
|
|
7446
|
+
const queue = sessionMetadata.messageQueue;
|
|
7447
|
+
if (queue && queue.length > 0) {
|
|
7448
|
+
const next = queue[0];
|
|
7449
|
+
const remaining = queue.slice(1);
|
|
7450
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
7462
7451
|
sessionService.updateMetadata(sessionMetadata);
|
|
7463
7452
|
sessionService.sendKeepAlive(true);
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
logger.error(`[Session ${sessionId}] Error processing inbox message: ${err.message}`);
|
|
7453
|
+
logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
|
|
7454
|
+
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
7455
|
+
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
7456
|
+
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
7469
7457
|
if (!acpStopped) {
|
|
7470
7458
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7471
7459
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -7676,7 +7664,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7676
7664
|
// Restore sharing & security context from persisted metadata
|
|
7677
7665
|
sharing: persisted.metadata?.sharing,
|
|
7678
7666
|
securityContext: persisted.metadata?.securityContext,
|
|
7679
|
-
forceIsolation:
|
|
7667
|
+
forceIsolation: !!persisted.metadata?.isolationMethod,
|
|
7680
7668
|
// Block queue processing until auto-continue completes
|
|
7681
7669
|
wasProcessing: persisted.wasProcessing && !!persisted.claudeResumeId && !isOrphaned
|
|
7682
7670
|
});
|
|
@@ -7835,7 +7823,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7835
7823
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
7836
7824
|
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
7837
7825
|
const PING_TIMEOUT_MS = 6e4;
|
|
7838
|
-
const MAX_FAILURES = 60;
|
|
7839
7826
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
7840
7827
|
let heartbeatRunning = false;
|
|
7841
7828
|
let lastReconnectAt = 0;
|
|
@@ -7906,7 +7893,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7906
7893
|
if (consecutiveHeartbeatFailures === 1) {
|
|
7907
7894
|
logger.log(`Ping failed: ${err.message}`);
|
|
7908
7895
|
} else if (consecutiveHeartbeatFailures % 6 === 0) {
|
|
7909
|
-
logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures}
|
|
7896
|
+
logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures} failures, retrying indefinitely)`);
|
|
7910
7897
|
}
|
|
7911
7898
|
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
|
|
7912
7899
|
const conn = server.rpc?._connection;
|
|
@@ -7919,17 +7906,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7919
7906
|
}
|
|
7920
7907
|
}
|
|
7921
7908
|
if (conn?._reader) {
|
|
7922
|
-
logger.log("
|
|
7909
|
+
logger.log("Cancelling stale HTTP stream reader to trigger reconnection");
|
|
7923
7910
|
try {
|
|
7924
7911
|
conn._reader.cancel?.("Stale connection");
|
|
7925
7912
|
} catch {
|
|
7926
7913
|
}
|
|
7914
|
+
} else if (conn?._abort_controller) {
|
|
7915
|
+
logger.log("Aborting stale HTTP stream to trigger reconnection");
|
|
7916
|
+
try {
|
|
7917
|
+
conn._abort_controller.abort();
|
|
7918
|
+
} catch {
|
|
7919
|
+
}
|
|
7927
7920
|
}
|
|
7928
7921
|
}
|
|
7929
|
-
if (consecutiveHeartbeatFailures >= MAX_FAILURES) {
|
|
7930
|
-
logger.log(`Heartbeat failed ${MAX_FAILURES} times. Shutting down.`);
|
|
7931
|
-
requestShutdown("heartbeat-timeout", err.message);
|
|
7932
|
-
}
|
|
7933
7922
|
}
|
|
7934
7923
|
}
|
|
7935
7924
|
} finally {
|
|
@@ -8024,8 +8013,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8024
8013
|
};
|
|
8025
8014
|
const shutdownReq = await resolvesWhenShutdownRequested;
|
|
8026
8015
|
await cleanup(shutdownReq.source);
|
|
8027
|
-
if (process.env.SVAMP_SUPERVISED === "1"
|
|
8028
|
-
|
|
8016
|
+
if (process.env.SVAMP_SUPERVISED === "1") {
|
|
8017
|
+
const intentionalSources = ["os-signal", "os-signal-cleanup", "hypha-app"];
|
|
8018
|
+
if (!intentionalSources.includes(shutdownReq.source)) {
|
|
8019
|
+
process.exit(1);
|
|
8020
|
+
}
|
|
8029
8021
|
}
|
|
8030
8022
|
process.exit(0);
|
|
8031
8023
|
} catch (error) {
|