svamp-cli 0.2.100 → 0.2.102

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.
@@ -52,6 +52,62 @@ function createChannelHttpServer(deps) {
52
52
  res.writeHead(200, { "content-type": "text/markdown" }).end(d.skill?.body || "");
53
53
  return;
54
54
  }
55
+ const keyOf = () => (req.headers.authorization || "").replace(/^Bearer\s+/i, "") || u.searchParams.get("key") || void 0;
56
+ m = u.pathname.match(/^\/channel\/([\w.-]+)\/receive$/);
57
+ if (m) {
58
+ const rpc2 = await findOwner(deps, m[1]);
59
+ if (!rpc2?.channelReceive) {
60
+ json(404, { error: "channel not found" });
61
+ return;
62
+ }
63
+ const out = await rpc2.channelReceive({
64
+ channel: m[1],
65
+ key: keyOf(),
66
+ from: u.searchParams.get("from") || void 0,
67
+ cursor: Number(u.searchParams.get("cursor") || 0),
68
+ correlationId: u.searchParams.get("correlationId") || void 0,
69
+ wait: u.searchParams.get("wait") != null ? Number(u.searchParams.get("wait")) : void 0
70
+ });
71
+ json(out?.error ? out.error === "channel not found" ? 404 : 401 : 200, out);
72
+ return;
73
+ }
74
+ m = u.pathname.match(/^\/channel\/([\w.-]+)\/events$/);
75
+ if (m) {
76
+ const rpc2 = await findOwner(deps, m[1]);
77
+ if (!rpc2?.channelReceive) {
78
+ res.writeHead(404).end("not found");
79
+ return;
80
+ }
81
+ const key = keyOf();
82
+ const from = u.searchParams.get("from") || void 0;
83
+ let cursor = Number(u.searchParams.get("cursor") || 0);
84
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive", "x-accel-buffering": "no" });
85
+ res.write(": connected\n\n");
86
+ let closed = false;
87
+ req.on("close", () => {
88
+ closed = true;
89
+ });
90
+ while (!closed) {
91
+ const out = await rpc2.channelReceive({ channel: m[1], key, from, cursor, wait: 25 });
92
+ if (out?.error) {
93
+ res.write(`event: error
94
+ data: ${JSON.stringify({ error: out.error })}
95
+
96
+ `);
97
+ break;
98
+ }
99
+ for (const reply of out.replies || []) res.write(`data: ${JSON.stringify(reply)}
100
+
101
+ `);
102
+ cursor = out.cursor ?? cursor;
103
+ if (!(out.replies || []).length) res.write(": keepalive\n\n");
104
+ }
105
+ try {
106
+ res.end();
107
+ } catch {
108
+ }
109
+ return;
110
+ }
55
111
  m = u.pathname.match(/^\/channel\/([\w.-]+)$/);
56
112
  if (!m) {
57
113
  json(404, { error: "not found" });
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-CdtYIBbd.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-B_FyvS11.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -12,6 +12,7 @@ import 'util';
12
12
  import 'node:crypto';
13
13
  import 'node:path';
14
14
  import 'node:os';
15
+ import 'node:events';
15
16
  import '@agentclientprotocol/sdk';
16
17
  import '@modelcontextprotocol/sdk/client/index.js';
17
18
  import '@modelcontextprotocol/sdk/client/stdio.js';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.100";
2
+ var version = "0.2.102";
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-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-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-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-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-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-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",
@@ -11,6 +11,7 @@ import { promisify } from 'util';
11
11
  import { randomBytes, randomUUID, createHash } from 'node:crypto';
12
12
  import { join as join$1 } from 'node:path';
13
13
  import os, { homedir, platform } from 'node:os';
14
+ import { EventEmitter } from 'node:events';
14
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
15
16
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
16
17
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
@@ -433,6 +434,75 @@ function buildTools(deps, skills) {
433
434
  }
434
435
  return "(sent to the coding agent)";
435
436
  }
437
+ },
438
+ // Create routine / loop / channel. ONLY call after the caller confirmed the
439
+ // proposed config (see the propose-then-confirm instruction in context.ts).
440
+ {
441
+ name: "create_routine",
442
+ readOnly: false,
443
+ description: "Create a session-scoped routine (cron schedule or webhook \u2192 message or loop). ONLY after the caller confirmed the proposal.",
444
+ parameters: { type: "object", properties: {
445
+ name: { type: "string", description: "Short human name." },
446
+ cron: { type: "string", description: 'Cron for a schedule trigger, e.g. "0 9 * * 1-5". Omit for a webhook.' },
447
+ tz: { type: "string", description: "IANA timezone for the schedule." },
448
+ action_kind: { type: "string", enum: ["message", "loop"], description: '"message" sends text to this session; "loop" starts a self-verifying loop.' },
449
+ message: { type: "string", description: "For message action: the text/template to deliver." },
450
+ task: { type: "string", description: "For loop action: the loop task." },
451
+ oracle: { type: "string", description: "For loop action: optional pass/fail command." }
452
+ }, required: ["name", "action_kind"], additionalProperties: false },
453
+ run: async (a) => {
454
+ const cron = a?.cron ? str$1(a.cron) : "";
455
+ const routine = {
456
+ name: str$1(a?.name),
457
+ enabled: true,
458
+ trigger: cron ? { type: "schedule", cron, ...a?.tz ? { tz: str$1(a.tz) } : {} } : { type: "webhook" },
459
+ action: str$1(a?.action_kind) === "loop" ? { kind: "loop", task_template: str$1(a?.task), loop: { task: str$1(a?.task), ...a?.oracle ? { oracle: str$1(a.oracle) } : {} } } : { kind: "message", template: str$1(a?.message) }
460
+ };
461
+ const r = await deps.saveRoutine(routine);
462
+ return r.success ? `Created routine "${str$1(a?.name)}" (${r.routine?.id || "saved"}).` : `Could not create routine: ${r.error || "unknown error"}.`;
463
+ }
464
+ },
465
+ {
466
+ name: "create_loop",
467
+ readOnly: false,
468
+ description: "Start a self-verifying loop in the bound session (iterates until an evaluator + optional oracle confirm done). ONLY after the caller confirmed the proposal.",
469
+ parameters: { type: "object", properties: {
470
+ task: { type: "string", description: "What the loop should accomplish." },
471
+ criteria: { type: "string", description: "How we know it is genuinely done." },
472
+ oracle: { type: "string", description: 'Optional pass/fail command (e.g. "npm test").' },
473
+ max_iterations: { type: "number", description: "Iteration ceiling (default 20)." }
474
+ }, required: ["task"], additionalProperties: false },
475
+ run: async (a) => {
476
+ await deps.startLoop({
477
+ task: str$1(a?.task),
478
+ criteria: a?.criteria ? str$1(a.criteria) : void 0,
479
+ oracle: a?.oracle ? str$1(a.oracle) : void 0,
480
+ maxIterations: typeof a?.max_iterations === "number" ? a.max_iterations : void 0
481
+ });
482
+ return `Started a loop: "${str$1(a?.task).slice(0, 80)}".`;
483
+ }
484
+ },
485
+ {
486
+ name: "create_channel",
487
+ readOnly: false,
488
+ description: "Create an inbound channel so other users/agents can message this session (identity-tagged). ONLY after the caller confirmed the proposal.",
489
+ parameters: { type: "object", properties: {
490
+ name: { type: "string", description: "Short channel name." },
491
+ description: { type: "string", description: "What this channel is for." },
492
+ identity_mode: { type: "string", enum: ["per-key", "caller-supplied", "fixed"], description: 'How callers are identified (default "per-key").' }
493
+ }, required: ["name"], additionalProperties: false },
494
+ run: async (a) => {
495
+ const mode = a?.identity_mode ? str$1(a.identity_mode) : "per-key";
496
+ const channel = {
497
+ name: str$1(a?.name),
498
+ ...a?.description ? { description: str$1(a.description) } : {},
499
+ enabled: true,
500
+ identity: mode === "fixed" ? { mode, fixed: { name: "caller", kind: "agent" } } : { mode },
501
+ action: { kind: "message" }
502
+ };
503
+ const r = await deps.saveChannel(channel);
504
+ return r.success ? `Created channel "${str$1(a?.name)}" (${r.channel?.id || "saved"}).` : `Could not create channel: ${r.error || "unknown error"}.`;
505
+ }
436
506
  }
437
507
  ];
438
508
  }
@@ -560,10 +630,12 @@ You are WISE Agent, a fast, text-mode companion to the deep coding agent (Claude
560
630
  - use_skill \u2014 load a project skill's full steps by name, then carry them out with run_bash.
561
631
  - run_bash \u2014 run a shell command on the session's machine (when granted).
562
632
  - send_to_session \u2014 hand a clear, reformulated instruction to the deep coding agent (when granted); pass wait=true to block for its reply.
633
+ - create_routine / create_loop / create_channel \u2014 set up a scheduled/triggered routine, a self-verifying loop, or an inbound channel for this session (when granted). ALWAYS propose first and confirm before calling these (see below).
563
634
 
564
635
  # Instructions
565
636
  - Answer general questions and questions about yourself directly. Use tools only to act on the machine/session.
566
637
  - Take the cheap path: read state directly; delegate anything LONG to summarize_session \u2014 keep your own context small.
638
+ - To create a routine, loop, or channel: first restate the resolved config in one line and ask the caller to reply "confirm" to proceed. Only call create_routine / create_loop / create_channel after they confirm in a follow-up message. Never create without confirmation.
567
639
  - For destructive actions (deleting, stopping, killing), require a verified caller and confirm intent; for safe reads, just do it.
568
640
  - If a tool fails or returns nothing useful, say so plainly \u2014 never fabricate a result.
569
641
  - Report the outcome in one line.`;
@@ -936,6 +1008,23 @@ function buildSessionDeps(rpc, opts = {}) {
936
1008
  recentMessageCount: messages.length,
937
1009
  latestMessage: latestText
938
1010
  };
1011
+ },
1012
+ async saveRoutine(routine) {
1013
+ return await rpc.saveRoutine(routine, ctx);
1014
+ },
1015
+ async startLoop(cfg) {
1016
+ await rpc.updateConfig({
1017
+ loop: {
1018
+ task: cfg.task,
1019
+ ...cfg.criteria ? { criteria: cfg.criteria } : {},
1020
+ ...cfg.oracle ? { oracle: cfg.oracle } : {},
1021
+ max_iterations: cfg.maxIterations ?? 20,
1022
+ evaluator: true
1023
+ }
1024
+ }, ctx);
1025
+ },
1026
+ async saveChannel(channel) {
1027
+ return await rpc.saveChannel(channel, ctx);
939
1028
  }
940
1029
  };
941
1030
  }
@@ -2077,7 +2166,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2077
2166
  const tunnels = handlers.tunnels;
2078
2167
  if (!tunnels) throw new Error("Tunnel management not available");
2079
2168
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2080
- const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
2169
+ const { FrpcTunnel } = await import('./frpc-cJUGFtWY.mjs');
2081
2170
  const tunnel = new FrpcTunnel({
2082
2171
  name: params.name,
2083
2172
  ports: params.ports,
@@ -2338,7 +2427,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2338
2427
  }
2339
2428
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2340
2429
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2341
- const { toolsForRole } = await import('./sideband-JeID_jF-.mjs');
2430
+ const { toolsForRole } = await import('./sideband-DYhbiCEA.mjs');
2342
2431
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2343
2432
  return fmt(r2);
2344
2433
  }
@@ -2414,7 +2503,15 @@ ${d?.error || "not found"}`;
2414
2503
  trackInbound();
2415
2504
  const rpc = await findChannelOwner(kwargs.channel);
2416
2505
  if (!rpc?.channelSend) return { error: "channel not found" };
2417
- return rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key }, context);
2506
+ return rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key, reply_to: kwargs.reply_to }, context);
2507
+ },
2508
+ // Async reply retrieval for queue-mode channels — long-poll the channel outbox
2509
+ // for replies addressed to the caller. Channel-identity auth (key/from).
2510
+ receive: async (kwargs = {}, context) => {
2511
+ trackInbound();
2512
+ const rpc = await findChannelOwner(kwargs.channel);
2513
+ if (!rpc?.channelReceive) return { error: "channel not found" };
2514
+ return rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
2418
2515
  }
2419
2516
  },
2420
2517
  { overwrite: true }
@@ -2872,10 +2969,27 @@ function generateSkillBody(channel, ctx) {
2872
2969
  const sendUrl = `${gw}/send`;
2873
2970
  const key = ctx?.key || "<your-key>";
2874
2971
  const isAgent = channel.action?.kind === "agent";
2972
+ const isQueue = channel.reply?.mode === "queue";
2973
+ const recvUrl = `${gw}/receive`;
2875
2974
  const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
2876
2975
  const name = channel.skill?.name || channel.name;
2877
2976
  const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
2878
- 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\`.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
2977
+ 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\`).`;
2978
+ const queueSection = isQueue ? `
2979
+
2980
+ ## Getting the reply (async)
2981
+ \`send()\` returns \`{ correlationId }\`. The agent answers later; retrieve replies addressed
2982
+ to you by **long-polling** \`receive\` with a cursor you advance each call:
2983
+ \`\`\`js
2984
+ const { correlationId } = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
2985
+ let cursor = 0;
2986
+ while (true) {
2987
+ const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
2988
+ cursor = r.cursor;
2989
+ for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
2990
+ }
2991
+ \`\`\`
2992
+ **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).` : "";
2879
2993
  const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
2880
2994
  return `---
2881
2995
  name: ${name}
@@ -2903,7 +3017,139 @@ POST ${sendUrl}
2903
3017
  Content-Type: application/json
2904
3018
 
2905
3019
  {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"}}
2906
- \`\`\``;
3020
+ \`\`\`${queueSection}`;
3021
+ }
3022
+
3023
+ const MAX_PER_CHANNEL = 200;
3024
+ const TTL_MS = 60 * 60 * 1e3;
3025
+ class ChannelOutbox {
3026
+ file;
3027
+ byChannel = /* @__PURE__ */ new Map();
3028
+ seqByChannel = /* @__PURE__ */ new Map();
3029
+ emitter = new EventEmitter();
3030
+ constructor(projectDir) {
3031
+ const dir = join$1(projectDir, ".svamp", "channels");
3032
+ this.file = join$1(dir, "_outbox.jsonl");
3033
+ this.emitter.setMaxListeners(0);
3034
+ try {
3035
+ mkdirSync$1(dir, { recursive: true });
3036
+ } catch {
3037
+ }
3038
+ this._load();
3039
+ }
3040
+ _load() {
3041
+ if (!existsSync(this.file)) return;
3042
+ const cutoff = Date.now() - TTL_MS;
3043
+ try {
3044
+ for (const line of readFileSync(this.file, "utf8").split("\n")) {
3045
+ if (!line.trim()) continue;
3046
+ let rec = null;
3047
+ try {
3048
+ rec = JSON.parse(line);
3049
+ } catch {
3050
+ continue;
3051
+ }
3052
+ if (!rec || rec.ts < cutoff) continue;
3053
+ const { channelId, ...reply } = rec;
3054
+ const arr = this.byChannel.get(channelId) || [];
3055
+ arr.push(reply);
3056
+ this.byChannel.set(channelId, arr);
3057
+ this.seqByChannel.set(channelId, Math.max(this.seqByChannel.get(channelId) || 0, reply.seq));
3058
+ }
3059
+ for (const [ch, arr] of this.byChannel) if (arr.length > MAX_PER_CHANNEL) this.byChannel.set(ch, arr.slice(-MAX_PER_CHANNEL));
3060
+ } catch {
3061
+ }
3062
+ }
3063
+ _evict(channelId) {
3064
+ const cutoff = Date.now() - TTL_MS;
3065
+ let arr = this.byChannel.get(channelId);
3066
+ if (!arr) return;
3067
+ if (arr.some((r) => r.ts < cutoff)) arr = arr.filter((r) => r.ts >= cutoff);
3068
+ if (arr.length > MAX_PER_CHANNEL) arr = arr.slice(-MAX_PER_CHANNEL);
3069
+ this.byChannel.set(channelId, arr);
3070
+ }
3071
+ /** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
3072
+ append(channelId, r) {
3073
+ const seq = (this.seqByChannel.get(channelId) || 0) + 1;
3074
+ this.seqByChannel.set(channelId, seq);
3075
+ const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
3076
+ const arr = this.byChannel.get(channelId) || [];
3077
+ arr.push(reply);
3078
+ this.byChannel.set(channelId, arr);
3079
+ this._evict(channelId);
3080
+ try {
3081
+ appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3082
+ } catch {
3083
+ try {
3084
+ mkdirSync$1(join$1(this.file, ".."), { recursive: true });
3085
+ appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3086
+ } catch {
3087
+ }
3088
+ }
3089
+ this.emitter.emit(channelId, reply);
3090
+ return reply;
3091
+ }
3092
+ /** Replies for `to` on `channelId` with seq > cursor (optionally one correlationId). */
3093
+ since(channelId, cursor, to, correlationId) {
3094
+ this._evict(channelId);
3095
+ return (this.byChannel.get(channelId) || []).filter((r) => r.seq > cursor && r.to === to && (!correlationId || r.correlationId === correlationId));
3096
+ }
3097
+ /** Highest seq on a channel (the cursor a caller gets back). */
3098
+ cursor(channelId) {
3099
+ return this.seqByChannel.get(channelId) || 0;
3100
+ }
3101
+ /**
3102
+ * Long-poll: resolve immediately if there are replies after `cursor`, else wait for
3103
+ * the next append (filtered to this channel + `to`) or `timeoutMs`, then return.
3104
+ */
3105
+ wait(channelId, cursor, to, timeoutMs, correlationId) {
3106
+ const ready = this.since(channelId, cursor, to, correlationId);
3107
+ if (ready.length) return Promise.resolve({ replies: ready, cursor: this.cursor(channelId) });
3108
+ return new Promise((resolve) => {
3109
+ const onReply = (r) => {
3110
+ if (r.to !== to || correlationId && r.correlationId !== correlationId) return;
3111
+ cleanup();
3112
+ resolve({ replies: this.since(channelId, cursor, to, correlationId), cursor: this.cursor(channelId) });
3113
+ };
3114
+ const timer = setTimeout(() => {
3115
+ cleanup();
3116
+ resolve({ replies: [], cursor: this.cursor(channelId) });
3117
+ }, Math.max(0, timeoutMs));
3118
+ const cleanup = () => {
3119
+ clearTimeout(timer);
3120
+ this.emitter.off(channelId, onReply);
3121
+ };
3122
+ this.emitter.on(channelId, onReply);
3123
+ });
3124
+ }
3125
+ /** Push subscription for SSE: calls onReply for each new reply addressed to `to`. */
3126
+ subscribe(channelId, to, onReply) {
3127
+ const handler = (r) => {
3128
+ if (r.to === to) onReply(r);
3129
+ };
3130
+ this.emitter.on(channelId, handler);
3131
+ return () => this.emitter.off(channelId, handler);
3132
+ }
3133
+ /** Drop a channel's outbox (on channel delete) — best-effort rewrite of the log. */
3134
+ purge(channelId) {
3135
+ this.byChannel.delete(channelId);
3136
+ this.seqByChannel.delete(channelId);
3137
+ if (!existsSync(this.file)) return;
3138
+ try {
3139
+ const kept = readFileSync(this.file, "utf8").split("\n").filter((l) => {
3140
+ if (!l.trim()) return false;
3141
+ try {
3142
+ return JSON.parse(l).channelId !== channelId;
3143
+ } catch {
3144
+ return false;
3145
+ }
3146
+ });
3147
+ const tmp = this.file + ".tmp";
3148
+ writeFileSync$1(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
3149
+ renameSync$1(tmp, this.file);
3150
+ } catch {
3151
+ }
3152
+ }
2907
3153
  }
2908
3154
 
2909
3155
  function resolveSender(channel, input = {}) {
@@ -3181,6 +3427,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3181
3427
  callbacks.onMetadataUpdate?.(metadata);
3182
3428
  };
3183
3429
  const channelStore = new ChannelStore(initialMetadata.path);
3430
+ const channelOutbox = new ChannelOutbox(initialMetadata.path);
3184
3431
  const cfg = server?.config || {};
3185
3432
  const channelsServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:channels` : void 0;
3186
3433
  const channelsBaseUrl = cfg.public_base_url || process.env.HYPHA_SERVER_URL || "https://hypha.aicell.io";
@@ -3388,6 +3635,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3388
3635
  removeChannel: async (id, context) => {
3389
3636
  authorizeRequest(context, metadata.sharing, "admin");
3390
3637
  const ok = channelStore.remove(id);
3638
+ channelOutbox.purge(id);
3391
3639
  syncChannelsToMetadata();
3392
3640
  return { success: ok };
3393
3641
  },
@@ -3458,6 +3706,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3458
3706
  return { ok: result.status === "completed", call_id: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
3459
3707
  }
3460
3708
  if (c.action?.kind === "loop") return { error: "loop channels are served by the channel server, not channelSend" };
3709
+ const queue = c.reply?.mode === "queue";
3710
+ const replySession = queue ? params.reply_to?.session : void 0;
3461
3711
  const inboxMsg = {
3462
3712
  messageId: callId,
3463
3713
  body: xmlEscape(String(params.message ?? "").slice(0, 16 * 1024)),
@@ -3466,17 +3716,56 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3466
3716
  from: r.sender.name,
3467
3717
  verified: r.sender.verified,
3468
3718
  channel: c.name,
3469
- subject: c.name
3719
+ subject: c.name,
3720
+ ...replySession ? { fromSession: replySession, threadId: callId } : {},
3721
+ ...queue && !replySession ? { channelId: c.id, correlationId: callId } : {}
3470
3722
  };
3471
3723
  await rpcHandlers.sendInboxMessage(inboxMsg, ownerCtx);
3472
3724
  channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "delivered" });
3473
3725
  syncChannelsToMetadata();
3474
- return { ok: true, call_id: callId, status: "accepted" };
3726
+ return { ok: true, call_id: callId, status: queue ? "queued" : "accepted", ...queue ? { correlationId: callId } : {} };
3475
3727
  } catch (e) {
3476
3728
  channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "error" });
3477
3729
  return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
3478
3730
  }
3479
3731
  },
3732
+ // Agent/owner answers a queued (async) channel message → channel outbox, addressed
3733
+ // to the original external caller. Routed here by `inbox reply` for channel-origin
3734
+ // inbox messages (which carry channelId + correlationId + from). Owner-gated since
3735
+ // it's the session speaking on its own behalf.
3736
+ channelReply: async (params, context) => {
3737
+ authorizeRequest(context, metadata.sharing, "interact");
3738
+ const c = channelStore.get(params.channel);
3739
+ if (!c) return { ok: false, error: "channel not found" };
3740
+ if (!params.to || !params.body) return { ok: false, error: "to and body are required" };
3741
+ const reply = channelOutbox.append(c.id, {
3742
+ correlationId: params.correlationId,
3743
+ to: String(params.to),
3744
+ body: String(params.body).slice(0, 16 * 1024)
3745
+ });
3746
+ channelStore.recordCall(c.id, { sender: String(params.to), verified: true, callId: params.correlationId || `seq_${reply.seq}`, outcome: "replied" });
3747
+ return { ok: true, seq: reply.seq };
3748
+ },
3749
+ // External caller retrieves async replies addressed to it. Channel-identity auth
3750
+ // (key/from) — NOT session sharing. Long-polls up to `wait` seconds for new replies
3751
+ // after `cursor`; the caller advances `cursor` each call (ack-by-cursor).
3752
+ channelReceive: async (params, context) => {
3753
+ const c = channelStore.get(params.channel);
3754
+ if (!c || c.enabled === false) return { error: "channel not found" };
3755
+ const u = context?.user;
3756
+ const r = resolveSender(c, {
3757
+ key: params.key,
3758
+ from: params.from,
3759
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
3760
+ hyphaAnonymous: u?.is_anonymous === true,
3761
+ hyphaWorkspace: u?.scope?.current_workspace
3762
+ });
3763
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3764
+ const cursor = Math.max(0, Number(params.cursor) || 0);
3765
+ const waitMs = Math.min(Math.max(0, Number(params.wait ?? 25) * 1e3), 6e4);
3766
+ const out = await channelOutbox.wait(c.id, cursor, r.sender.name, waitMs, params.correlationId);
3767
+ return { ok: true, replies: out.replies, cursor: out.cursor };
3768
+ },
3480
3769
  // ── Agent State ──
3481
3770
  getAgentState: async (context) => {
3482
3771
  authorizeRequest(context, metadata.sharing, "view");
@@ -3821,6 +4110,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3821
4110
  const child = spawn("claude", args, {
3822
4111
  cwd,
3823
4112
  timeout: 6e4,
4113
+ // Ignore stdin: --print otherwise waits ~3s for piped input ("no stdin
4114
+ // data received in 3s, proceeding without it") — pure latency per /btw.
4115
+ stdio: ["ignore", "pipe", "pipe"],
3824
4116
  env: { ...process.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" }
3825
4117
  });
3826
4118
  let stdout = "";
@@ -9834,7 +10126,7 @@ async function startDaemon(options) {
9834
10126
  const list = loadExposedTunnels().filter((t) => t.name !== name);
9835
10127
  saveExposedTunnels(list);
9836
10128
  }
9837
- const { ServeManager } = await import('./serveManager-lmPtmRnR.mjs');
10129
+ const { ServeManager } = await import('./serveManager-B757hHGd.mjs');
9838
10130
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
9839
10131
  ensureAutoInstalledSkills(logger).catch(() => {
9840
10132
  });
@@ -10494,6 +10786,10 @@ async function startDaemon(options) {
10494
10786
  }
10495
10787
  if (msg.session_id) {
10496
10788
  claudeResumeId = msg.session_id;
10789
+ if (sessionMetadata.claudeSessionId !== msg.session_id) {
10790
+ sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
10791
+ sessionService.updateMetadata(sessionMetadata);
10792
+ }
10497
10793
  }
10498
10794
  signalProcessing(false);
10499
10795
  sessionWasProcessing = false;
@@ -12124,7 +12420,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12124
12420
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
12125
12421
  if (channelHttpPort > 0) {
12126
12422
  try {
12127
- const { createChannelHttpServer } = await import('./httpServer-wwHHk1EM.mjs');
12423
+ const { createChannelHttpServer } = await import('./httpServer-D9qLS8ed.mjs');
12128
12424
  const channelHttpServer = createChannelHttpServer({
12129
12425
  getSessionIds: () => {
12130
12426
  const ids = [];
@@ -12145,7 +12441,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12145
12441
  const specs = loadExposedTunnels();
12146
12442
  if (specs.length === 0) return;
12147
12443
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12148
- const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
12444
+ const { FrpcTunnel } = await import('./frpc-cJUGFtWY.mjs');
12149
12445
  for (const spec of specs) {
12150
12446
  if (tunnels.has(spec.name)) continue;
12151
12447
  try {
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
5
- import { c as connectToHypha, a as createSessionStore, r as registerMachineService, P as generateHookSettings } from './run-CdtYIBbd.mjs';
5
+ import { c as connectToHypha, a as createSessionStore, r as registerMachineService, P as generateHookSettings } from './run-B_FyvS11.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -14,6 +14,7 @@ import 'url';
14
14
  import 'child_process';
15
15
  import 'crypto';
16
16
  import 'util';
17
+ import 'node:events';
17
18
  import '@agentclientprotocol/sdk';
18
19
  import '@modelcontextprotocol/sdk/client/index.js';
19
20
  import '@modelcontextprotocol/sdk/client/stdio.js';
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-rSREfaQg.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-DpRXzSr9.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-rSREfaQg.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-DpRXzSr9.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-rSREfaQg.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-DpRXzSr9.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-rSREfaQg.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-DpRXzSr9.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-rSREfaQg.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-DpRXzSr9.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-CdtYIBbd.mjs';
7
+ import { k as getHyphaServerUrl, S as ServeAuth, l as hasCookieToken } from './run-B_FyvS11.mjs';
8
8
  import 'os';
9
9
  import 'fs/promises';
10
10
  import 'url';
@@ -14,6 +14,7 @@ import 'util';
14
14
  import 'node:crypto';
15
15
  import 'node:path';
16
16
  import 'node:os';
17
+ import 'node:events';
17
18
  import '@agentclientprotocol/sdk';
18
19
  import '@modelcontextprotocol/sdk/client/index.js';
19
20
  import '@modelcontextprotocol/sdk/client/stdio.js';
@@ -712,7 +713,7 @@ class ServeManager {
712
713
  const mount = this.mounts.get(mountName);
713
714
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, `static-${subdomainSafe}-${mount.linkToken}`]]) : void 0;
714
715
  try {
715
- const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
716
+ const { FrpcTunnel } = await import('./frpc-cJUGFtWY.mjs');
716
717
  let tunnel;
717
718
  tunnel = new FrpcTunnel({
718
719
  name: tunnelName,