svamp-cli 0.2.112 → 0.2.114

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,5 +1,5 @@
1
- import { F as resolveModel, O as describeMisconfiguration, P as buildMachineDeps } from './run-Bs_f2KsC.mjs';
2
- import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-D1f0_8ao.mjs';
1
+ import { F as resolveModel, O as describeMisconfiguration, P as buildMachineDeps } from './run-aNUfTpRW.mjs';
2
+ import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-BCKpph51.mjs';
3
3
  import { WebSocket } from 'ws';
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import 'os';
@@ -13,8 +13,8 @@ import 'node:fs';
13
13
  import 'node:child_process';
14
14
  import 'util';
15
15
  import 'node:path';
16
- import 'node:os';
17
16
  import 'node:events';
17
+ import 'node:os';
18
18
  import '@agentclientprotocol/sdk';
19
19
  import '@modelcontextprotocol/sdk/client/index.js';
20
20
  import '@modelcontextprotocol/sdk/client/stdio.js';
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { c as connectToHypha, a as createSessionStore, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, s as startDaemon, b as stopDaemon } from './run-Bs_f2KsC.mjs';
1
+ export { c as connectToHypha, a as createSessionStore, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, s as startDaemon, b as stopDaemon } from './run-aNUfTpRW.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -11,8 +11,8 @@ import 'node:fs';
11
11
  import 'node:child_process';
12
12
  import 'util';
13
13
  import 'node:path';
14
- import 'node:os';
15
14
  import 'node:events';
15
+ import 'node:os';
16
16
  import '@agentclientprotocol/sdk';
17
17
  import '@modelcontextprotocol/sdk/client/index.js';
18
18
  import '@modelcontextprotocol/sdk/client/stdio.js';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.112";
2
+ var version = "0.2.114";
3
3
  var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
4
  var author = "Amun AI AB";
5
5
  var license = "SEE LICENSE IN LICENSE";
@@ -19,7 +19,7 @@ var exports$1 = {
19
19
  var scripts = {
20
20
  build: "rm -rf dist bin/skills && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && cp -r ../../skills/loop bin/skills/loop && tsc --noEmit && pkgroll",
21
21
  typecheck: "tsc --noEmit",
22
- test: "npx tsx test/test-context-window.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-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-loop-activation.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-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-interactive-helpers.mjs && npx tsx test/test-codex-backend.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-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-short-id.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-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-routine.mjs && npx tsx test/test-routine-rpc.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-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs",
22
+ test: "npx tsx test/test-context-window.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-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-loop-activation.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-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-interactive-helpers.mjs && npx tsx test/test-codex-backend.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-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-short-id.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-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-routine.mjs && npx tsx test/test-routine-rpc.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-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs",
23
23
  "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
24
  dev: "tsx src/cli.ts",
25
25
  "dev:daemon": "tsx src/cli.ts daemon start-sync",
@@ -1,4 +1,4 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { n as shortId, c as connectToHypha, a as createSessionStore, r as registerMachineService, Q as generateHookSettings } from './run-Bs_f2KsC.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { n as shortId, c as connectToHypha, a as createSessionStore, r as registerMachineService, Q as generateHookSettings } from './run-aNUfTpRW.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';
@@ -10,8 +10,8 @@ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, rename
10
10
  import { exec, execSync, spawn, execFile as execFile$1, execFileSync } from 'node:child_process';
11
11
  import { promisify } from 'util';
12
12
  import { join, basename, dirname } from 'node:path';
13
- import os, { homedir, platform } from 'node:os';
14
13
  import { EventEmitter } from 'node:events';
14
+ import os, { homedir, platform } from 'node:os';
15
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
16
16
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
17
17
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
@@ -1388,22 +1388,36 @@ function generateSkillBody(channel, ctx) {
1388
1388
  const bindNote = mode === "stateless" ? `**Stateless channel** \u2014 each call spawns a fresh, isolated one-shot session (cold-start: a few seconds), runs, replies, and closes. No shared memory between calls.` : mode === "fixed" ? `Routes to a **fixed session** (\`${rSession}\`); reachable only while that session is live.` : rSession ? `Routes to **session \`${rSession}\`** (the one this instruction was copied from); reachable only while it is live, else a fresh stateless run answers.` : `**Dynamic channel** \u2014 pass \`session: "<id>"\` to target a specific live session, else a fresh stateless run answers.`;
1389
1389
  const name = channel.skill?.name || channel.name;
1390
1390
  const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
1391
- const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : isQueue ? `This is an **async** channel: \`send()\` returns a \`correlationId\` and the agent replies later \u2014 poll \`receive()\` (or GET /receive \xB7 /events) for replies addressed to you.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
1391
+ const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : isQueue ? `This is an **async** channel: \`send()\` usually returns \`{ status: "queued", correlationId }\` and the agent replies later \u2014 poll \`receive()\` (or GET /receive \xB7 /events) for replies addressed to you. **But it may also answer synchronously** with \`{ status: "completed", reply }\` (e.g. when the target session is gone and a fresh one-shot run handles the call) \u2014 always check \`status\` first and use \`reply\` directly when present; \`receive()\` has nothing to return for a one-shot.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
1392
1392
  const queueSection = isQueue ? `
1393
1393
 
1394
- ## Getting the reply (async)
1395
- \`send()\` returns \`{ correlationId }\`. The agent answers later; retrieve replies addressed
1396
- to you by **long-polling** \`receive\` with a cursor you advance each call:
1394
+ ## Getting the reply
1395
+ \`send()\` returns one of two shapes \u2014 **check \`status\` first**:
1396
+ - \`{ status: "completed", reply, correlationId }\` \u2014 answered synchronously (e.g. a
1397
+ dynamic channel whose target session is gone falls back to a fresh one-shot run).
1398
+ Use \`reply\` directly; do **not** poll \`receive()\` \u2014 there is no persistent session
1399
+ to poll and it will return an error.
1400
+ - \`{ status: "queued", correlationId }\` \u2014 a live session will answer later; long-poll
1401
+ \`receive\` for replies addressed to you, advancing \`cursor\` each call:
1397
1402
  \`\`\`js
1398
- const { correlationId } = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
1399
- let cursor = 0;
1403
+ const res = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
1404
+ if (res.status === "completed") return res.reply; // answered now \u2014 done
1405
+ let cursor = 0; // else poll for the async reply
1400
1406
  while (true) {
1401
1407
  const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
1402
1408
  cursor = r.cursor;
1403
- for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
1409
+ for (const reply of r.replies) if (reply.correlationId === res.correlationId) return reply.body;
1404
1410
  }
1405
1411
  \`\`\`
1406
- **HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE).` : "";
1412
+ **HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE).
1413
+
1414
+ ### Agent-to-agent: get the reply in your own inbox
1415
+ If **you are a Svamp agent session**, pass \`reply_to: { session: "<your-session-id>" }\`
1416
+ to \`send()\`. The reply is then delivered straight to **your session's inbox**
1417
+ (\`inbox reply\`-able, persistent) \u2014 symmetric agent\u2194agent messaging. Without
1418
+ \`reply_to.session\` you are treated as an **external caller**: the reply goes to this
1419
+ channel's outbox and you must poll \`receive()\` (above) for it \u2014 it will **not** appear
1420
+ in your inbox.` : "";
1407
1421
  const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
1408
1422
  return `---
1409
1423
  name: ${name}
@@ -1449,7 +1463,10 @@ function resolveSender(channel, input = {}) {
1449
1463
  }
1450
1464
  if (id.mode === "per-key") {
1451
1465
  const caller = (id.callers || []).find((c) => c.key && c.key === key);
1452
- if (!caller) return { error: "invalid or missing key" };
1466
+ if (!caller) {
1467
+ const acceptsHypha = Array.isArray(id.hypha_allow) && id.hypha_allow.length > 0;
1468
+ return { error: acceptsHypha ? "invalid or missing key \u2014 supply a caller key issued by the channel owner, or call with an authenticated Hypha identity (this channel accepts hypha_allow callers; anonymous/keyless requests are rejected)" : "invalid or missing key \u2014 this channel requires a caller key issued by the channel owner" };
1469
+ }
1453
1470
  return { sender: { name: caller.name, kind: caller.kind, verified: true } };
1454
1471
  }
1455
1472
  if (id.mode === "caller-supplied") {
@@ -1482,6 +1499,138 @@ function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, no
1482
1499
  return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
1483
1500
  }
1484
1501
 
1502
+ const MAX_PER_CHANNEL = 200;
1503
+ const TTL_MS = 60 * 60 * 1e3;
1504
+ class ChannelOutbox {
1505
+ file;
1506
+ byChannel = /* @__PURE__ */ new Map();
1507
+ seqByChannel = /* @__PURE__ */ new Map();
1508
+ emitter = new EventEmitter();
1509
+ constructor(projectDir) {
1510
+ const dir = join(projectDir, ".svamp", "channels");
1511
+ this.file = join(dir, "_outbox.jsonl");
1512
+ this.emitter.setMaxListeners(0);
1513
+ try {
1514
+ mkdirSync(dir, { recursive: true });
1515
+ } catch {
1516
+ }
1517
+ this._load();
1518
+ }
1519
+ _load() {
1520
+ if (!existsSync(this.file)) return;
1521
+ const cutoff = Date.now() - TTL_MS;
1522
+ try {
1523
+ for (const line of readFileSync(this.file, "utf8").split("\n")) {
1524
+ if (!line.trim()) continue;
1525
+ let rec = null;
1526
+ try {
1527
+ rec = JSON.parse(line);
1528
+ } catch {
1529
+ continue;
1530
+ }
1531
+ if (!rec || rec.ts < cutoff) continue;
1532
+ const { channelId, ...reply } = rec;
1533
+ const arr = this.byChannel.get(channelId) || [];
1534
+ arr.push(reply);
1535
+ this.byChannel.set(channelId, arr);
1536
+ this.seqByChannel.set(channelId, Math.max(this.seqByChannel.get(channelId) || 0, reply.seq));
1537
+ }
1538
+ for (const [ch, arr] of this.byChannel) if (arr.length > MAX_PER_CHANNEL) this.byChannel.set(ch, arr.slice(-MAX_PER_CHANNEL));
1539
+ } catch {
1540
+ }
1541
+ }
1542
+ _evict(channelId) {
1543
+ const cutoff = Date.now() - TTL_MS;
1544
+ let arr = this.byChannel.get(channelId);
1545
+ if (!arr) return;
1546
+ if (arr.some((r) => r.ts < cutoff)) arr = arr.filter((r) => r.ts >= cutoff);
1547
+ if (arr.length > MAX_PER_CHANNEL) arr = arr.slice(-MAX_PER_CHANNEL);
1548
+ this.byChannel.set(channelId, arr);
1549
+ }
1550
+ /** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
1551
+ append(channelId, r) {
1552
+ const seq = (this.seqByChannel.get(channelId) || 0) + 1;
1553
+ this.seqByChannel.set(channelId, seq);
1554
+ const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
1555
+ const arr = this.byChannel.get(channelId) || [];
1556
+ arr.push(reply);
1557
+ this.byChannel.set(channelId, arr);
1558
+ this._evict(channelId);
1559
+ try {
1560
+ appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
1561
+ } catch {
1562
+ try {
1563
+ mkdirSync(join(this.file, ".."), { recursive: true });
1564
+ appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
1565
+ } catch {
1566
+ }
1567
+ }
1568
+ this.emitter.emit(channelId, reply);
1569
+ return reply;
1570
+ }
1571
+ /** Replies for `to` on `channelId` with seq > cursor (optionally one correlationId). */
1572
+ since(channelId, cursor, to, correlationId) {
1573
+ this._evict(channelId);
1574
+ return (this.byChannel.get(channelId) || []).filter((r) => r.seq > cursor && r.to === to && (!correlationId || r.correlationId === correlationId));
1575
+ }
1576
+ /** Highest seq on a channel (the cursor a caller gets back). */
1577
+ cursor(channelId) {
1578
+ return this.seqByChannel.get(channelId) || 0;
1579
+ }
1580
+ /**
1581
+ * Long-poll: resolve immediately if there are replies after `cursor`, else wait for
1582
+ * the next append (filtered to this channel + `to`) or `timeoutMs`, then return.
1583
+ */
1584
+ wait(channelId, cursor, to, timeoutMs, correlationId) {
1585
+ const ready = this.since(channelId, cursor, to, correlationId);
1586
+ if (ready.length) return Promise.resolve({ replies: ready, cursor: this.cursor(channelId) });
1587
+ return new Promise((resolve) => {
1588
+ const onReply = (r) => {
1589
+ if (r.to !== to || correlationId && r.correlationId !== correlationId) return;
1590
+ cleanup();
1591
+ resolve({ replies: this.since(channelId, cursor, to, correlationId), cursor: this.cursor(channelId) });
1592
+ };
1593
+ const timer = setTimeout(() => {
1594
+ cleanup();
1595
+ resolve({ replies: [], cursor: this.cursor(channelId) });
1596
+ }, Math.max(0, timeoutMs));
1597
+ const cleanup = () => {
1598
+ clearTimeout(timer);
1599
+ this.emitter.off(channelId, onReply);
1600
+ };
1601
+ this.emitter.on(channelId, onReply);
1602
+ });
1603
+ }
1604
+ /** Push subscription for SSE: calls onReply for each new reply addressed to `to`. */
1605
+ subscribe(channelId, to, onReply) {
1606
+ const handler = (r) => {
1607
+ if (r.to === to) onReply(r);
1608
+ };
1609
+ this.emitter.on(channelId, handler);
1610
+ return () => this.emitter.off(channelId, handler);
1611
+ }
1612
+ /** Drop a channel's outbox (on channel delete) — best-effort rewrite of the log. */
1613
+ purge(channelId) {
1614
+ this.byChannel.delete(channelId);
1615
+ this.seqByChannel.delete(channelId);
1616
+ if (!existsSync(this.file)) return;
1617
+ try {
1618
+ const kept = readFileSync(this.file, "utf8").split("\n").filter((l) => {
1619
+ if (!l.trim()) return false;
1620
+ try {
1621
+ return JSON.parse(l).channelId !== channelId;
1622
+ } catch {
1623
+ return false;
1624
+ }
1625
+ });
1626
+ const tmp = this.file + ".tmp";
1627
+ writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
1628
+ renameSync(tmp, this.file);
1629
+ } catch {
1630
+ }
1631
+ }
1632
+ }
1633
+
1485
1634
  function getParamNames(fn) {
1486
1635
  const src = fn.toString();
1487
1636
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -2527,7 +2676,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2527
2676
  const tunnels = handlers.tunnels;
2528
2677
  if (!tunnels) throw new Error("Tunnel management not available");
2529
2678
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2530
- const { FrpcTunnel } = await import('./frpc-BoPlePa3.mjs');
2679
+ const { FrpcTunnel } = await import('./frpc-BjWZ129L.mjs');
2531
2680
  const tunnel = new FrpcTunnel({
2532
2681
  name: params.name,
2533
2682
  ports: params.ports,
@@ -2788,7 +2937,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2788
2937
  }
2789
2938
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2790
2939
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2791
- const { toolsForRole } = await import('./sideband-D1f0_8ao.mjs');
2940
+ const { toolsForRole } = await import('./sideband-BCKpph51.mjs');
2792
2941
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2793
2942
  return fmt(r2);
2794
2943
  }
@@ -2887,7 +3036,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2887
3036
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
2888
3037
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
2889
3038
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
2890
- const { queryCore } = await import('./commands-P9-75WDZ.mjs');
3039
+ const { queryCore } = await import('./commands-C29ciVUe.mjs');
2891
3040
  const timeout = c.reply?.timeout_sec || 120;
2892
3041
  let result;
2893
3042
  try {
@@ -2904,7 +3053,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2904
3053
  }
2905
3054
  if (result.status === "error") return { ok: false, call_id: callId, status: "error", error: result.error };
2906
3055
  if (result.status === "permission-pending") return { ok: false, call_id: callId, status: "permission-pending", error: result.error };
2907
- return { ok: true, call_id: callId, status: "completed", reply: result.response };
3056
+ return { ok: true, call_id: callId, correlationId: callId, status: "completed", reply: result.response };
2908
3057
  };
2909
3058
  const channelsServiceInfo = await server.registerService(
2910
3059
  {
@@ -2925,10 +3074,16 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2925
3074
  }
2926
3075
  return out;
2927
3076
  },
2928
- describe: async (kwargs = {}) => {
2929
- const rpc = await findChannelOwner(kwargs.channel);
3077
+ // Accept a plain `?channel=<id>` GET (positional, like `skill`) AS WELL AS
3078
+ // the kwargs-wrapped form. The Hypha HTTP gateway only maps a `?channel=`
3079
+ // query onto a param literally named `channel`; a kwargs-only signature left
3080
+ // `channel` undefined on GET, so describe() 404'd on every channel that
3081
+ // list() returned. Normalize both the string and `{channel}` object shapes.
3082
+ describe: async (channel) => {
3083
+ const id = typeof channel === "string" ? channel : channel?.channel;
3084
+ const rpc = id ? await findChannelOwner(id) : void 0;
2930
3085
  if (!rpc?.channelDescribe) return { error: "channel not found" };
2931
- return rpc.channelDescribe(kwargs.channel);
3086
+ return rpc.channelDescribe(id);
2932
3087
  },
2933
3088
  // Clean GET endpoint for the self-contained skill markdown. Param is
2934
3089
  // named `channel` (not `kwargs`) so the Hypha HTTP gateway maps a plain
@@ -2958,9 +3113,26 @@ ${d?.error || "not found"}`;
2958
3113
  trackInbound();
2959
3114
  const res = resolveChannel(kwargs.channel, kwargs.session);
2960
3115
  if ("error" in res) return { error: res.error };
2961
- if (res.tier === "stateless") return { error: "stateless channels have no async replies (no persistent session)" };
2962
- if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped` };
2963
- return res.rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
3116
+ if (res.tier === "session" && !("stopped" in res)) {
3117
+ return res.rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
3118
+ }
3119
+ const { c, dir } = res;
3120
+ if (c.reply?.mode !== "queue") {
3121
+ return { error: "this channel has no async replies (not a queue-mode channel)" };
3122
+ }
3123
+ const u = context?.user;
3124
+ const r = resolveSender(c, {
3125
+ key: kwargs.key,
3126
+ from: kwargs.from,
3127
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
3128
+ hyphaAnonymous: u?.is_anonymous === true,
3129
+ hyphaWorkspace: u?.scope?.current_workspace
3130
+ });
3131
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3132
+ const cursor = Math.max(0, Number(kwargs.cursor) || 0);
3133
+ const outbox = new ChannelOutbox(dir);
3134
+ const replies = outbox.since(c.id, cursor, r.sender.name, kwargs.correlationId);
3135
+ return { ok: true, replies, cursor: outbox.cursor(c.id) };
2964
3136
  }
2965
3137
  },
2966
3138
  { overwrite: true }
@@ -3202,138 +3374,6 @@ class RoutineRunner {
3202
3374
  }
3203
3375
  }
3204
3376
 
3205
- const MAX_PER_CHANNEL = 200;
3206
- const TTL_MS = 60 * 60 * 1e3;
3207
- class ChannelOutbox {
3208
- file;
3209
- byChannel = /* @__PURE__ */ new Map();
3210
- seqByChannel = /* @__PURE__ */ new Map();
3211
- emitter = new EventEmitter();
3212
- constructor(projectDir) {
3213
- const dir = join(projectDir, ".svamp", "channels");
3214
- this.file = join(dir, "_outbox.jsonl");
3215
- this.emitter.setMaxListeners(0);
3216
- try {
3217
- mkdirSync(dir, { recursive: true });
3218
- } catch {
3219
- }
3220
- this._load();
3221
- }
3222
- _load() {
3223
- if (!existsSync(this.file)) return;
3224
- const cutoff = Date.now() - TTL_MS;
3225
- try {
3226
- for (const line of readFileSync(this.file, "utf8").split("\n")) {
3227
- if (!line.trim()) continue;
3228
- let rec = null;
3229
- try {
3230
- rec = JSON.parse(line);
3231
- } catch {
3232
- continue;
3233
- }
3234
- if (!rec || rec.ts < cutoff) continue;
3235
- const { channelId, ...reply } = rec;
3236
- const arr = this.byChannel.get(channelId) || [];
3237
- arr.push(reply);
3238
- this.byChannel.set(channelId, arr);
3239
- this.seqByChannel.set(channelId, Math.max(this.seqByChannel.get(channelId) || 0, reply.seq));
3240
- }
3241
- for (const [ch, arr] of this.byChannel) if (arr.length > MAX_PER_CHANNEL) this.byChannel.set(ch, arr.slice(-MAX_PER_CHANNEL));
3242
- } catch {
3243
- }
3244
- }
3245
- _evict(channelId) {
3246
- const cutoff = Date.now() - TTL_MS;
3247
- let arr = this.byChannel.get(channelId);
3248
- if (!arr) return;
3249
- if (arr.some((r) => r.ts < cutoff)) arr = arr.filter((r) => r.ts >= cutoff);
3250
- if (arr.length > MAX_PER_CHANNEL) arr = arr.slice(-MAX_PER_CHANNEL);
3251
- this.byChannel.set(channelId, arr);
3252
- }
3253
- /** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
3254
- append(channelId, r) {
3255
- const seq = (this.seqByChannel.get(channelId) || 0) + 1;
3256
- this.seqByChannel.set(channelId, seq);
3257
- const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
3258
- const arr = this.byChannel.get(channelId) || [];
3259
- arr.push(reply);
3260
- this.byChannel.set(channelId, arr);
3261
- this._evict(channelId);
3262
- try {
3263
- appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3264
- } catch {
3265
- try {
3266
- mkdirSync(join(this.file, ".."), { recursive: true });
3267
- appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3268
- } catch {
3269
- }
3270
- }
3271
- this.emitter.emit(channelId, reply);
3272
- return reply;
3273
- }
3274
- /** Replies for `to` on `channelId` with seq > cursor (optionally one correlationId). */
3275
- since(channelId, cursor, to, correlationId) {
3276
- this._evict(channelId);
3277
- return (this.byChannel.get(channelId) || []).filter((r) => r.seq > cursor && r.to === to && (!correlationId || r.correlationId === correlationId));
3278
- }
3279
- /** Highest seq on a channel (the cursor a caller gets back). */
3280
- cursor(channelId) {
3281
- return this.seqByChannel.get(channelId) || 0;
3282
- }
3283
- /**
3284
- * Long-poll: resolve immediately if there are replies after `cursor`, else wait for
3285
- * the next append (filtered to this channel + `to`) or `timeoutMs`, then return.
3286
- */
3287
- wait(channelId, cursor, to, timeoutMs, correlationId) {
3288
- const ready = this.since(channelId, cursor, to, correlationId);
3289
- if (ready.length) return Promise.resolve({ replies: ready, cursor: this.cursor(channelId) });
3290
- return new Promise((resolve) => {
3291
- const onReply = (r) => {
3292
- if (r.to !== to || correlationId && r.correlationId !== correlationId) return;
3293
- cleanup();
3294
- resolve({ replies: this.since(channelId, cursor, to, correlationId), cursor: this.cursor(channelId) });
3295
- };
3296
- const timer = setTimeout(() => {
3297
- cleanup();
3298
- resolve({ replies: [], cursor: this.cursor(channelId) });
3299
- }, Math.max(0, timeoutMs));
3300
- const cleanup = () => {
3301
- clearTimeout(timer);
3302
- this.emitter.off(channelId, onReply);
3303
- };
3304
- this.emitter.on(channelId, onReply);
3305
- });
3306
- }
3307
- /** Push subscription for SSE: calls onReply for each new reply addressed to `to`. */
3308
- subscribe(channelId, to, onReply) {
3309
- const handler = (r) => {
3310
- if (r.to === to) onReply(r);
3311
- };
3312
- this.emitter.on(channelId, handler);
3313
- return () => this.emitter.off(channelId, handler);
3314
- }
3315
- /** Drop a channel's outbox (on channel delete) — best-effort rewrite of the log. */
3316
- purge(channelId) {
3317
- this.byChannel.delete(channelId);
3318
- this.seqByChannel.delete(channelId);
3319
- if (!existsSync(this.file)) return;
3320
- try {
3321
- const kept = readFileSync(this.file, "utf8").split("\n").filter((l) => {
3322
- if (!l.trim()) return false;
3323
- try {
3324
- return JSON.parse(l).channelId !== channelId;
3325
- } catch {
3326
- return false;
3327
- }
3328
- });
3329
- const tmp = this.file + ".tmp";
3330
- writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
3331
- renameSync(tmp, this.file);
3332
- } catch {
3333
- }
3334
- }
3335
- }
3336
-
3337
3377
  function channelPublicView(c) {
3338
3378
  return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind };
3339
3379
  }
@@ -3870,7 +3910,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3870
3910
  const result = await dispatchAgentChannel({ channel: c, sender: r.sender, message: rendered, deps, env: process.env });
3871
3911
  channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: result.status });
3872
3912
  syncChannelsToMetadata();
3873
- return { ok: result.status === "completed", call_id: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
3913
+ return { ok: result.status === "completed", call_id: callId, correlationId: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
3874
3914
  }
3875
3915
  if (c.action?.kind === "loop") return { error: "loop channels are served by the channel server, not channelSend" };
3876
3916
  const queue = c.reply?.mode === "queue";
@@ -9950,26 +9990,25 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9950
9990
  } catch {
9951
9991
  }
9952
9992
  };
9953
- let lastLoopPhase = (() => {
9993
+ let announcedLoopStart = (() => {
9954
9994
  const s = readLoopState(directory, sessionId);
9955
- return s ? s.phase ?? null : null;
9995
+ return s && (s.phase === "done" || s.phase === "gave_up") ? s.started_at ?? "\u2205" : null;
9956
9996
  })();
9957
9997
  const loopChecker = () => {
9958
9998
  try {
9959
9999
  const s = readLoopState(directory, sessionId);
9960
- const phase = s ? s.phase ?? null : null;
9961
- const wasActive = lastLoopPhase === "running" || lastLoopPhase === "continue";
9962
- if (wasActive && (phase === "done" || phase === "gave_up")) {
9963
- const iter = typeof s?.iteration === "number" ? s.iteration : 0;
9964
- const plural = iter === 1 ? "" : "s";
9965
- const msg = phase === "done" ? `\u{1F501} Loop complete \u2705 \u2014 finished after ${iter} iteration${plural}.` : `\u{1F501} Loop stopped \u{1F6D1} \u2014 gave up after ${iter} iteration${plural}${s?.gave_up_reason ? ` (${s.gave_up_reason})` : ""}.`;
9966
- sessionService.pushMessage(
9967
- phase === "gave_up" ? { type: "message", message: msg, level: "warning" } : { type: "message", message: msg },
9968
- "event"
9969
- );
9970
- logger.log(`[svampConfig] ${phase === "done" ? "Loop complete" : "Loop gave up"} (iter ${iter})`);
9971
- }
9972
- lastLoopPhase = phase;
10000
+ if (!s || s.phase !== "done" && s.phase !== "gave_up") return;
10001
+ const key = s.started_at ?? "\u2205";
10002
+ if (key === announcedLoopStart) return;
10003
+ announcedLoopStart = key;
10004
+ const iter = typeof s.iteration === "number" ? s.iteration : 0;
10005
+ const plural = iter === 1 ? "" : "s";
10006
+ const msg = s.phase === "done" ? `\u{1F501} Loop complete \u2705 \u2014 finished after ${iter} iteration${plural}.` : `\u{1F501} Loop stopped \u{1F6D1} \u2014 gave up after ${iter} iteration${plural}${s.gave_up_reason ? ` (${s.gave_up_reason})` : ""}.`;
10007
+ sessionService.pushMessage(
10008
+ s.phase === "gave_up" ? { type: "message", message: msg, level: "warning" } : { type: "message", message: msg },
10009
+ "event"
10010
+ );
10011
+ logger.log(`[svampConfig] ${s.phase === "done" ? "Loop complete" : "Loop gave up"} (iter ${iter})`);
9973
10012
  } catch {
9974
10013
  }
9975
10014
  };
@@ -10452,7 +10491,7 @@ async function startDaemon(options) {
10452
10491
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10453
10492
  saveExposedTunnels(list);
10454
10493
  }
10455
- const { ServeManager } = await import('./serveManager-D3sMgApm.mjs');
10494
+ const { ServeManager } = await import('./serveManager-DrPFeC2o.mjs');
10456
10495
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10457
10496
  ensureAutoInstalledSkills(logger).catch(() => {
10458
10497
  });
@@ -12937,7 +12976,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12937
12976
  const specs = loadExposedTunnels();
12938
12977
  if (specs.length === 0) return;
12939
12978
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12940
- const { FrpcTunnel } = await import('./frpc-BoPlePa3.mjs');
12979
+ const { FrpcTunnel } = await import('./frpc-BjWZ129L.mjs');
12941
12980
  for (const spec of specs) {
12942
12981
  if (tunnels.has(spec.name)) continue;
12943
12982
  try {
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-P9-75WDZ.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-C29ciVUe.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-P9-75WDZ.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-C29ciVUe.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-P9-75WDZ.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-C29ciVUe.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-P9-75WDZ.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-C29ciVUe.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-P9-75WDZ.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-C29ciVUe.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();
@@ -4,7 +4,7 @@ import * as fs from 'fs';
4
4
  import * as http from 'http';
5
5
  import * as net from 'net';
6
6
  import * as path from 'path';
7
- import { k as getHyphaServerUrl, S as ServeAuth, l as hasCookieToken } from './run-Bs_f2KsC.mjs';
7
+ import { k as getHyphaServerUrl, S as ServeAuth, l as hasCookieToken } from './run-aNUfTpRW.mjs';
8
8
  import 'os';
9
9
  import 'fs/promises';
10
10
  import 'url';
@@ -13,8 +13,8 @@ import 'node:fs';
13
13
  import 'node:child_process';
14
14
  import 'util';
15
15
  import 'node:path';
16
- import 'node:os';
17
16
  import 'node:events';
17
+ import 'node:os';
18
18
  import '@agentclientprotocol/sdk';
19
19
  import '@modelcontextprotocol/sdk/client/index.js';
20
20
  import '@modelcontextprotocol/sdk/client/stdio.js';
@@ -713,7 +713,7 @@ class ServeManager {
713
713
  const mount = this.mounts.get(mountName);
714
714
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, `static-${subdomainSafe}-${mount.linkToken}`]]) : void 0;
715
715
  try {
716
- const { FrpcTunnel } = await import('./frpc-BoPlePa3.mjs');
716
+ const { FrpcTunnel } = await import('./frpc-BjWZ129L.mjs');
717
717
  let tunnel;
718
718
  tunnel = new FrpcTunnel({
719
719
  name: tunnelName,