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.
@@ -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 path, { join, dirname, resolve, basename } from 'path';
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1025
+ }
877
1026
  function formatInboxMessageXml(msg) {
878
1027
  if (!isStructuredMessage(msg)) return msg.body;
879
- const attrs = [
880
- `message-id="${msg.messageId}"`,
881
- `from="${msg.from}"`,
882
- `from-session="${msg.fromSession || ""}"`,
883
- `to="${msg.to || ""}"`,
884
- `subject="${msg.subject}"`,
885
- `urgency="${msg.urgency || "normal"}"`,
886
- `timestamp="${msg.timestamp}"`
887
- ];
888
- if (msg.replyTo) attrs.push(`reply-to="${msg.replyTo}"`);
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 Messaging ──
1428
+ // ── Inbox ──
1298
1429
  sendInboxMessage: async (message, context) => {
1299
1430
  authorizeRequest(context, metadata.sharing, "interact");
1300
- if (!message.messageId || !message.body) {
1301
- throw new Error("Inbox message requires messageId and body");
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
- return { messageId: message.messageId, delivered: true };
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 inbox = metadata.inbox || [];
1326
- if (opts?.unreadOnly) {
1327
- inbox = inbox.filter((m) => !m.read);
1328
- }
1329
- if (opts?.limit && opts.limit > 0) {
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 (messageIds, context) => {
1445
+ markInboxRead: async (messageId, context) => {
1335
1446
  authorizeRequest(context, metadata.sharing, "interact");
1336
- let updated = 0;
1337
- const inbox = (metadata.inbox || []).map((m) => {
1338
- if (messageIds.includes(m.messageId) && !m.read) {
1339
- updated++;
1340
- return { ...m, read: true };
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 (messageIds, context) => {
1356
- authorizeRequest(context, metadata.sharing, "interact");
1357
- const currentInbox = metadata.inbox || [];
1358
- let removed;
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
- removed = currentInbox.length;
1365
- metadata = { ...metadata, inbox: void 0 };
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
- return { removed };
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(path.join(this.persistDir, file), "utf-8");
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 = path.join(this.persistDir, `${spec.id}.json`);
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(path.join(this.persistDir, `${id}.json`));
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 delayMs = spec.restartDelay * 1e3;
4493
- console.log(`[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (restart #${state.restartCount + 1})`);
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 existingInbox = getMetadata().inbox || [];
5075
+ const existingQueue = getMetadata().messageQueue || [];
4975
5076
  setMetadata((m) => ({
4976
5077
  ...m,
4977
5078
  ralphLoop,
4978
- inbox: [...existingInbox, {
4979
- messageId: randomUUID$1(),
4980
- body: prompt,
5079
+ messageQueue: [...existingQueue, {
5080
+ id: randomUUID$1(),
5081
+ text: prompt,
4981
5082
  displayText: state.task,
4982
- timestamp: Date.now(),
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
- const forceIsolation = sessionMetadata.forceIsolation ?? options2.forceIsolation;
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
- const forceIsolation = sessionMetadata.forceIsolation ?? options2.forceIsolation;
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.inbox?.filter((m) => !m.read)?.length ?? 0;
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.inbox?.filter((m) => !m.read)?.length ?? 0;
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 existingInbox = sessionMetadata.inbox || [];
6546
+ const existingQueue = sessionMetadata.messageQueue || [];
6449
6547
  sessionMetadata = {
6450
6548
  ...sessionMetadata,
6451
- inbox: [...existingInbox, { messageId: randomUUID$1(), body: text, timestamp: Date.now(), read: false }]
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.inbox?.filter((m) => !m.read)?.length ?? 0;
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
- const xmlText = formatInboxMessageXml(message);
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
- logger.log(`[Session ${sessionId}] Urgent inbox message from ${message.from}: ${message.subject}`);
6612
- const text = xmlText;
6613
- if (sessionWasProcessing) {
6614
- const existingInbox = sessionMetadata.inbox || [];
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
- sessionWasProcessing = true;
6619
- signalProcessing(true);
6620
- sessionService.pushMessage(`[Inbox] ${message.subject}`, "user");
6621
- userMessagePending = true;
6622
- turnInitiatedByUser = true;
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
- if (legacyQueue && legacyQueue.length > 0) {
6690
- sessionService.updateMetadata(sessionMetadata);
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 inbox = sessionMetadata.inbox;
6817
- const unreadIdx = inbox?.findIndex((m) => !m.read) ?? -1;
6818
- if (inbox && unreadIdx >= 0) {
6819
- const next = inbox[unreadIdx];
6820
- const updatedInbox = inbox.map((m, i) => i === unreadIdx ? { ...m, read: true } : m);
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
- const deliverText = isStructuredMessage(next) ? formatInboxMessageXml(next) : next.body;
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.body, "user");
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(deliverText, queueMeta);
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(deliverText, queueMeta);
6896
+ spawnClaude(next.text, queueMeta);
6861
6897
  } else {
6862
6898
  const stdinMsg = JSON.stringify({
6863
6899
  type: "user",
6864
- message: { role: "user", content: deliverText }
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 existingInbox = sessionMetadata.inbox || [];
7033
+ const existingQueue = sessionMetadata.messageQueue || [];
6998
7034
  sessionMetadata = {
6999
7035
  ...sessionMetadata,
7000
- inbox: [...existingInbox, { messageId: randomUUID$1(), body: text, timestamp: Date.now(), read: false }]
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 existingInbox = sessionMetadata.inbox || [];
7043
+ const existingQueue = sessionMetadata.messageQueue || [];
7008
7044
  sessionMetadata = {
7009
7045
  ...sessionMetadata,
7010
- inbox: [...existingInbox, { messageId: randomUUID$1(), body: text, timestamp: Date.now(), read: false }]
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 unreadInbox = sessionMetadata.inbox?.filter((m) => !m.read);
7149
- if (unreadInbox && unreadInbox.length > 0 && sessionMetadata.lifecycleState === "idle") {
7150
- const next = unreadInbox[0];
7151
- sessionMetadata = {
7152
- ...sessionMetadata,
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
- const deliverText = isStructuredMessage(next) ? formatInboxMessageXml(next) : next.body;
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, deliverText).catch((err) => {
7161
- logger.error(`[Session ${sessionId}] Error processing inbox message: ${err.message}`);
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 unreadInbox = sessionMetadata.inbox?.filter((m) => !m.read);
7279
- if (unreadInbox && unreadInbox.length > 0 && sessionMetadata.lifecycleState === "idle") {
7280
- const next = unreadInbox[0];
7281
- sessionMetadata = {
7282
- ...sessionMetadata,
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
- const deliverText = isStructuredMessage(next) ? formatInboxMessageXml(next) : next.body;
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, deliverText).catch((err) => {
7291
- logger.error(`[Session ${sessionId}] Error processing inbox message: ${err.message}`);
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 (((sessionMetadata.forceIsolation ?? options2.forceIsolation) || sessionMetadata.sharing?.enabled) && isolationCapabilities.preferred) {
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 pendingInbox = sessionMetadata.inbox?.filter((m) => !m.read);
7374
- if (pendingInbox && pendingInbox.length > 0) {
7375
- const next = pendingInbox[0];
7376
- sessionMetadata = {
7377
- ...sessionMetadata,
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
- const deliverText = isStructuredMessage(next) ? formatInboxMessageXml(next) : next.body;
7384
- sessionService.pushMessage(next.displayText || next.body, "user");
7385
- logger.log(`[${agentName} Session ${sessionId}] Processing inbox message (priority over Ralph advance): "${next.body.slice(0, 50)}..."`);
7386
- agentBackend.sendPrompt(sessionId, deliverText).catch((err) => {
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 unreadInboxItems = sessionMetadata.inbox?.filter((m) => !m.read);
7455
- if (unreadInboxItems && unreadInboxItems.length > 0) {
7456
- const next = unreadInboxItems[0];
7457
- sessionMetadata = {
7458
- ...sessionMetadata,
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
- const deliverText = isStructuredMessage(next) ? formatInboxMessageXml(next) : next.body;
7465
- logger.log(`[Session ${sessionId}] Processing inbox message: "${next.body.slice(0, 50)}..."`);
7466
- sessionService.pushMessage(next.displayText || next.body, "user");
7467
- agentBackend.sendPrompt(sessionId, deliverText).catch((err) => {
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: persisted.metadata?.forceIsolation ?? !!persisted.metadata?.isolationMethod,
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}/${MAX_FAILURES})`);
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("Aborting stale HTTP stream to trigger reconnection");
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" && shutdownReq.source === "version-update") {
8028
- process.exit(1);
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) {