svamp-cli 0.2.273 → 0.2.275

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.
Files changed (26) hide show
  1. package/dist/{agentCommands-BRY74Q0h.mjs → agentCommands-DGT-1wjo.mjs} +5 -5
  2. package/dist/{auth-cez4R4me.mjs → auth-CT55Rse4.mjs} +1 -1
  3. package/dist/{cli-C-19p_H4.mjs → cli-DYTmO9JL.mjs} +63 -63
  4. package/dist/cli.mjs +2 -2
  5. package/dist/{commands-eua1Gt4_.mjs → commands-BV_9mFkz.mjs} +2 -2
  6. package/dist/{commands-BAr_4xB1.mjs → commands-BvmFXAmu.mjs} +2 -2
  7. package/dist/{commands-CobhxPGy.mjs → commands-C0rRjhMD.mjs} +41 -18
  8. package/dist/{commands-BZokG8vu.mjs → commands-C8Zc5axO.mjs} +11 -9
  9. package/dist/{commands-2aatLLjq.mjs → commands-CVPcbtWV.mjs} +1 -1
  10. package/dist/{commands-C8ceYKji.mjs → commands-CXHZapLH.mjs} +1 -1
  11. package/dist/{commands-CZqQryOD.mjs → commands-D7fIWAgz.mjs} +4 -4
  12. package/dist/{fleet-CzrL5O69.mjs → fleet-BCel6ZVc.mjs} +2 -2
  13. package/dist/{frpc-CFKdshLL.mjs → frpc-D3faR6iR.mjs} +73 -32
  14. package/dist/{headlessCli-DG4HyG2F.mjs → headlessCli-DVBFm7og.mjs} +2 -2
  15. package/dist/index.mjs +1 -1
  16. package/dist/package-DIuZ7Wfp.mjs +64 -0
  17. package/dist/{pinnedClaudeCode-B9O-hKxm.mjs → pinnedClaudeCode-CgEtGkX2.mjs} +1 -1
  18. package/dist/{rpc-Bbnunkmx.mjs → rpc-BQ8zLSUc.mjs} +1 -1
  19. package/dist/{rpc-fy0bKbnH.mjs → rpc-xlV-4-BG.mjs} +1 -1
  20. package/dist/{run-fDelGoR-.mjs → run-C3Fcbk-H.mjs} +211 -41
  21. package/dist/{run-vr8PHjL1.mjs → run-DMPVho7i.mjs} +1 -1
  22. package/dist/{scheduler-C_3x4bEj.mjs → scheduler-DaFZY2ob.mjs} +1 -1
  23. package/dist/{serveCommands-DglJYzYC.mjs → serveCommands-B9Yhx1fl.mjs} +5 -5
  24. package/dist/{sideband-Mqkuef0c.mjs → sideband-CYqmMG_m.mjs} +1 -1
  25. package/package.json +2 -2
  26. package/dist/package-BU9leRlh.mjs +0 -64
@@ -1158,8 +1158,14 @@ async function readProcessTable() {
1158
1158
  let stdout;
1159
1159
  try {
1160
1160
  const result = await execFileAsync$1("ps", ["-A", "-o", "pid=,ppid=,etime=,%cpu=,rss=,command="], {
1161
- maxBuffer: 8 * 1024 * 1024
1161
+ maxBuffer: 8 * 1024 * 1024,
1162
1162
  // 8MB — plenty for any realistic process table
1163
+ // Hard wall-clock deadline: on a wedged host (D-state / stuck FS) `ps` can block
1164
+ // forever; maxBuffer bounds size, not time. Without this the awaited promise never
1165
+ // settles and callers (killDescendant admin RPC, Tasks-panel detectDescendants) hang.
1166
+ // On timeout execFile rejects → the catch below returns [] (best-effort, no crash).
1167
+ timeout: 5e3,
1168
+ killSignal: "SIGKILL"
1163
1169
  });
1164
1170
  stdout = result.stdout;
1165
1171
  } catch {
@@ -1390,9 +1396,15 @@ function serveStaticMount(req, res, opts) {
1390
1396
  return;
1391
1397
  }
1392
1398
  const indexPath = realContainedPath(rootDir, path.join(target, "index.html"));
1393
- if (indexPath && fs.statSync(indexPath).isFile()) {
1394
- sendFile(req, res, indexPath, fs.statSync(indexPath));
1395
- return;
1399
+ if (indexPath) {
1400
+ try {
1401
+ const ist = fs.statSync(indexPath);
1402
+ if (ist.isFile()) {
1403
+ sendFile(req, res, indexPath, ist);
1404
+ return;
1405
+ }
1406
+ } catch {
1407
+ }
1396
1408
  }
1397
1409
  if (!browse) {
1398
1410
  res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
@@ -1596,16 +1608,19 @@ function isStructurallyLiveJwt(token) {
1596
1608
  async function verifyTokenViaHypha(token, hyphaServerUrl) {
1597
1609
  try {
1598
1610
  const baseUrl = hyphaServerUrl.replace(/\/$/, "");
1599
- const url = `${baseUrl}/public/services/ws/get_user_info`;
1611
+ const url = `${baseUrl}/public/services/ws/parse_token`;
1600
1612
  const resp = await fetch(url, {
1601
- method: "GET",
1613
+ method: "POST",
1602
1614
  headers: {
1603
- Authorization: `Bearer ${token}`
1615
+ Authorization: `Bearer ${token}`,
1616
+ "Content-Type": "application/json"
1604
1617
  },
1618
+ body: JSON.stringify({ token }),
1605
1619
  signal: AbortSignal.timeout(1e4)
1606
1620
  });
1607
1621
  if (!resp.ok) return null;
1608
1622
  const data = await resp.json();
1623
+ if (data?.is_anonymous === true) return null;
1609
1624
  const email = data?.email;
1610
1625
  if (typeof email === "string" && email) return email.toLowerCase();
1611
1626
  return null;
@@ -2397,7 +2412,12 @@ class ServeManager {
2397
2412
  cwd: cfg.workdir ?? process.cwd(),
2398
2413
  env: { ...process.env, ...cfg.env ?? {} },
2399
2414
  stdio: ["ignore", "pipe", "pipe"],
2400
- detached: false
2415
+ // #0483: run in its OWN process group so we can kill the whole tree. Managed mounts
2416
+ // are typically wrappers (npm start / npm run dev / python -m …) whose real listener
2417
+ // is a GRANDCHILD; killing only the direct pid leaves the grandchild holding the port
2418
+ // → next warmup hits EADDRINUSE → mount permanently broken + orphan accumulation.
2419
+ // killManagedTree() below signals -pid (the group).
2420
+ detached: true
2401
2421
  });
2402
2422
  child.stdout?.on("data", (d) => {
2403
2423
  const line = d.toString().trimEnd();
@@ -2423,10 +2443,7 @@ class ServeManager {
2423
2443
  this.log(`Managed process '${name}' ready on 127.0.0.1:${cfg.port}`);
2424
2444
  }).catch((err) => {
2425
2445
  this.log(`Managed process '${name}' warmup failed: ${err.message}`);
2426
- try {
2427
- child.kill("SIGTERM");
2428
- } catch {
2429
- }
2446
+ this.killManagedTree(child, "SIGTERM");
2430
2447
  this.managedProcs.delete(name);
2431
2448
  throw err;
2432
2449
  });
@@ -2434,12 +2451,33 @@ class ServeManager {
2434
2451
  child,
2435
2452
  pid: child.pid ?? 0,
2436
2453
  lastRequestAt: Date.now(),
2454
+ activeConns: 0,
2437
2455
  warmupPromise
2438
2456
  };
2439
2457
  this.managedProcs.set(name, newHandle);
2440
2458
  this.log(`Managed process '${name}' starting: ${cfg.command} ${(cfg.args ?? []).join(" ")} (port ${cfg.port})`);
2441
2459
  return warmupPromise;
2442
2460
  }
2461
+ /**
2462
+ * #0483: kill the managed child's whole PROCESS GROUP (it was spawned detached, so it leads
2463
+ * its own group). This reaps grandchildren (the real listener behind an npm/python wrapper)
2464
+ * that a direct child.kill() would orphan, leaving the port bound. Falls back to a direct
2465
+ * child.kill() if the group signal fails (e.g. pid already gone).
2466
+ */
2467
+ killManagedTree(child, signal) {
2468
+ const pid = child.pid;
2469
+ if (pid && pid > 0) {
2470
+ try {
2471
+ process.kill(-pid, signal);
2472
+ return;
2473
+ } catch {
2474
+ }
2475
+ }
2476
+ try {
2477
+ child.kill(signal);
2478
+ } catch {
2479
+ }
2480
+ }
2443
2481
  /** Poll a URL until it returns <500 or the deadline passes. */
2444
2482
  async warmupProbe(url, timeoutMs) {
2445
2483
  const deadline = Date.now() + timeoutMs;
@@ -2472,17 +2510,11 @@ class ServeManager {
2472
2510
  const h = this.managedProcs.get(name);
2473
2511
  if (!h) return;
2474
2512
  this.managedProcs.delete(name);
2475
- try {
2476
- h.child.kill("SIGTERM");
2477
- } catch {
2478
- }
2479
2513
  const child = h.child;
2514
+ this.killManagedTree(child, "SIGTERM");
2480
2515
  await new Promise((resolve) => {
2481
2516
  const t = setTimeout(() => {
2482
- try {
2483
- child.kill("SIGKILL");
2484
- } catch {
2485
- }
2517
+ this.killManagedTree(child, "SIGKILL");
2486
2518
  resolve();
2487
2519
  }, 5e3);
2488
2520
  child.once("exit", () => {
@@ -2502,6 +2534,10 @@ class ServeManager {
2502
2534
  if (!cfg || !cfg.idleTimeoutSec || cfg.idleTimeoutSec <= 0) continue;
2503
2535
  const h = this.managedProcs.get(name);
2504
2536
  if (!h || h.warmupPromise) continue;
2537
+ if (h.activeConns > 0) {
2538
+ h.lastRequestAt = now;
2539
+ continue;
2540
+ }
2505
2541
  if (now - h.lastRequestAt >= cfg.idleTimeoutSec * 1e3) {
2506
2542
  this.log(`Idle eviction: stopping '${name}' (idle ${Math.round((now - h.lastRequestAt) / 1e3)}s \u2265 ${cfg.idleTimeoutSec}s)`);
2507
2543
  this.stopManagedProcess(name).catch(() => {
@@ -2532,7 +2568,9 @@ class ServeManager {
2532
2568
  ...this.port > 0 ? { port: this.port } : {}
2533
2569
  };
2534
2570
  try {
2535
- fs.writeFileSync(this.persistFile, JSON.stringify(state, null, 2));
2571
+ const tmp = `${this.persistFile}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
2572
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
2573
+ fs.renameSync(tmp, this.persistFile);
2536
2574
  } catch (err) {
2537
2575
  this.log(`Error persisting serve state: ${err.message}`);
2538
2576
  }
@@ -2703,7 +2741,18 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
2703
2741
  }
2704
2742
  }
2705
2743
  const handle = this.managedProcs.get(mount.name);
2706
- if (handle) handle.lastRequestAt = Date.now();
2744
+ if (handle) {
2745
+ handle.lastRequestAt = Date.now();
2746
+ handle.activeConns++;
2747
+ let released = false;
2748
+ const release = () => {
2749
+ if (released) return;
2750
+ released = true;
2751
+ handle.activeConns = Math.max(0, handle.activeConns - 1);
2752
+ handle.lastRequestAt = Date.now();
2753
+ };
2754
+ res.on("close", release);
2755
+ }
2707
2756
  const targetPath = mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || "");
2708
2757
  const proxyReq = http.request({
2709
2758
  hostname: "127.0.0.1",
@@ -2804,7 +2853,17 @@ Connection: close\r
2804
2853
  }
2805
2854
  }
2806
2855
  const handle = this.managedProcs.get(mount.name);
2807
- if (handle) handle.lastRequestAt = Date.now();
2856
+ if (handle) {
2857
+ handle.lastRequestAt = Date.now();
2858
+ handle.activeConns++;
2859
+ let released = false;
2860
+ clientSocket.on("close", () => {
2861
+ if (released) return;
2862
+ released = true;
2863
+ handle.activeConns = Math.max(0, handle.activeConns - 1);
2864
+ handle.lastRequestAt = Date.now();
2865
+ });
2866
+ }
2808
2867
  const targetPath = mountResolvedByHost ? req.url || "/" : (url.pathname.slice(`/${mountName}`.length) || "/") + (url.search || "");
2809
2868
  const upstream = net.connect(cfg.port, "127.0.0.1", () => {
2810
2869
  const lines = [`${req.method} ${targetPath} HTTP/1.1`];
@@ -2896,7 +2955,7 @@ Connection: close\r
2896
2955
  const mount = this.mounts.get(mountName);
2897
2956
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
2898
2957
  try {
2899
- const { FrpcTunnel } = await import('./frpc-CFKdshLL.mjs');
2958
+ const { FrpcTunnel } = await import('./frpc-D3faR6iR.mjs');
2900
2959
  let tunnel;
2901
2960
  tunnel = new FrpcTunnel({
2902
2961
  name: tunnelName,
@@ -4388,6 +4447,7 @@ class ChannelOutbox {
4388
4447
  }
4389
4448
  /** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
4390
4449
  append(channelId, r) {
4450
+ this.reload();
4391
4451
  const seq = (this.seqByChannel.get(channelId) || 0) + 1;
4392
4452
  this.seqByChannel.set(channelId, seq);
4393
4453
  const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
@@ -4406,6 +4466,7 @@ class ChannelOutbox {
4406
4466
  }
4407
4467
  if (++this.appendsSinceCompact >= COMPACT_EVERY) {
4408
4468
  this.appendsSinceCompact = 0;
4469
+ this.reload();
4409
4470
  this._compact();
4410
4471
  }
4411
4472
  this.emitter.emit(channelId, reply);
@@ -4463,6 +4524,7 @@ class ChannelOutbox {
4463
4524
  }
4464
4525
  /** Drop a channel's outbox (on channel delete) — best-effort rewrite of the log. */
4465
4526
  purge(channelId) {
4527
+ if (existsSync(this.file)) this.reload();
4466
4528
  this.byChannel.delete(channelId);
4467
4529
  this.seqByChannel.delete(channelId);
4468
4530
  if (!existsSync(this.file)) return;
@@ -5698,7 +5760,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
5698
5760
  const tunnels = handlers.tunnels;
5699
5761
  if (!tunnels) throw new Error("Tunnel management not available");
5700
5762
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
5701
- const { FrpcTunnel } = await import('./frpc-CFKdshLL.mjs');
5763
+ const { FrpcTunnel } = await import('./frpc-D3faR6iR.mjs');
5702
5764
  const tunnel = new FrpcTunnel({
5703
5765
  name: params.name,
5704
5766
  ports: params.ports,
@@ -6172,7 +6234,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6172
6234
  }
6173
6235
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
6174
6236
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
6175
- const { toolsForRole } = await import('./sideband-Mqkuef0c.mjs');
6237
+ const { toolsForRole } = await import('./sideband-CYqmMG_m.mjs');
6176
6238
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
6177
6239
  return fmt(r2);
6178
6240
  }
@@ -6271,7 +6333,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6271
6333
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
6272
6334
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
6273
6335
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
6274
- const { queryCore } = await import('./commands-CZqQryOD.mjs');
6336
+ const { queryCore } = await import('./commands-D7fIWAgz.mjs');
6275
6337
  const timeout = c.reply?.timeout_sec || 120;
6276
6338
  let result;
6277
6339
  try {
@@ -6366,7 +6428,9 @@ ${d?.error || "not found"}`;
6366
6428
  });
6367
6429
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
6368
6430
  const cursor = Math.max(0, Number(kwargs.cursor) || 0);
6369
- const waitMs = Math.min(Math.max(0, Number(kwargs.wait ?? 25) * 1e3), 6e4);
6431
+ const waitRaw = Number(kwargs.wait ?? 25);
6432
+ const waitSec = Number.isFinite(waitRaw) ? waitRaw : 25;
6433
+ const waitMs = Math.min(Math.max(0, waitSec * 1e3), 6e4);
6370
6434
  const outbox = new ChannelOutbox(dir);
6371
6435
  const deadline = Date.now() + waitMs;
6372
6436
  for (; ; ) {
@@ -6477,7 +6541,7 @@ function writeInboundContext(sessionId, ctx) {
6477
6541
  try {
6478
6542
  const p = ctxPath(sessionId);
6479
6543
  mkdirSync$1(join$1(SVAMP_HOME$2, "inbound"), { recursive: true });
6480
- const tmp = p + ".tmp";
6544
+ const tmp = `${p}.${process.pid}.${randomUUID()}.tmp`;
6481
6545
  writeFileSync$1(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
6482
6546
  renameSync(tmp, p);
6483
6547
  } catch {
@@ -6637,6 +6701,12 @@ function classifyInbound(message, now = Date.now(), opts) {
6637
6701
  const tripped = recordWake(now);
6638
6702
  return { action: "wake", reason: "urgent", breakerTripped: tripped };
6639
6703
  }
6704
+ function isHopCapParked(message, opts) {
6705
+ const hop = message.hopCount ?? 0;
6706
+ const human = senderIsHuman(message);
6707
+ const self = false;
6708
+ return !human && !self && !message.channel && hop > MAX_AGENT_HOPS;
6709
+ }
6640
6710
  function applyInboxClear(inbox, opts) {
6641
6711
  let kept;
6642
6712
  if (opts?.messageId) kept = inbox.filter((m) => m.messageId !== opts.messageId);
@@ -6670,6 +6740,9 @@ function saveInbox(projectDir, sessionId, inbox) {
6670
6740
  } catch {
6671
6741
  }
6672
6742
  }
6743
+ function isPending(m) {
6744
+ return !m.handled && !m.read;
6745
+ }
6673
6746
 
6674
6747
  function envInt(name, fallback) {
6675
6748
  const raw = process.env[name];
@@ -7654,10 +7727,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7654
7727
  },
7655
7728
  // Agent/owner answers a queued (async) channel message → channel outbox, addressed
7656
7729
  // to the original external caller. Routed here by `inbox reply` for channel-origin
7657
- // inbox messages (which carry channelId + correlationId + from). Owner-gated since
7658
- // it's the session speaking on its own behalf.
7730
+ // inbox messages (which carry channelId + correlationId + from). #0466: owner-gated
7731
+ // (admin tier — the highest shared role; owner + same-owner-workspace still pass
7732
+ // above) since this speaks AS the session to EXTERNAL parties, recorded verified:true.
7733
+ // A mere interact-tier collaborator must not be able to emit outbound replies as the
7734
+ // session — that would be attribution spoofing to third parties.
7659
7735
  channelReply: async (params, context) => {
7660
- authorizeRequest(context, metadata.sharing, "interact");
7736
+ authorizeRequest(context, metadata.sharing, "admin");
7661
7737
  const c = channelStore.get(params.channel);
7662
7738
  if (!c) return { ok: false, error: "channel not found" };
7663
7739
  if (!params.to || !params.body) return { ok: false, error: "to and body are required" };
@@ -15939,7 +16015,7 @@ async function startDaemon(options) {
15939
16015
  try {
15940
16016
  const dir = loadSessionIndex()[sessionId]?.directory;
15941
16017
  if (!dir) return;
15942
- const { reconcileServiceLinks } = await import('./agentCommands-BRY74Q0h.mjs');
16018
+ const { reconcileServiceLinks } = await import('./agentCommands-DGT-1wjo.mjs');
15943
16019
  const configPath = getSvampConfigPath(dir, sessionId);
15944
16020
  const config = readSvampConfig(configPath);
15945
16021
  const entries = Array.from(urls.entries());
@@ -15957,7 +16033,7 @@ async function startDaemon(options) {
15957
16033
  }
15958
16034
  }
15959
16035
  async function createExposedTunnel(spec) {
15960
- const { FrpcTunnel } = await import('./frpc-CFKdshLL.mjs');
16036
+ const { FrpcTunnel } = await import('./frpc-D3faR6iR.mjs');
15961
16037
  const tunnel = new FrpcTunnel({
15962
16038
  name: spec.name,
15963
16039
  ports: spec.ports,
@@ -15985,7 +16061,7 @@ async function startDaemon(options) {
15985
16061
  ensureAutoInstalledCommands(logger);
15986
16062
  (async () => {
15987
16063
  try {
15988
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-B9O-hKxm.mjs');
16064
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-CgEtGkX2.mjs');
15989
16065
  beginClaudeVersionReconcile((msg) => logger.log(msg));
15990
16066
  } catch (e) {
15991
16067
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -18147,11 +18223,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
18147
18223
  });
18148
18224
  },
18149
18225
  onIssue: async (params) => {
18150
- const { issueRpc } = await import('./rpc-Bbnunkmx.mjs');
18226
+ const { issueRpc } = await import('./rpc-BQ8zLSUc.mjs');
18151
18227
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
18152
18228
  },
18153
18229
  onWorkflow: async (params) => {
18154
- const { workflowRpc } = await import('./rpc-fy0bKbnH.mjs');
18230
+ const { workflowRpc } = await import('./rpc-xlV-4-BG.mjs');
18155
18231
  return workflowRpc(params?.cwd || directory, params || {});
18156
18232
  },
18157
18233
  onRipgrep: async (args, cwd) => {
@@ -18368,6 +18444,47 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
18368
18444
  sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
18369
18445
  sessionService.updateMetadata(sessionMetadata);
18370
18446
  logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
18447
+ const inboxReconcileMs = Number(process.env.SVAMP_INBOX_RECONCILE_MS);
18448
+ const INBOX_RECONCILE_INTERVAL_MS = Number.isFinite(inboxReconcileMs) && inboxReconcileMs >= 0 ? inboxReconcileMs : 15e3;
18449
+ let inboxReconcileTimer = null;
18450
+ const reconcileIdleInbox = () => {
18451
+ if (trackedSession.stopped) {
18452
+ if (inboxReconcileTimer) clearInterval(inboxReconcileTimer);
18453
+ return;
18454
+ }
18455
+ if (sessionWasProcessing) return;
18456
+ if (isKillingClaude || isRestartingClaude || isSwitchingMode) return;
18457
+ const queue = sessionMetadata.messageQueue;
18458
+ if (queue && queue.length > 0) {
18459
+ processMessageQueueRef?.();
18460
+ return;
18461
+ }
18462
+ let inbox = [];
18463
+ try {
18464
+ inbox = loadInbox(directory, sessionId);
18465
+ } catch {
18466
+ return;
18467
+ }
18468
+ const deliverable = inbox.filter((m) => isPending(m) && !isHopCapParked(m));
18469
+ if (deliverable.length === 0) return;
18470
+ logger.log(`[Session ${sessionId}] Idle-inbox reconcile: ${deliverable.length} undelivered pending message(s) on an idle agent \u2014 delivering`);
18471
+ const existing = sessionMetadata.messageQueue || [];
18472
+ const seen = new Set(existing.map((e) => e.id));
18473
+ const additions = deliverable.filter((m) => !seen.has(m.messageId)).map((m) => {
18474
+ const formatted = formatInboxMessageXml(m);
18475
+ return { id: m.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: m.hopCount ?? 0, inboxThreadId: m.threadId || m.messageId };
18476
+ });
18477
+ if (additions.length === 0) {
18478
+ processMessageQueueRef?.();
18479
+ return;
18480
+ }
18481
+ sessionMetadata = { ...sessionMetadata, messageQueue: [...existing, ...additions] };
18482
+ sessionService.updateMetadata(sessionMetadata);
18483
+ processMessageQueueRef?.();
18484
+ };
18485
+ if (INBOX_RECONCILE_INTERVAL_MS > 0) {
18486
+ inboxReconcileTimer = setInterval(reconcileIdleInbox, INBOX_RECONCILE_INTERVAL_MS);
18487
+ }
18371
18488
  return {
18372
18489
  type: "success",
18373
18490
  sessionId,
@@ -18721,11 +18838,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
18721
18838
  });
18722
18839
  },
18723
18840
  onIssue: async (params) => {
18724
- const { issueRpc } = await import('./rpc-Bbnunkmx.mjs');
18841
+ const { issueRpc } = await import('./rpc-BQ8zLSUc.mjs');
18725
18842
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
18726
18843
  },
18727
18844
  onWorkflow: async (params) => {
18728
- const { workflowRpc } = await import('./rpc-fy0bKbnH.mjs');
18845
+ const { workflowRpc } = await import('./rpc-xlV-4-BG.mjs');
18729
18846
  return workflowRpc(params?.cwd || directory, params || {});
18730
18847
  },
18731
18848
  onRipgrep: async (args, cwd) => {
@@ -19161,6 +19278,59 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
19161
19278
  }
19162
19279
  };
19163
19280
  pidToTrackedSession.set(randomUUID$1(), trackedSession);
19281
+ let acpInboxReconcileTimer = null;
19282
+ const reconcileAcpIdleInbox = () => {
19283
+ if (acpStopped) {
19284
+ if (acpInboxReconcileTimer) clearInterval(acpInboxReconcileTimer);
19285
+ return;
19286
+ }
19287
+ if (!acpBackendReady) return;
19288
+ if (sessionMetadata.lifecycleState !== "idle") return;
19289
+ let inbox = [];
19290
+ try {
19291
+ inbox = loadInbox(directory, sessionId);
19292
+ } catch {
19293
+ return;
19294
+ }
19295
+ const existing = sessionMetadata.messageQueue || [];
19296
+ const seen = new Set(existing.map((e) => e.id));
19297
+ const deliverable = inbox.filter((m) => isPending(m) && !isHopCapParked(m) && !seen.has(m.messageId));
19298
+ let queue = existing;
19299
+ if (deliverable.length > 0) {
19300
+ const additions = deliverable.map((m) => {
19301
+ const formatted = formatInboxMessageXml(m);
19302
+ return { id: m.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: m.hopCount ?? 0, inboxThreadId: m.threadId || m.messageId };
19303
+ });
19304
+ queue = [...existing, ...additions];
19305
+ sessionMetadata = { ...sessionMetadata, messageQueue: queue };
19306
+ sessionService.updateMetadata(sessionMetadata);
19307
+ }
19308
+ if (!queue || queue.length === 0) return;
19309
+ const next = queue[0];
19310
+ const remaining = queue.slice(1);
19311
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
19312
+ sessionService.updateMetadata(sessionMetadata);
19313
+ if (typeof next.inboxHop === "number") {
19314
+ writeInboundContext(sessionId, { hopCount: next.inboxHop, threadId: next.inboxThreadId });
19315
+ sessionService.markInboxHandled(next.id);
19316
+ }
19317
+ logger.log(`[${agentName} Session ${sessionId}] Idle-inbox reconcile: delivering queued message to idle agent`);
19318
+ sessionService.pushMessage(next.displayText || next.text, "user");
19319
+ sessionService.sendKeepAlive(true);
19320
+ agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
19321
+ logger.error(`[${agentName} Session ${sessionId}] Error delivering reconciled inbox message: ${err.message}`);
19322
+ if (!acpStopped) {
19323
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
19324
+ sessionService.updateMetadata(sessionMetadata);
19325
+ sessionService.sendSessionEnd();
19326
+ }
19327
+ });
19328
+ };
19329
+ {
19330
+ const acpReconcileMs = Number(process.env.SVAMP_INBOX_RECONCILE_MS);
19331
+ const acpReconcileInterval = Number.isFinite(acpReconcileMs) && acpReconcileMs >= 0 ? acpReconcileMs : 15e3;
19332
+ if (acpReconcileInterval > 0) acpInboxReconcileTimer = setInterval(reconcileAcpIdleInbox, acpReconcileInterval);
19333
+ }
19164
19334
  logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
19165
19335
  agentBackend.startSession().then(() => {
19166
19336
  acpBackendReady = true;
@@ -19564,7 +19734,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
19564
19734
  }
19565
19735
  if (persistedSessions.length > 0) {
19566
19736
  try {
19567
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-B9O-hKxm.mjs');
19737
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-CgEtGkX2.mjs');
19568
19738
  await awaitClaudeVersionReady();
19569
19739
  } catch {
19570
19740
  }
@@ -19761,7 +19931,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
19761
19931
  const PING_TIMEOUT_MS = 15e3;
19762
19932
  const POST_RECONNECT_GRACE_MS = 2e4;
19763
19933
  const RECONNECT_JITTER_MS = 2500;
19764
- const { WorkflowScheduler } = await import('./scheduler-C_3x4bEj.mjs');
19934
+ const { WorkflowScheduler } = await import('./scheduler-DaFZY2ob.mjs');
19765
19935
  const workflowScheduler = new WorkflowScheduler({
19766
19936
  projectRoots: () => {
19767
19937
  const dirs = /* @__PURE__ */ new Set();
@@ -1,4 +1,4 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { ad as applyClaudeProxyEnv, ae as composeSessionId, af as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, ag as generateHookSettings } from './run-fDelGoR-.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { ad as applyClaudeProxyEnv, ae as composeSessionId, af as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, ag as generateHookSettings } from './run-C3Fcbk-H.mjs';
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
@@ -1,4 +1,4 @@
1
- import { j as resolveProjectRoot, D as listWorkflows, E as isWorkflowEnabled, F as workflowCrons, y as runWorkflow, G as cronMatches } from './run-fDelGoR-.mjs';
1
+ import { j as resolveProjectRoot, D as listWorkflows, E as isWorkflowEnabled, F as workflowCrons, y as runWorkflow, G as cronMatches } from './run-C3Fcbk-H.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-CZqQryOD.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-D7fIWAgz.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
@@ -93,7 +93,7 @@ async function serveAdd(args, machineId) {
93
93
  }
94
94
  }
95
95
  async function serveApply(args, machineId) {
96
- const { connectAndGetMachine } = await import('./commands-CZqQryOD.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-D7fIWAgz.mjs');
97
97
  const fs = await import('fs');
98
98
  const yaml = await import('yaml');
99
99
  const file = positionalArgs(args)[0];
@@ -182,7 +182,7 @@ async function serveApply(args, machineId) {
182
182
  }
183
183
  }
184
184
  async function serveRemove(args, machineId) {
185
- const { connectAndGetMachine } = await import('./commands-CZqQryOD.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-D7fIWAgz.mjs');
186
186
  const pos = positionalArgs(args);
187
187
  const name = pos[0];
188
188
  if (!name) {
@@ -202,7 +202,7 @@ async function serveRemove(args, machineId) {
202
202
  }
203
203
  }
204
204
  async function serveList(args, machineId) {
205
- const { connectAndGetMachine } = await import('./commands-CZqQryOD.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-D7fIWAgz.mjs');
206
206
  const all = hasFlag(args, "--all", "-a");
207
207
  const json = hasFlag(args, "--json");
208
208
  const sessionId = getFlag(args, "--session");
@@ -235,7 +235,7 @@ async function serveList(args, machineId) {
235
235
  }
236
236
  }
237
237
  async function serveInfo(machineId) {
238
- const { connectAndGetMachine } = await import('./commands-CZqQryOD.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-D7fIWAgz.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();
@@ -1,4 +1,4 @@
1
- import { R as READ_ONLY_TOOLS, J as loadMachineContext, K as buildMachineInstructions, L as machineToolsForRole, M as buildMachineTools } from './run-fDelGoR-.mjs';
1
+ import { R as READ_ONLY_TOOLS, J as loadMachineContext, K as buildMachineInstructions, L as machineToolsForRole, M as buildMachineTools } from './run-C3Fcbk-H.mjs';
2
2
  import 'node:child_process';
3
3
  import 'os';
4
4
  import 'fs/promises';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.273",
3
+ "version": "0.2.275",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "rm -rf dist bin/skills bin/commands && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && cp -r ../../skills/crew bin/skills/crew && cp -r ../../commands bin/commands && tsc --noEmit && pkgroll",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-backend-accounts.mjs && npx tsx test/test-backend-oauth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-session-access-fallback.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-app-server.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-compact-detect.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-loop-verify.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-cron.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-workflow-idle.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-ws-upgrade.mjs && npx tsx test/test-serve-auth.mjs && npx tsx test/test-static-file-server.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-outbox-reload.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs && npx tsx test/test-artifact-guard.mjs && npx tsx test/test-graceful-restart.mjs && npx tsx test/test-flush-exit.mjs",
23
+ "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-backend-accounts.mjs && npx tsx test/test-backend-oauth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-inbox-reconcile.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-session-access-fallback.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-app-server.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-compact-detect.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-loop-verify.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-cron.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-workflow-idle.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-ws-upgrade.mjs && npx tsx test/test-serve-auth.mjs && npx tsx test/test-static-file-server.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-outbox-reload.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs && npx tsx test/test-artifact-guard.mjs && npx tsx test/test-graceful-restart.mjs && npx tsx test/test-flush-exit.mjs",
24
24
  "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
25
25
  "dev": "tsx src/cli.ts",
26
26
  "dev:daemon": "tsx src/cli.ts daemon start-sync",