svamp-cli 0.1.74 → 0.1.75

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.
@@ -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,107 @@ 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
+ listeners: [],
719
+ cols,
720
+ rows,
721
+ createdAt: Date.now(),
722
+ cwd
723
+ };
724
+ terminalSessions.set(sessionId, session);
725
+ ptyProcess.onData((data) => {
726
+ const filtered = filterForXterm(data);
727
+ if (!filtered) return;
728
+ const update = { type: "output", content: filtered, sessionId };
729
+ for (const listener of session.listeners) {
730
+ try {
731
+ listener(update);
732
+ } catch {
733
+ }
734
+ }
735
+ });
736
+ ptyProcess.onExit(({ exitCode, signal }) => {
737
+ const update = { type: "exit", content: "", sessionId, exitCode, signal };
738
+ for (const listener of session.listeners) {
739
+ try {
740
+ listener(update);
741
+ } catch {
742
+ }
743
+ }
744
+ terminalSessions.delete(sessionId);
745
+ });
746
+ return { sessionId, cols, rows };
747
+ },
748
+ /** Write data (keystrokes) to a terminal session. */
749
+ terminalWrite: async (params, context) => {
750
+ authorizeRequest(context, currentMetadata.sharing, "admin");
751
+ const session = terminalSessions.get(params.sessionId);
752
+ if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
753
+ session.pty.write(params.data);
754
+ return { success: true };
755
+ },
756
+ /** Resize a terminal session. */
757
+ terminalResize: async (params, context) => {
758
+ authorizeRequest(context, currentMetadata.sharing, "admin");
759
+ const session = terminalSessions.get(params.sessionId);
760
+ if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
761
+ session.pty.resize(params.cols, params.rows);
762
+ session.cols = params.cols;
763
+ session.rows = params.rows;
764
+ return { success: true };
765
+ },
766
+ /**
767
+ * Attach to a terminal session for real-time output streaming.
768
+ * The onData callback receives { type: 'output'|'exit', content, sessionId }.
769
+ */
770
+ terminalAttach: async (params, context) => {
771
+ authorizeRequest(context, currentMetadata.sharing, "admin");
772
+ const session = terminalSessions.get(params.sessionId);
773
+ if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
774
+ if (typeof params.onData !== "function") throw new Error("onData callback is required");
775
+ session.listeners.push(params.onData);
776
+ return { success: true, cols: session.cols, rows: session.rows };
777
+ },
778
+ /** Stop (kill) a terminal session. */
779
+ terminalStop: 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
+ session.pty.kill();
784
+ terminalSessions.delete(params.sessionId);
785
+ return { success: true };
786
+ },
787
+ /** List active terminal sessions. */
788
+ terminalList: async (context) => {
789
+ authorizeRequest(context, currentMetadata.sharing, "view");
790
+ return Array.from(terminalSessions.values()).map((s) => ({
791
+ sessionId: s.id,
792
+ cols: s.cols,
793
+ rows: s.rows,
794
+ cwd: s.cwd,
795
+ createdAt: s.createdAt
796
+ }));
797
+ },
670
798
  // Machine-level directory listing (read-only, view role)
671
799
  listDirectory: async (path, options, context) => {
672
800
  authorizeRequest(context, currentMetadata.sharing, "view");
@@ -871,27 +999,6 @@ async function registerMachineService(server, machineId, metadata, daemonState,
871
999
  };
872
1000
  }
873
1001
 
874
- function isStructuredMessage(msg) {
875
- return !!(msg.from && msg.subject);
876
- }
877
- function formatInboxMessageXml(msg) {
878
- 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}"`);
891
- return `<svamp-message ${attrs.join(" ")}>
892
- ${msg.body}
893
- </svamp-message>`;
894
- }
895
1002
  function loadMessages(messagesDir, sessionId) {
896
1003
  const filePath = join$1(messagesDir, "messages.jsonl");
897
1004
  if (!existsSync(filePath)) return [];
@@ -1268,24 +1375,6 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1268
1375
  });
1269
1376
  return await callbacks.onUpdateSecurityContext(newSecurityContext);
1270
1377
  },
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
1378
  /** Apply a new system prompt and restart the agent process */
1290
1379
  applySystemPrompt: async (prompt, context) => {
1291
1380
  authorizeRequest(context, metadata.sharing, "admin");
@@ -1294,86 +1383,6 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1294
1383
  }
1295
1384
  return await callbacks.onApplySystemPrompt(prompt);
1296
1385
  },
1297
- // ── Inbox Messaging ──
1298
- sendInboxMessage: async (message, context) => {
1299
- 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
- });
1320
- callbacks.onInboxMessage?.(message);
1321
- return { messageId: message.messageId, delivered: true };
1322
- },
1323
- getInbox: async (opts, context) => {
1324
- 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 };
1333
- },
1334
- markInboxRead: async (messageIds, context) => {
1335
- 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 };
1354
- },
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 };
1363
- } 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
- });
1374
- }
1375
- return { removed };
1376
- },
1377
1386
  // ── Listener Registration ──
1378
1387
  registerListener: async (callback, context) => {
1379
1388
  authorizeRequest(context, metadata.sharing, "view");
@@ -4971,16 +4980,15 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
4971
4980
  const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
4972
4981
  const prompt = buildRalphPrompt(state.task, state);
4973
4982
  const ralphSysPrompt = buildRalphSystemPrompt(state, progressRelPath);
4974
- const existingInbox = getMetadata().inbox || [];
4983
+ const existingQueue = getMetadata().messageQueue || [];
4975
4984
  setMetadata((m) => ({
4976
4985
  ...m,
4977
4986
  ralphLoop,
4978
- inbox: [...existingInbox, {
4979
- messageId: randomUUID$1(),
4980
- body: prompt,
4987
+ messageQueue: [...existingQueue, {
4988
+ id: randomUUID$1(),
4989
+ text: prompt,
4981
4990
  displayText: state.task,
4982
- timestamp: Date.now(),
4983
- read: false,
4991
+ createdAt: Date.now(),
4984
4992
  ralphSystemPrompt: ralphSysPrompt
4985
4993
  }]
4986
4994
  }));
@@ -5648,8 +5656,7 @@ async function startDaemon(options) {
5648
5656
  }
5649
5657
  });
5650
5658
  }, buildIsolationConfig2 = function(dir) {
5651
- const forceIsolation = sessionMetadata.forceIsolation ?? options2.forceIsolation;
5652
- if (!forceIsolation && !sessionMetadata.sharing?.enabled) return null;
5659
+ if (!options2.forceIsolation && !sessionMetadata.sharing?.enabled) return null;
5653
5660
  const method = isolationCapabilities.preferred;
5654
5661
  if (!method) return null;
5655
5662
  const detail = isolationCapabilities.details[method];
@@ -5690,7 +5697,6 @@ async function startDaemon(options) {
5690
5697
  lifecycleState: resumeSessionId ? "idle" : "starting",
5691
5698
  sharing: options2.sharing,
5692
5699
  securityContext: options2.securityContext,
5693
- forceIsolation: options2.forceIsolation || void 0,
5694
5700
  tags: options2.tags,
5695
5701
  parentSessionId: options2.parentSessionId,
5696
5702
  ...options2.parentSessionId && (() => {
@@ -5766,8 +5772,7 @@ async function startDaemon(options) {
5766
5772
  startupNonJsonLines = [];
5767
5773
  startupRetryMessage = initialMessage;
5768
5774
  let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
5769
- const forceIsolation = sessionMetadata.forceIsolation ?? options2.forceIsolation;
5770
- if (forceIsolation || sessionMetadata.sharing?.enabled) {
5775
+ if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
5771
5776
  rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
5772
5777
  }
5773
5778
  if (sessionMetadata.ralphLoop?.active) {
@@ -5804,7 +5809,7 @@ async function startDaemon(options) {
5804
5809
  if (wrapped.cleanupFiles) isolationCleanupFiles = wrapped.cleanupFiles;
5805
5810
  sessionMetadata = { ...sessionMetadata, isolationMethod: isoConfig.method };
5806
5811
  logger.log(`[Session ${sessionId}] Isolation: ${isoConfig.method} (binary: ${isoConfig.binaryPath})`);
5807
- } else if (forceIsolation || sessionMetadata.sharing?.enabled) {
5812
+ } else if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
5808
5813
  logger.log(`[Session ${sessionId}] WARNING: No isolation runtime (nono/docker/podman) available. Session is NOT sandboxed.`);
5809
5814
  sessionMetadata = { ...sessionMetadata, isolationMethod: void 0 };
5810
5815
  } else {
@@ -6029,7 +6034,7 @@ async function startDaemon(options) {
6029
6034
  logger.log(`[Session ${sessionId}] ${taskInfo}`);
6030
6035
  sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
6031
6036
  }
6032
- const queueLen = sessionMetadata.inbox?.filter((m) => !m.read)?.length ?? 0;
6037
+ const queueLen = sessionMetadata.messageQueue?.length ?? 0;
6033
6038
  if (msg.is_error) {
6034
6039
  const rlStateForError = readRalphState(getRalphStateFilePath(directory, sessionId));
6035
6040
  if (rlStateForError) {
@@ -6334,7 +6339,7 @@ The automated loop has finished. Review the progress above and let me know if yo
6334
6339
  return;
6335
6340
  }
6336
6341
  }
6337
- const queueLen = sessionMetadata.inbox?.filter((m) => !m.read)?.length ?? 0;
6342
+ const queueLen = sessionMetadata.messageQueue?.length ?? 0;
6338
6343
  if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
6339
6344
  signalProcessing(false);
6340
6345
  setTimeout(() => processMessageQueueRef?.(), 200);
@@ -6445,10 +6450,10 @@ The automated loop has finished. Review the progress above and let me know if yo
6445
6450
  }
6446
6451
  if (isKillingClaude || isRestartingClaude || isSwitchingMode) {
6447
6452
  logger.log(`[Session ${sessionId}] Message received while restarting Claude, queuing to prevent loss`);
6448
- const existingInbox = sessionMetadata.inbox || [];
6453
+ const existingQueue = sessionMetadata.messageQueue || [];
6449
6454
  sessionMetadata = {
6450
6455
  ...sessionMetadata,
6451
- inbox: [...existingInbox, { messageId: randomUUID$1(), body: text, timestamp: Date.now(), read: false }]
6456
+ messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
6452
6457
  };
6453
6458
  sessionService.updateMetadata(sessionMetadata);
6454
6459
  signalProcessing(false);
@@ -6483,7 +6488,7 @@ The automated loop has finished. Review the progress above and let me know if yo
6483
6488
  }
6484
6489
  signalProcessing(false);
6485
6490
  sessionWasProcessing = false;
6486
- const queueLen = sessionMetadata.inbox?.filter((m) => !m.read)?.length ?? 0;
6491
+ const queueLen = sessionMetadata.messageQueue?.length ?? 0;
6487
6492
  const abortMsg = queueLen > 0 ? `Aborted by user. ${queueLen} queued message(s) will be processed next.` : "Aborted by user";
6488
6493
  sessionService.pushMessage(
6489
6494
  { type: "message", message: abortMsg },
@@ -6565,24 +6570,6 @@ The automated loop has finished. Review the progress above and let me know if yo
6565
6570
  sessionMetadata = { ...sessionMetadata, securityContext: newSecurityContext };
6566
6571
  return await restartClaudeHandler();
6567
6572
  },
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
6573
  onSharingUpdate: (newSharing) => {
6587
6574
  logger.log(`[Session ${sessionId}] Sharing config updated \u2014 persisting to disk`);
6588
6575
  sessionMetadata = { ...sessionMetadata, sharing: newSharing };
@@ -6605,72 +6592,14 @@ The automated loop has finished. Review the progress above and let me know if yo
6605
6592
  lastSpawnMeta = { ...lastSpawnMeta, appendSystemPrompt: prompt };
6606
6593
  return await restartClaudeHandler();
6607
6594
  },
6608
- onInboxMessage: (message) => {
6609
- const xmlText = formatInboxMessageXml(message);
6610
- 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);
6617
- } 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);
6648
- }
6649
- }
6650
- },
6651
6595
  onKillSession: () => {
6652
6596
  logger.log(`[Session ${sessionId}] Kill session requested`);
6653
6597
  stopSession(sessionId);
6654
6598
  },
6655
6599
  onMetadataUpdate: (newMeta) => {
6656
6600
  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
6601
  sessionMetadata = {
6670
6602
  ...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
6603
  // Daemon drives lifecycleState — don't let frontend overwrite with stale value
6675
6604
  lifecycleState: sessionMetadata.lifecycleState,
6676
6605
  // Preserve claudeSessionId set by 'system init' (frontend may not have it)
@@ -6686,11 +6615,8 @@ The automated loop has finished. Review the progress above and let me know if yo
6686
6615
  if (prevRalphLoop && !newMeta.ralphLoop) {
6687
6616
  sessionService.updateMetadata(sessionMetadata);
6688
6617
  }
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) {
6618
+ const queue = newMeta.messageQueue;
6619
+ if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
6694
6620
  setTimeout(() => {
6695
6621
  processMessageQueueRef?.();
6696
6622
  }, 200);
@@ -6813,18 +6739,16 @@ The automated loop has finished. Review the progress above and let me know if yo
6813
6739
  if (trackedSession?.stopped) return;
6814
6740
  if (isKillingClaude) return;
6815
6741
  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 };
6742
+ const queue = sessionMetadata.messageQueue;
6743
+ if (queue && queue.length > 0) {
6744
+ const next = queue[0];
6745
+ const remaining = queue.slice(1);
6746
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0 };
6822
6747
  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)}..."`);
6748
+ logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
6825
6749
  sessionWasProcessing = true;
6826
6750
  signalProcessing(true);
6827
- sessionService.pushMessage(next.displayText || next.body, "user");
6751
+ sessionService.pushMessage(next.displayText || next.text, "user");
6828
6752
  userMessagePending = true;
6829
6753
  turnInitiatedByUser = true;
6830
6754
  const queueMeta = next.ralphSystemPrompt ? { appendSystemPrompt: next.ralphSystemPrompt } : void 0;
@@ -6846,7 +6770,7 @@ The automated loop has finished. Review the progress above and let me know if yo
6846
6770
  if (trackedSession?.stopped) return;
6847
6771
  if (isRestartingClaude || isSwitchingMode) return;
6848
6772
  claudeResumeId = void 0;
6849
- spawnClaude(deliverText, queueMeta);
6773
+ spawnClaude(next.text, queueMeta);
6850
6774
  } catch (err) {
6851
6775
  logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
6852
6776
  isKillingClaude = false;
@@ -6857,11 +6781,11 @@ The automated loop has finished. Review the progress above and let me know if yo
6857
6781
  } else {
6858
6782
  try {
6859
6783
  if (!claudeProcess || claudeProcess.exitCode !== null) {
6860
- spawnClaude(deliverText, queueMeta);
6784
+ spawnClaude(next.text, queueMeta);
6861
6785
  } else {
6862
6786
  const stdinMsg = JSON.stringify({
6863
6787
  type: "user",
6864
- message: { role: "user", content: deliverText }
6788
+ message: { role: "user", content: next.text }
6865
6789
  });
6866
6790
  claudeProcess.stdin?.write(stdinMsg + "\n");
6867
6791
  }
@@ -6994,20 +6918,20 @@ The automated loop has finished. Review the progress above and let me know if yo
6994
6918
  }
6995
6919
  if (!acpBackendReady) {
6996
6920
  logger.log(`[${agentName} Session ${sessionId}] Backend not ready \u2014 queuing message`);
6997
- const existingInbox = sessionMetadata.inbox || [];
6921
+ const existingQueue = sessionMetadata.messageQueue || [];
6998
6922
  sessionMetadata = {
6999
6923
  ...sessionMetadata,
7000
- inbox: [...existingInbox, { messageId: randomUUID$1(), body: text, timestamp: Date.now(), read: false }]
6924
+ messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
7001
6925
  };
7002
6926
  sessionService.updateMetadata(sessionMetadata);
7003
6927
  return;
7004
6928
  }
7005
6929
  if (sessionMetadata.lifecycleState === "running") {
7006
6930
  logger.log(`[${agentName} Session ${sessionId}] Agent busy \u2014 queuing message`);
7007
- const existingInbox = sessionMetadata.inbox || [];
6931
+ const existingQueue = sessionMetadata.messageQueue || [];
7008
6932
  sessionMetadata = {
7009
6933
  ...sessionMetadata,
7010
- inbox: [...existingInbox, { messageId: randomUUID$1(), body: text, timestamp: Date.now(), read: false }]
6934
+ messageQueue: [...existingQueue, { id: randomUUID$1(), text, createdAt: Date.now() }]
7011
6935
  };
7012
6936
  sessionService.updateMetadata(sessionMetadata);
7013
6937
  return;
@@ -7064,10 +6988,6 @@ The automated loop has finished. Review the progress above and let me know if yo
7064
6988
  sessionMetadata = { ...sessionMetadata, securityContext: newSecurityContext };
7065
6989
  return { success: false, message: "Security context updates with restart are not yet supported for this agent type." };
7066
6990
  },
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
6991
  onSharingUpdate: (newSharing) => {
7072
6992
  logger.log(`[${agentName} Session ${sessionId}] Sharing config updated \u2014 persisting in-memory`);
7073
6993
  sessionMetadata = { ...sessionMetadata, sharing: newSharing };
@@ -7075,59 +6995,14 @@ The automated loop has finished. Review the progress above and let me know if yo
7075
6995
  onApplySystemPrompt: async () => {
7076
6996
  return { success: false, message: "System prompt updates with restart are not yet supported for this agent type." };
7077
6997
  },
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
6998
  onKillSession: () => {
7109
6999
  logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
7110
7000
  stopSession(sessionId);
7111
7001
  },
7112
7002
  onMetadataUpdate: (newMeta) => {
7113
7003
  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
7004
  sessionMetadata = {
7127
7005
  ...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
7006
  // Daemon drives lifecycleState — don't let frontend overwrite with stale value
7132
7007
  lifecycleState: sessionMetadata.lifecycleState,
7133
7008
  ...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
@@ -7141,24 +7016,17 @@ The automated loop has finished. Review the progress above and let me know if yo
7141
7016
  if (prevRalphLoop && !newMeta.ralphLoop) {
7142
7017
  sessionService.updateMetadata(sessionMetadata);
7143
7018
  }
7144
- if (legacyQueue && legacyQueue.length > 0) {
7145
- sessionService.updateMetadata(sessionMetadata);
7146
- }
7147
7019
  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
- };
7020
+ const queue = newMeta.messageQueue;
7021
+ if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
7022
+ const next = queue[0];
7023
+ const remaining = queue.slice(1);
7024
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
7156
7025
  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)}..."`);
7026
+ logger.log(`[Session ${sessionId}] Processing queued message from metadata update: "${next.text.slice(0, 50)}..."`);
7159
7027
  sessionService.sendKeepAlive(true);
7160
- agentBackend.sendPrompt(sessionId, deliverText).catch((err) => {
7161
- logger.error(`[Session ${sessionId}] Error processing inbox message: ${err.message}`);
7028
+ agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
7029
+ logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
7162
7030
  if (!acpStopped) {
7163
7031
  sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
7164
7032
  sessionService.updateMetadata(sessionMetadata);
@@ -7275,20 +7143,16 @@ The automated loop has finished. Review the progress above and let me know if yo
7275
7143
  () => {
7276
7144
  if (acpStopped) return;
7277
7145
  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
- };
7146
+ const queue = sessionMetadata.messageQueue;
7147
+ if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
7148
+ const next = queue[0];
7149
+ const remaining = queue.slice(1);
7150
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
7286
7151
  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)}..."`);
7152
+ logger.log(`[Session ${sessionId}] Processing queued message (ACP ralph activation): "${next.text.slice(0, 50)}..."`);
7289
7153
  sessionService.sendKeepAlive(true);
7290
- agentBackend.sendPrompt(sessionId, deliverText).catch((err) => {
7291
- logger.error(`[Session ${sessionId}] Error processing inbox message: ${err.message}`);
7154
+ agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
7155
+ logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
7292
7156
  if (!acpStopped) {
7293
7157
  sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
7294
7158
  sessionService.updateMetadata(sessionMetadata);
@@ -7302,7 +7166,7 @@ The automated loop has finished. Review the progress above and let me know if yo
7302
7166
  const writeSvampConfigPatchAcp = svampConfigChecker.writeConfig;
7303
7167
  const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
7304
7168
  let agentIsoConfig;
7305
- if (((sessionMetadata.forceIsolation ?? options2.forceIsolation) || sessionMetadata.sharing?.enabled) && isolationCapabilities.preferred) {
7169
+ if ((options2.forceIsolation || sessionMetadata.sharing?.enabled) && isolationCapabilities.preferred) {
7306
7170
  const method = isolationCapabilities.preferred;
7307
7171
  const detail = isolationCapabilities.details[method];
7308
7172
  if (detail.found && detail.verified !== false) {
@@ -7370,21 +7234,17 @@ The automated loop has finished. Review the progress above and let me know if yo
7370
7234
  logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
7371
7235
  sessionService.pushMessage({ type: "message", message: reason }, "event");
7372
7236
  } 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
- };
7237
+ const pendingQueue = sessionMetadata.messageQueue;
7238
+ if (pendingQueue && pendingQueue.length > 0) {
7239
+ const next = pendingQueue[0];
7240
+ const remaining = pendingQueue.slice(1);
7241
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
7381
7242
  sessionService.updateMetadata(sessionMetadata);
7382
7243
  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}`);
7244
+ sessionService.pushMessage(next.displayText || next.text, "user");
7245
+ logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
7246
+ agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
7247
+ logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
7388
7248
  if (!acpStopped) {
7389
7249
  sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
7390
7250
  sessionService.updateMetadata(sessionMetadata);
@@ -7451,21 +7311,17 @@ The automated loop has finished. Review the progress above and let me know if yo
7451
7311
  return;
7452
7312
  }
7453
7313
  }
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
- };
7314
+ const queue = sessionMetadata.messageQueue;
7315
+ if (queue && queue.length > 0) {
7316
+ const next = queue[0];
7317
+ const remaining = queue.slice(1);
7318
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
7462
7319
  sessionService.updateMetadata(sessionMetadata);
7463
7320
  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}`);
7321
+ logger.log(`[Session ${sessionId}] Processing queued message: "${next.text.slice(0, 50)}..."`);
7322
+ sessionService.pushMessage(next.displayText || next.text, "user");
7323
+ agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
7324
+ logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
7469
7325
  if (!acpStopped) {
7470
7326
  sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
7471
7327
  sessionService.updateMetadata(sessionMetadata);
@@ -7676,7 +7532,7 @@ The automated loop has finished. Review the progress above and let me know if yo
7676
7532
  // Restore sharing & security context from persisted metadata
7677
7533
  sharing: persisted.metadata?.sharing,
7678
7534
  securityContext: persisted.metadata?.securityContext,
7679
- forceIsolation: persisted.metadata?.forceIsolation ?? !!persisted.metadata?.isolationMethod,
7535
+ forceIsolation: !!persisted.metadata?.isolationMethod,
7680
7536
  // Block queue processing until auto-continue completes
7681
7537
  wasProcessing: persisted.wasProcessing && !!persisted.claudeResumeId && !isOrphaned
7682
7538
  });