svamp-cli 0.1.75 → 0.1.78
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 → agentCommands-uNFhhdN1.mjs} +108 -1
- package/dist/cli.mjs +254 -95
- package/dist/{commands-a7p1jW-t.mjs → commands-B6FEeZeP.mjs} +129 -61
- package/dist/{commands-BLjcT1Vl.mjs → commands-BYbuedOK.mjs} +68 -4
- package/dist/{commands-Dq8WSqvt.mjs → commands-Cf3mXxPZ.mjs} +2 -2
- package/dist/{commands-UFi0_ESV.mjs → commands-DJoYOM_1.mjs} +25 -25
- package/dist/index.mjs +1 -1
- package/dist/{package-BFnad6d1.mjs → package-DTOqWYBv.mjs} +4 -4
- package/dist/{run-Dy5lxT3M.mjs → run-DqvxMsWh.mjs} +1 -1
- package/dist/{run-lhAjX4NB.mjs → run-DsXDjwLW.mjs} +369 -84
- package/dist/staticServer-CWcmMF5V.mjs +477 -0
- package/dist/{tunnel-C2kqST5d.mjs → tunnel-BDKdemh0.mjs} +51 -9
- package/package.json +4 -4
|
@@ -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';
|
|
@@ -309,18 +309,6 @@ async function getPtyModule() {
|
|
|
309
309
|
function generateTerminalId() {
|
|
310
310
|
return `term-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
311
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
|
-
}
|
|
324
312
|
function getMachineMetadataPath(svampHomeDir) {
|
|
325
313
|
return join(svampHomeDir, "machine-metadata.json");
|
|
326
314
|
}
|
|
@@ -705,43 +693,78 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
705
693
|
const cwd = params.cwd || getHomedir();
|
|
706
694
|
const shell = params.shell || process.env.SHELL || "bash";
|
|
707
695
|
const sessionId = generateTerminalId();
|
|
696
|
+
const cleanEnv = { ...process.env };
|
|
697
|
+
const claudeEnvVars = Object.keys(cleanEnv).filter(
|
|
698
|
+
(k) => k.startsWith("CLAUDE_") || k.startsWith("MCP_") || k === "ANTHROPIC_API_KEY"
|
|
699
|
+
);
|
|
700
|
+
for (const k of claudeEnvVars) delete cleanEnv[k];
|
|
708
701
|
const ptyProcess = pty.spawn(shell, [], {
|
|
709
702
|
name: "xterm-256color",
|
|
710
703
|
cols,
|
|
711
704
|
rows,
|
|
712
705
|
cwd,
|
|
713
|
-
env: {
|
|
706
|
+
env: {
|
|
707
|
+
...cleanEnv,
|
|
708
|
+
TERM: "xterm-256color"
|
|
709
|
+
}
|
|
714
710
|
});
|
|
711
|
+
if (shell.endsWith("zsh")) {
|
|
712
|
+
ptyProcess.write("unsetopt PROMPT_SP\n");
|
|
713
|
+
ptyProcess.write("clear\n");
|
|
714
|
+
}
|
|
715
715
|
const session = {
|
|
716
716
|
id: sessionId,
|
|
717
717
|
pty: ptyProcess,
|
|
718
|
-
|
|
718
|
+
outputBuffer: [],
|
|
719
|
+
exited: false,
|
|
719
720
|
cols,
|
|
720
721
|
rows,
|
|
721
722
|
createdAt: Date.now(),
|
|
722
723
|
cwd
|
|
723
724
|
};
|
|
724
725
|
terminalSessions.set(sessionId, session);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
726
|
+
let outputBatch = "";
|
|
727
|
+
let outputTimer = null;
|
|
728
|
+
const flushOutput = () => {
|
|
729
|
+
outputTimer = null;
|
|
730
|
+
if (!outputBatch) return;
|
|
731
|
+
const batch = outputBatch;
|
|
732
|
+
outputBatch = "";
|
|
733
|
+
session.outputBuffer.push(batch);
|
|
734
|
+
if (session.outputBuffer.length > 1e3) {
|
|
735
|
+
session.outputBuffer.splice(0, session.outputBuffer.length - 500);
|
|
734
736
|
}
|
|
737
|
+
try {
|
|
738
|
+
server.emit({
|
|
739
|
+
type: "svamp:terminal-output",
|
|
740
|
+
data: { type: "output", content: batch, sessionId },
|
|
741
|
+
to: "*"
|
|
742
|
+
});
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
ptyProcess.onData((data) => {
|
|
747
|
+
if (!data) return;
|
|
748
|
+
outputBatch += data;
|
|
749
|
+
if (!outputTimer) outputTimer = setTimeout(flushOutput, 16);
|
|
735
750
|
});
|
|
736
751
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
752
|
+
session.exited = true;
|
|
753
|
+
session.exitCode = exitCode;
|
|
754
|
+
session.exitSignal = signal;
|
|
755
|
+
try {
|
|
756
|
+
server.emit({
|
|
757
|
+
type: "svamp:terminal-output",
|
|
758
|
+
data: { type: "exit", content: "", sessionId, exitCode, signal },
|
|
759
|
+
to: "*"
|
|
760
|
+
});
|
|
761
|
+
} catch {
|
|
743
762
|
}
|
|
744
|
-
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
if (terminalSessions.has(sessionId)) {
|
|
765
|
+
terminalSessions.delete(sessionId);
|
|
766
|
+
}
|
|
767
|
+
}, 6e4);
|
|
745
768
|
});
|
|
746
769
|
return { sessionId, cols, rows };
|
|
747
770
|
},
|
|
@@ -764,23 +787,34 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
764
787
|
return { success: true };
|
|
765
788
|
},
|
|
766
789
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
790
|
+
* Read buffered output from a terminal session (polling).
|
|
791
|
+
* Returns { output, exited, exitCode? }. Drains the buffer.
|
|
769
792
|
*/
|
|
770
|
-
|
|
793
|
+
terminalRead: async (params, context) => {
|
|
771
794
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
772
795
|
const session = terminalSessions.get(params.sessionId);
|
|
773
796
|
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
797
|
+
const output = session.outputBuffer.splice(0).join("");
|
|
798
|
+
const result = {
|
|
799
|
+
output,
|
|
800
|
+
exited: session.exited
|
|
801
|
+
};
|
|
802
|
+
if (session.exited) {
|
|
803
|
+
result.exitCode = session.exitCode;
|
|
804
|
+
result.exitSignal = session.exitSignal;
|
|
805
|
+
if (!output) terminalSessions.delete(params.sessionId);
|
|
806
|
+
}
|
|
807
|
+
return result;
|
|
777
808
|
},
|
|
778
|
-
/** Stop (kill) a terminal session. */
|
|
809
|
+
/** Stop (kill) a terminal session. Idempotent — returns success even if already gone. */
|
|
779
810
|
terminalStop: async (params, context) => {
|
|
780
811
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
781
812
|
const session = terminalSessions.get(params.sessionId);
|
|
782
|
-
if (!session)
|
|
783
|
-
|
|
813
|
+
if (!session) return { success: true };
|
|
814
|
+
try {
|
|
815
|
+
session.pty.kill();
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
784
818
|
terminalSessions.delete(params.sessionId);
|
|
785
819
|
return { success: true };
|
|
786
820
|
},
|
|
@@ -792,9 +826,23 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
792
826
|
cols: s.cols,
|
|
793
827
|
rows: s.rows,
|
|
794
828
|
cwd: s.cwd,
|
|
795
|
-
createdAt: s.createdAt
|
|
829
|
+
createdAt: s.createdAt,
|
|
830
|
+
exited: s.exited
|
|
796
831
|
}));
|
|
797
832
|
},
|
|
833
|
+
/** Reattach to an existing terminal session. Returns buffered output for scrollback replay. */
|
|
834
|
+
terminalReattach: async (params, context) => {
|
|
835
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
836
|
+
const session = terminalSessions.get(params.sessionId);
|
|
837
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
838
|
+
const scrollback = session.outputBuffer.join("");
|
|
839
|
+
if (params.cols && params.rows) {
|
|
840
|
+
session.pty.resize(params.cols, params.rows);
|
|
841
|
+
session.cols = params.cols;
|
|
842
|
+
session.rows = params.rows;
|
|
843
|
+
}
|
|
844
|
+
return { sessionId: session.id, cols: session.cols, rows: session.rows, scrollback, exited: session.exited };
|
|
845
|
+
},
|
|
798
846
|
// Machine-level directory listing (read-only, view role)
|
|
799
847
|
listDirectory: async (path, options, context) => {
|
|
800
848
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
@@ -926,6 +974,48 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
926
974
|
const { deleteServiceGroup } = await import('./api-BRbsyqJ4.mjs');
|
|
927
975
|
return deleteServiceGroup(params.name);
|
|
928
976
|
},
|
|
977
|
+
// ── Tunnel management ────────────────────────────────────────────
|
|
978
|
+
/** Start a reverse tunnel for a service group (local/cloud machine). */
|
|
979
|
+
tunnelStart: async (params, context) => {
|
|
980
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
981
|
+
const tunnels = handlers.tunnels;
|
|
982
|
+
if (!tunnels) throw new Error("Tunnel management not available");
|
|
983
|
+
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
984
|
+
const { TunnelClient } = await import('./tunnel-BDKdemh0.mjs');
|
|
985
|
+
const client = new TunnelClient({
|
|
986
|
+
name: params.name,
|
|
987
|
+
ports: params.ports,
|
|
988
|
+
maxReconnectAttempts: 0,
|
|
989
|
+
// infinite for daemon
|
|
990
|
+
onError: (err) => console.error(`[TUNNEL] ${params.name}: ${err.message}`),
|
|
991
|
+
onConnect: () => console.log(`[TUNNEL] ${params.name}: connected`),
|
|
992
|
+
onDisconnect: () => console.log(`[TUNNEL] ${params.name}: disconnected, reconnecting...`)
|
|
993
|
+
});
|
|
994
|
+
await client.connect();
|
|
995
|
+
tunnels.set(params.name, client);
|
|
996
|
+
return { name: params.name, ports: params.ports, ...client.status };
|
|
997
|
+
},
|
|
998
|
+
/** Stop a tunnel. */
|
|
999
|
+
tunnelStop: async (params, context) => {
|
|
1000
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
1001
|
+
const tunnels = handlers.tunnels;
|
|
1002
|
+
if (!tunnels) throw new Error("Tunnel management not available");
|
|
1003
|
+
const client = tunnels.get(params.name);
|
|
1004
|
+
if (!client) throw new Error(`Tunnel '${params.name}' not found`);
|
|
1005
|
+
client.destroy();
|
|
1006
|
+
tunnels.delete(params.name);
|
|
1007
|
+
return { name: params.name, stopped: true };
|
|
1008
|
+
},
|
|
1009
|
+
/** List active tunnels with health status. */
|
|
1010
|
+
tunnelList: async (context) => {
|
|
1011
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
1012
|
+
const tunnels = handlers.tunnels;
|
|
1013
|
+
if (!tunnels) return [];
|
|
1014
|
+
return Array.from(tunnels.entries()).map(([name, client]) => ({
|
|
1015
|
+
name,
|
|
1016
|
+
...client.status
|
|
1017
|
+
}));
|
|
1018
|
+
},
|
|
929
1019
|
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
930
1020
|
wiseCreateEphemeralToken: async (params, context) => {
|
|
931
1021
|
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
@@ -999,6 +1089,28 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
999
1089
|
};
|
|
1000
1090
|
}
|
|
1001
1091
|
|
|
1092
|
+
function isStructuredMessage(msg) {
|
|
1093
|
+
return !!(msg.from && msg.subject);
|
|
1094
|
+
}
|
|
1095
|
+
function escapeXml(s) {
|
|
1096
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1097
|
+
}
|
|
1098
|
+
function formatInboxMessageXml(msg) {
|
|
1099
|
+
if (!isStructuredMessage(msg)) return msg.body;
|
|
1100
|
+
const attrs = [`message-id="${escapeXml(msg.messageId)}"`];
|
|
1101
|
+
if (msg.from) attrs.push(`from="${escapeXml(msg.from)}"`);
|
|
1102
|
+
if (msg.fromSession) attrs.push(`from-session="${escapeXml(msg.fromSession)}"`);
|
|
1103
|
+
if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
|
|
1104
|
+
if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
|
|
1105
|
+
if (msg.urgency) attrs.push(`urgency="${msg.urgency}"`);
|
|
1106
|
+
if (msg.replyTo) attrs.push(`reply-to="${escapeXml(msg.replyTo)}"`);
|
|
1107
|
+
if (msg.cc && msg.cc.length > 0) attrs.push(`cc="${msg.cc.map(escapeXml).join(",")}"`);
|
|
1108
|
+
if (msg.threadId) attrs.push(`thread-id="${escapeXml(msg.threadId)}"`);
|
|
1109
|
+
attrs.push(`timestamp="${msg.timestamp}"`);
|
|
1110
|
+
return `<svamp-message ${attrs.join(" ")}>
|
|
1111
|
+
${msg.body}
|
|
1112
|
+
</svamp-message>`;
|
|
1113
|
+
}
|
|
1002
1114
|
function loadMessages(messagesDir, sessionId) {
|
|
1003
1115
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
1004
1116
|
if (!existsSync(filePath)) return [];
|
|
@@ -1040,6 +1152,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1040
1152
|
mode: "remote",
|
|
1041
1153
|
time: Date.now()
|
|
1042
1154
|
};
|
|
1155
|
+
const inbox = [];
|
|
1156
|
+
const INBOX_MAX = 100;
|
|
1043
1157
|
const listeners = [];
|
|
1044
1158
|
const removeListener = (listener, reason) => {
|
|
1045
1159
|
const idx = listeners.indexOf(listener);
|
|
@@ -1308,10 +1422,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1308
1422
|
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
1309
1423
|
return await callbacks.onListDirectory(path);
|
|
1310
1424
|
},
|
|
1311
|
-
bash: async (command, cwd, context) => {
|
|
1425
|
+
bash: async (command, cwd, timeout, context) => {
|
|
1312
1426
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
1313
1427
|
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
1314
|
-
return await callbacks.onBash(command, cwd);
|
|
1428
|
+
return await callbacks.onBash(command, cwd, timeout);
|
|
1315
1429
|
},
|
|
1316
1430
|
ripgrep: async (args, cwd, context) => {
|
|
1317
1431
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
@@ -1357,7 +1471,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1357
1471
|
callbacks.onSharingUpdate?.(newSharing);
|
|
1358
1472
|
return { success: true, sharing: newSharing };
|
|
1359
1473
|
},
|
|
1360
|
-
/** Update security context and restart the agent process with new rules
|
|
1474
|
+
/** Update security context and restart the agent process with new rules.
|
|
1475
|
+
* Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely. */
|
|
1361
1476
|
updateSecurityContext: async (newSecurityContext, context) => {
|
|
1362
1477
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
1363
1478
|
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
@@ -1366,14 +1481,15 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1366
1481
|
if (!callbacks.onUpdateSecurityContext) {
|
|
1367
1482
|
throw new Error("Security context updates are not supported for this session");
|
|
1368
1483
|
}
|
|
1369
|
-
|
|
1484
|
+
const resolvedContext = newSecurityContext === "__disable__" ? null : newSecurityContext;
|
|
1485
|
+
metadata = { ...metadata, securityContext: resolvedContext };
|
|
1370
1486
|
metadataVersion++;
|
|
1371
1487
|
notifyListeners({
|
|
1372
1488
|
type: "update-session",
|
|
1373
1489
|
sessionId,
|
|
1374
1490
|
metadata: { value: metadata, version: metadataVersion }
|
|
1375
1491
|
});
|
|
1376
|
-
return await callbacks.onUpdateSecurityContext(
|
|
1492
|
+
return await callbacks.onUpdateSecurityContext(resolvedContext);
|
|
1377
1493
|
},
|
|
1378
1494
|
/** Apply a new system prompt and restart the agent process */
|
|
1379
1495
|
applySystemPrompt: async (prompt, context) => {
|
|
@@ -1383,6 +1499,43 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1383
1499
|
}
|
|
1384
1500
|
return await callbacks.onApplySystemPrompt(prompt);
|
|
1385
1501
|
},
|
|
1502
|
+
// ── Inbox ──
|
|
1503
|
+
sendInboxMessage: async (message, context) => {
|
|
1504
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1505
|
+
inbox.push(message);
|
|
1506
|
+
while (inbox.length > INBOX_MAX) inbox.shift();
|
|
1507
|
+
callbacks.onInboxMessage?.(message);
|
|
1508
|
+
notifyListeners({ type: "inbox-update", sessionId, message });
|
|
1509
|
+
return { success: true, messageId: message.messageId };
|
|
1510
|
+
},
|
|
1511
|
+
getInbox: async (opts, context) => {
|
|
1512
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1513
|
+
let result = [...inbox];
|
|
1514
|
+
if (opts?.unread) result = result.filter((m) => !m.read);
|
|
1515
|
+
result.sort((a, b) => b.timestamp - a.timestamp);
|
|
1516
|
+
if (opts?.limit && opts.limit > 0) result = result.slice(0, opts.limit);
|
|
1517
|
+
return { messages: result, total: inbox.length };
|
|
1518
|
+
},
|
|
1519
|
+
markInboxRead: async (messageId, context) => {
|
|
1520
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1521
|
+
const msg = inbox.find((m) => m.messageId === messageId);
|
|
1522
|
+
if (!msg) return { success: false, error: "Message not found" };
|
|
1523
|
+
msg.read = true;
|
|
1524
|
+
notifyListeners({ type: "inbox-update", sessionId, message: msg });
|
|
1525
|
+
return { success: true };
|
|
1526
|
+
},
|
|
1527
|
+
clearInbox: async (opts, context) => {
|
|
1528
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1529
|
+
if (opts?.all) {
|
|
1530
|
+
inbox.length = 0;
|
|
1531
|
+
} else {
|
|
1532
|
+
for (let i = inbox.length - 1; i >= 0; i--) {
|
|
1533
|
+
if (inbox[i].read) inbox.splice(i, 1);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
notifyListeners({ type: "inbox-update", sessionId, cleared: true });
|
|
1537
|
+
return { success: true, remaining: inbox.length };
|
|
1538
|
+
},
|
|
1386
1539
|
// ── Listener Registration ──
|
|
1387
1540
|
registerListener: async (callback, context) => {
|
|
1388
1541
|
authorizeRequest(context, metadata.sharing, "view");
|
|
@@ -4167,6 +4320,8 @@ function sanitizeEnvForSharing(env) {
|
|
|
4167
4320
|
const DEFAULT_PROBE_INTERVAL_S = 10;
|
|
4168
4321
|
const DEFAULT_PROBE_TIMEOUT_S = 5;
|
|
4169
4322
|
const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
|
|
4323
|
+
const MAX_RESTART_DELAY_S = 300;
|
|
4324
|
+
const BACKOFF_RESET_WINDOW_MS = 6e4;
|
|
4170
4325
|
const MAX_LOG_LINES = 300;
|
|
4171
4326
|
class ProcessSupervisor {
|
|
4172
4327
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -4364,7 +4519,7 @@ class ProcessSupervisor {
|
|
|
4364
4519
|
for (const file of files) {
|
|
4365
4520
|
if (!file.endsWith(".json")) continue;
|
|
4366
4521
|
try {
|
|
4367
|
-
const raw = await readFile(
|
|
4522
|
+
const raw = await readFile(path__default.join(this.persistDir, file), "utf-8");
|
|
4368
4523
|
const spec = JSON.parse(raw);
|
|
4369
4524
|
const entry = this.makeEntry(spec);
|
|
4370
4525
|
this.entries.set(spec.id, entry);
|
|
@@ -4385,14 +4540,14 @@ class ProcessSupervisor {
|
|
|
4385
4540
|
}
|
|
4386
4541
|
}
|
|
4387
4542
|
async persistSpec(spec) {
|
|
4388
|
-
const filePath =
|
|
4543
|
+
const filePath = path__default.join(this.persistDir, `${spec.id}.json`);
|
|
4389
4544
|
const tmpPath = filePath + ".tmp";
|
|
4390
4545
|
await writeFile$1(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4391
4546
|
await rename(tmpPath, filePath);
|
|
4392
4547
|
}
|
|
4393
4548
|
async deleteSpec(id) {
|
|
4394
4549
|
try {
|
|
4395
|
-
await unlink(
|
|
4550
|
+
await unlink(path__default.join(this.persistDir, `${id}.json`));
|
|
4396
4551
|
} catch {
|
|
4397
4552
|
}
|
|
4398
4553
|
}
|
|
@@ -4498,8 +4653,19 @@ class ProcessSupervisor {
|
|
|
4498
4653
|
state.status = "failed";
|
|
4499
4654
|
return;
|
|
4500
4655
|
}
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4656
|
+
const uptime = state.startedAt ? Date.now() - state.startedAt : 0;
|
|
4657
|
+
const baseDelay = spec.restartDelay * 1e3;
|
|
4658
|
+
let delayMs;
|
|
4659
|
+
if (uptime > BACKOFF_RESET_WINDOW_MS) {
|
|
4660
|
+
state.restartCount = 0;
|
|
4661
|
+
delayMs = baseDelay;
|
|
4662
|
+
} else {
|
|
4663
|
+
const backoffExponent = Math.min(state.restartCount, 10);
|
|
4664
|
+
delayMs = Math.min(baseDelay * Math.pow(2, backoffExponent), MAX_RESTART_DELAY_S * 1e3);
|
|
4665
|
+
const jitter = (Math.random() * 0.2 - 0.1) * delayMs;
|
|
4666
|
+
delayMs = Math.max(baseDelay, Math.round(delayMs + jitter));
|
|
4667
|
+
}
|
|
4668
|
+
console.log(`[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (restart #${state.restartCount + 1}, uptime=${Math.round(uptime / 1e3)}s)`);
|
|
4503
4669
|
entry.restartTimer = setTimeout(() => {
|
|
4504
4670
|
if (entry.stopping) return;
|
|
4505
4671
|
state.restartCount++;
|
|
@@ -5537,6 +5703,7 @@ async function startDaemon(options) {
|
|
|
5537
5703
|
let server = null;
|
|
5538
5704
|
const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
|
|
5539
5705
|
await supervisor.init();
|
|
5706
|
+
const tunnels = /* @__PURE__ */ new Map();
|
|
5540
5707
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
5541
5708
|
});
|
|
5542
5709
|
preventMachineSleep(logger);
|
|
@@ -5657,6 +5824,7 @@ async function startDaemon(options) {
|
|
|
5657
5824
|
});
|
|
5658
5825
|
}, buildIsolationConfig2 = function(dir) {
|
|
5659
5826
|
if (!options2.forceIsolation && !sessionMetadata.sharing?.enabled) return null;
|
|
5827
|
+
if (sessionMetadata.securityContext === null) return null;
|
|
5660
5828
|
const method = isolationCapabilities.preferred;
|
|
5661
5829
|
if (!method) return null;
|
|
5662
5830
|
const detail = isolationCapabilities.details[method];
|
|
@@ -5676,7 +5844,7 @@ async function startDaemon(options) {
|
|
|
5676
5844
|
if (sessionMetadata.sharing?.enabled && stagedCredentials) {
|
|
5677
5845
|
config.credentialStagingPath = stagedCredentials.homePath;
|
|
5678
5846
|
}
|
|
5679
|
-
const activeSecurityContext = sessionMetadata.securityContext
|
|
5847
|
+
const activeSecurityContext = sessionMetadata.securityContext ?? options2.securityContext;
|
|
5680
5848
|
if (activeSecurityContext) {
|
|
5681
5849
|
config = applySecurityContext(config, activeSecurityContext);
|
|
5682
5850
|
}
|
|
@@ -6335,6 +6503,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6335
6503
|
logger.log(`[Session ${sessionId}] Retrying startup without --resume`);
|
|
6336
6504
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6337
6505
|
sessionService.updateMetadata(sessionMetadata);
|
|
6506
|
+
sessionService.sendKeepAlive(true);
|
|
6338
6507
|
spawnClaude(startupRetryMessage);
|
|
6339
6508
|
return;
|
|
6340
6509
|
}
|
|
@@ -6391,6 +6560,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6391
6560
|
}
|
|
6392
6561
|
if (claudeResumeId) {
|
|
6393
6562
|
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
6563
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6394
6564
|
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
6395
6565
|
return { success: true, message: "Claude process restarted successfully." };
|
|
6396
6566
|
} else {
|
|
@@ -6596,6 +6766,25 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6596
6766
|
logger.log(`[Session ${sessionId}] Kill session requested`);
|
|
6597
6767
|
stopSession(sessionId);
|
|
6598
6768
|
},
|
|
6769
|
+
onInboxMessage: (message) => {
|
|
6770
|
+
if (trackedSession?.stopped) return;
|
|
6771
|
+
logger.log(`[Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
|
|
6772
|
+
if (message.urgency === "urgent") {
|
|
6773
|
+
const formatted = formatInboxMessageXml(message);
|
|
6774
|
+
logger.log(`[Session ${sessionId}] Delivering urgent inbox message to agent`);
|
|
6775
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6776
|
+
spawnClaude(formatted);
|
|
6777
|
+
} else {
|
|
6778
|
+
const stdinMsg = JSON.stringify({
|
|
6779
|
+
type: "user",
|
|
6780
|
+
message: { role: "user", content: formatted }
|
|
6781
|
+
});
|
|
6782
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6783
|
+
}
|
|
6784
|
+
signalProcessing(true);
|
|
6785
|
+
sessionWasProcessing = true;
|
|
6786
|
+
}
|
|
6787
|
+
},
|
|
6599
6788
|
onMetadataUpdate: (newMeta) => {
|
|
6600
6789
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6601
6790
|
sessionMetadata = {
|
|
@@ -6625,11 +6814,12 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6625
6814
|
onUpdateConfig: (patch) => {
|
|
6626
6815
|
writeSvampConfigPatch(patch);
|
|
6627
6816
|
},
|
|
6628
|
-
onBash: async (command, cwd) => {
|
|
6629
|
-
|
|
6817
|
+
onBash: async (command, cwd, timeout) => {
|
|
6818
|
+
const execTimeout = timeout || 12e4;
|
|
6819
|
+
logger.log(`[Session ${sessionId}] Bash: ${command} (cwd: ${cwd || directory}, timeout: ${execTimeout}ms)`);
|
|
6630
6820
|
const { exec } = await import('child_process');
|
|
6631
6821
|
return new Promise((resolve2) => {
|
|
6632
|
-
exec(command, { cwd: cwd || directory, timeout:
|
|
6822
|
+
exec(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
6633
6823
|
if (err) {
|
|
6634
6824
|
resolve2({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
|
|
6635
6825
|
} else {
|
|
@@ -6938,6 +7128,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6938
7128
|
}
|
|
6939
7129
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6940
7130
|
sessionService.updateMetadata(sessionMetadata);
|
|
7131
|
+
sessionService.sendKeepAlive(true);
|
|
6941
7132
|
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
6942
7133
|
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
6943
7134
|
if (!acpStopped) {
|
|
@@ -6999,6 +7190,25 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6999
7190
|
logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
|
|
7000
7191
|
stopSession(sessionId);
|
|
7001
7192
|
},
|
|
7193
|
+
onInboxMessage: (message) => {
|
|
7194
|
+
if (acpStopped) return;
|
|
7195
|
+
logger.log(`[${agentName} Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
|
|
7196
|
+
if (message.urgency === "urgent" && acpBackendReady) {
|
|
7197
|
+
const formatted = formatInboxMessageXml(message);
|
|
7198
|
+
logger.log(`[${agentName} Session ${sessionId}] Delivering urgent inbox message to agent`);
|
|
7199
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
7200
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
7201
|
+
sessionService.sendKeepAlive(true);
|
|
7202
|
+
agentBackend.sendPrompt(sessionId, formatted).catch((err) => {
|
|
7203
|
+
logger.error(`[${agentName} Session ${sessionId}] Error delivering urgent inbox message: ${err.message}`);
|
|
7204
|
+
if (!acpStopped) {
|
|
7205
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7206
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
7207
|
+
sessionService.sendSessionEnd();
|
|
7208
|
+
}
|
|
7209
|
+
});
|
|
7210
|
+
}
|
|
7211
|
+
},
|
|
7002
7212
|
onMetadataUpdate: (newMeta) => {
|
|
7003
7213
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
7004
7214
|
sessionMetadata = {
|
|
@@ -7038,10 +7248,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7038
7248
|
onUpdateConfig: (patch) => {
|
|
7039
7249
|
writeSvampConfigPatchAcp(patch);
|
|
7040
7250
|
},
|
|
7041
|
-
onBash: async (command, cwd) => {
|
|
7251
|
+
onBash: async (command, cwd, timeout) => {
|
|
7252
|
+
const execTimeout = timeout || 12e4;
|
|
7042
7253
|
const { exec } = await import('child_process');
|
|
7043
7254
|
return new Promise((resolve2) => {
|
|
7044
|
-
exec(command, { cwd: cwd || directory, timeout:
|
|
7255
|
+
exec(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
7045
7256
|
if (err) {
|
|
7046
7257
|
resolve2({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
|
|
7047
7258
|
} else {
|
|
@@ -7492,7 +7703,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7492
7703
|
}
|
|
7493
7704
|
return ids;
|
|
7494
7705
|
},
|
|
7495
|
-
supervisor
|
|
7706
|
+
supervisor,
|
|
7707
|
+
tunnels
|
|
7496
7708
|
}
|
|
7497
7709
|
);
|
|
7498
7710
|
logger.log(`Machine service registered: svamp-machine-${machineId}`);
|
|
@@ -7691,7 +7903,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7691
7903
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
7692
7904
|
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
7693
7905
|
const PING_TIMEOUT_MS = 6e4;
|
|
7694
|
-
const MAX_FAILURES = 60;
|
|
7695
7906
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
7696
7907
|
let heartbeatRunning = false;
|
|
7697
7908
|
let lastReconnectAt = 0;
|
|
@@ -7762,7 +7973,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7762
7973
|
if (consecutiveHeartbeatFailures === 1) {
|
|
7763
7974
|
logger.log(`Ping failed: ${err.message}`);
|
|
7764
7975
|
} else if (consecutiveHeartbeatFailures % 6 === 0) {
|
|
7765
|
-
logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures}
|
|
7976
|
+
logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures} failures, retrying indefinitely)`);
|
|
7766
7977
|
}
|
|
7767
7978
|
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
|
|
7768
7979
|
const conn = server.rpc?._connection;
|
|
@@ -7775,17 +7986,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7775
7986
|
}
|
|
7776
7987
|
}
|
|
7777
7988
|
if (conn?._reader) {
|
|
7778
|
-
logger.log("
|
|
7989
|
+
logger.log("Cancelling stale HTTP stream reader to trigger reconnection");
|
|
7779
7990
|
try {
|
|
7780
7991
|
conn._reader.cancel?.("Stale connection");
|
|
7781
7992
|
} catch {
|
|
7782
7993
|
}
|
|
7994
|
+
} else if (conn?._abort_controller) {
|
|
7995
|
+
logger.log("Aborting stale HTTP stream to trigger reconnection");
|
|
7996
|
+
try {
|
|
7997
|
+
conn._abort_controller.abort();
|
|
7998
|
+
} catch {
|
|
7999
|
+
}
|
|
7783
8000
|
}
|
|
7784
8001
|
}
|
|
7785
|
-
if (consecutiveHeartbeatFailures >= MAX_FAILURES) {
|
|
7786
|
-
logger.log(`Heartbeat failed ${MAX_FAILURES} times. Shutting down.`);
|
|
7787
|
-
requestShutdown("heartbeat-timeout", err.message);
|
|
7788
|
-
}
|
|
7789
8002
|
}
|
|
7790
8003
|
}
|
|
7791
8004
|
} finally {
|
|
@@ -7870,6 +8083,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7870
8083
|
}
|
|
7871
8084
|
await supervisor.stopAll().catch(() => {
|
|
7872
8085
|
});
|
|
8086
|
+
for (const [name, client] of tunnels) {
|
|
8087
|
+
client.destroy();
|
|
8088
|
+
logger.log(`Tunnel '${name}' destroyed`);
|
|
8089
|
+
}
|
|
8090
|
+
tunnels.clear();
|
|
7873
8091
|
artifactSync.destroy();
|
|
7874
8092
|
try {
|
|
7875
8093
|
await server.disconnect();
|
|
@@ -7880,8 +8098,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7880
8098
|
};
|
|
7881
8099
|
const shutdownReq = await resolvesWhenShutdownRequested;
|
|
7882
8100
|
await cleanup(shutdownReq.source);
|
|
7883
|
-
if (process.env.SVAMP_SUPERVISED === "1"
|
|
7884
|
-
|
|
8101
|
+
if (process.env.SVAMP_SUPERVISED === "1") {
|
|
8102
|
+
const intentionalSources = ["os-signal", "os-signal-cleanup", "hypha-app"];
|
|
8103
|
+
if (!intentionalSources.includes(shutdownReq.source)) {
|
|
8104
|
+
process.exit(1);
|
|
8105
|
+
}
|
|
7885
8106
|
}
|
|
7886
8107
|
process.exit(0);
|
|
7887
8108
|
} catch (error) {
|
|
@@ -7897,33 +8118,97 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7897
8118
|
}
|
|
7898
8119
|
}
|
|
7899
8120
|
async function stopDaemon(options) {
|
|
8121
|
+
const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
|
|
8122
|
+
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
8123
|
+
const pidsToSignal = [];
|
|
8124
|
+
const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
|
|
8125
|
+
try {
|
|
8126
|
+
if (existsSync$1(supervisorPidFile)) {
|
|
8127
|
+
const supervisorPid = parseInt(readFileSync$1(supervisorPidFile, "utf-8").trim(), 10);
|
|
8128
|
+
if (supervisorPid && !isNaN(supervisorPid)) {
|
|
8129
|
+
try {
|
|
8130
|
+
process.kill(supervisorPid, 0);
|
|
8131
|
+
pidsToSignal.push(supervisorPid);
|
|
8132
|
+
} catch {
|
|
8133
|
+
}
|
|
8134
|
+
}
|
|
8135
|
+
}
|
|
8136
|
+
} catch {
|
|
8137
|
+
}
|
|
7900
8138
|
const state = readDaemonStateFile();
|
|
7901
|
-
if (
|
|
8139
|
+
if (state) {
|
|
8140
|
+
try {
|
|
8141
|
+
process.kill(state.pid, 0);
|
|
8142
|
+
if (!pidsToSignal.includes(state.pid)) {
|
|
8143
|
+
pidsToSignal.push(state.pid);
|
|
8144
|
+
}
|
|
8145
|
+
} catch {
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
if (pidsToSignal.length === 0) {
|
|
8149
|
+
try {
|
|
8150
|
+
const { execSync } = await import('child_process');
|
|
8151
|
+
const pgrepOutput = execSync(
|
|
8152
|
+
'pgrep -f "svamp daemon start-(supervised|sync)" 2>/dev/null || true',
|
|
8153
|
+
{ encoding: "utf-8" }
|
|
8154
|
+
).trim();
|
|
8155
|
+
if (pgrepOutput) {
|
|
8156
|
+
for (const line of pgrepOutput.split("\n")) {
|
|
8157
|
+
const pid = parseInt(line.trim(), 10);
|
|
8158
|
+
if (pid && !isNaN(pid) && pid !== process.pid) {
|
|
8159
|
+
pidsToSignal.push(pid);
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
} catch {
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
if (pidsToSignal.length === 0) {
|
|
7902
8167
|
console.log("No daemon running");
|
|
8168
|
+
cleanupDaemonStateFile();
|
|
7903
8169
|
return;
|
|
7904
8170
|
}
|
|
7905
|
-
const
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
8171
|
+
for (const pid of pidsToSignal) {
|
|
8172
|
+
try {
|
|
8173
|
+
process.kill(pid, signal);
|
|
8174
|
+
console.log(`Sent ${signal} to PID ${pid} \u2014 ${mode}`);
|
|
8175
|
+
} catch {
|
|
8176
|
+
console.log(`PID ${pid} already gone`);
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
pidsToSignal[0];
|
|
8180
|
+
for (let i = 0; i < 100; i++) {
|
|
8181
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
8182
|
+
const anyAlive = pidsToSignal.some((pid) => {
|
|
7913
8183
|
try {
|
|
7914
|
-
process.kill(
|
|
8184
|
+
process.kill(pid, 0);
|
|
8185
|
+
return true;
|
|
8186
|
+
} catch {
|
|
8187
|
+
return false;
|
|
8188
|
+
}
|
|
8189
|
+
});
|
|
8190
|
+
if (!anyAlive) {
|
|
8191
|
+
console.log("Daemon stopped");
|
|
8192
|
+
cleanupDaemonStateFile();
|
|
8193
|
+
try {
|
|
8194
|
+
if (existsSync$1(supervisorPidFile)) await import('fs').then((fs2) => fs2.promises.unlink(supervisorPidFile));
|
|
7915
8195
|
} catch {
|
|
7916
|
-
console.log("Daemon stopped");
|
|
7917
|
-
cleanupDaemonStateFile();
|
|
7918
|
-
return;
|
|
7919
8196
|
}
|
|
8197
|
+
return;
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
console.log("Daemon did not stop in time, sending SIGKILL");
|
|
8201
|
+
for (const pid of pidsToSignal) {
|
|
8202
|
+
try {
|
|
8203
|
+
process.kill(pid, "SIGKILL");
|
|
8204
|
+
} catch {
|
|
7920
8205
|
}
|
|
7921
|
-
console.log("Daemon did not stop in time, sending SIGKILL");
|
|
7922
|
-
process.kill(state.pid, "SIGKILL");
|
|
7923
|
-
} catch {
|
|
7924
|
-
console.log("Daemon is not running (stale state file)");
|
|
7925
8206
|
}
|
|
7926
8207
|
cleanupDaemonStateFile();
|
|
8208
|
+
try {
|
|
8209
|
+
if (existsSync$1(supervisorPidFile)) await import('fs').then((fs2) => fs2.promises.unlink(supervisorPidFile));
|
|
8210
|
+
} catch {
|
|
8211
|
+
}
|
|
7927
8212
|
}
|
|
7928
8213
|
function daemonStatus() {
|
|
7929
8214
|
const state = readDaemonStateFile();
|