svamp-cli 0.2.101 → 0.2.103

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 { E as resolveModel, N as describeMisconfiguration, O as buildMachineDeps } from './run-CsMTSngP.mjs';
2
- import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-C10Ni7p_.mjs';
1
+ import { E as resolveModel, N as describeMisconfiguration, O as buildMachineDeps } from './run-BmZjAEob.mjs';
2
+ import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-CNyGVxRy.mjs';
3
3
  import { WebSocket } from 'ws';
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import 'os';
@@ -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';
@@ -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-CsMTSngP.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-BmZjAEob.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.101";
2
+ var version = "0.2.103";
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';
@@ -2165,7 +2166,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2165
2166
  const tunnels = handlers.tunnels;
2166
2167
  if (!tunnels) throw new Error("Tunnel management not available");
2167
2168
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2168
- const { FrpcTunnel } = await import('./frpc-WVnBbyjf.mjs');
2169
+ const { FrpcTunnel } = await import('./frpc-Dn5pmk_f.mjs');
2169
2170
  const tunnel = new FrpcTunnel({
2170
2171
  name: params.name,
2171
2172
  ports: params.ports,
@@ -2426,7 +2427,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2426
2427
  }
2427
2428
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2428
2429
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2429
- const { toolsForRole } = await import('./sideband-C10Ni7p_.mjs');
2430
+ const { toolsForRole } = await import('./sideband-CNyGVxRy.mjs');
2430
2431
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2431
2432
  return fmt(r2);
2432
2433
  }
@@ -2502,7 +2503,15 @@ ${d?.error || "not found"}`;
2502
2503
  trackInbound();
2503
2504
  const rpc = await findChannelOwner(kwargs.channel);
2504
2505
  if (!rpc?.channelSend) return { error: "channel not found" };
2505
- 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);
2506
2515
  }
2507
2516
  },
2508
2517
  { overwrite: true }
@@ -2960,10 +2969,27 @@ function generateSkillBody(channel, ctx) {
2960
2969
  const sendUrl = `${gw}/send`;
2961
2970
  const key = ctx?.key || "<your-key>";
2962
2971
  const isAgent = channel.action?.kind === "agent";
2972
+ const isQueue = channel.reply?.mode === "queue";
2973
+ const recvUrl = `${gw}/receive`;
2963
2974
  const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
2964
2975
  const name = channel.skill?.name || channel.name;
2965
2976
  const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
2966
- 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).` : "";
2967
2993
  const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
2968
2994
  return `---
2969
2995
  name: ${name}
@@ -2991,7 +3017,139 @@ POST ${sendUrl}
2991
3017
  Content-Type: application/json
2992
3018
 
2993
3019
  {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"}}
2994
- \`\`\``;
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
+ }
2995
3153
  }
2996
3154
 
2997
3155
  function resolveSender(channel, input = {}) {
@@ -3269,6 +3427,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3269
3427
  callbacks.onMetadataUpdate?.(metadata);
3270
3428
  };
3271
3429
  const channelStore = new ChannelStore(initialMetadata.path);
3430
+ const channelOutbox = new ChannelOutbox(initialMetadata.path);
3272
3431
  const cfg = server?.config || {};
3273
3432
  const channelsServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:channels` : void 0;
3274
3433
  const channelsBaseUrl = cfg.public_base_url || process.env.HYPHA_SERVER_URL || "https://hypha.aicell.io";
@@ -3476,6 +3635,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3476
3635
  removeChannel: async (id, context) => {
3477
3636
  authorizeRequest(context, metadata.sharing, "admin");
3478
3637
  const ok = channelStore.remove(id);
3638
+ channelOutbox.purge(id);
3479
3639
  syncChannelsToMetadata();
3480
3640
  return { success: ok };
3481
3641
  },
@@ -3546,6 +3706,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3546
3706
  return { ok: result.status === "completed", call_id: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
3547
3707
  }
3548
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;
3549
3711
  const inboxMsg = {
3550
3712
  messageId: callId,
3551
3713
  body: xmlEscape(String(params.message ?? "").slice(0, 16 * 1024)),
@@ -3554,17 +3716,56 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3554
3716
  from: r.sender.name,
3555
3717
  verified: r.sender.verified,
3556
3718
  channel: c.name,
3557
- subject: c.name
3719
+ subject: c.name,
3720
+ ...replySession ? { fromSession: replySession, threadId: callId } : {},
3721
+ ...queue && !replySession ? { channelId: c.id, correlationId: callId } : {}
3558
3722
  };
3559
3723
  await rpcHandlers.sendInboxMessage(inboxMsg, ownerCtx);
3560
3724
  channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "delivered" });
3561
3725
  syncChannelsToMetadata();
3562
- return { ok: true, call_id: callId, status: "accepted" };
3726
+ return { ok: true, call_id: callId, status: queue ? "queued" : "accepted", ...queue ? { correlationId: callId } : {} };
3563
3727
  } catch (e) {
3564
3728
  channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "error" });
3565
3729
  return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
3566
3730
  }
3567
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
+ },
3568
3769
  // ── Agent State ──
3569
3770
  getAgentState: async (context) => {
3570
3771
  authorizeRequest(context, metadata.sharing, "view");
@@ -3909,6 +4110,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3909
4110
  const child = spawn("claude", args, {
3910
4111
  cwd,
3911
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"],
3912
4116
  env: { ...process.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" }
3913
4117
  });
3914
4118
  let stdout = "";
@@ -9342,6 +9546,17 @@ function isLoopActive(directory) {
9342
9546
  const s = readLoopState(directory);
9343
9547
  return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
9344
9548
  }
9549
+ function loopOwnerSession(directory) {
9550
+ const s = readLoopState(directory);
9551
+ if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return null;
9552
+ return typeof s.session_id === "string" ? s.session_id : null;
9553
+ }
9554
+ function isLoopActiveForSession(directory, sessionId) {
9555
+ const s = readLoopState(directory);
9556
+ if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return false;
9557
+ if (typeof s.session_id !== "string") return true;
9558
+ return s.session_id === sessionId;
9559
+ }
9345
9560
  function resolveLoopInit() {
9346
9561
  const candidates = [
9347
9562
  join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
@@ -9359,6 +9574,7 @@ function initLoop(directory, cfg) {
9359
9574
  if (typeof cfg.maxIterations === "number") args.push("--max", String(cfg.maxIterations));
9360
9575
  args.push("--evaluator", cfg.evaluator === false ? "off" : "on");
9361
9576
  if (cfg.model) args.push("--model", cfg.model);
9577
+ if (cfg.sessionId) args.push("--session", cfg.sessionId);
9362
9578
  const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
9363
9579
  return res.status === 0;
9364
9580
  }
@@ -9460,7 +9676,8 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9460
9676
  criteria: typeof lp.criteria === "string" && lp.criteria.trim() ? lp.criteria.trim() : void 0,
9461
9677
  oracle,
9462
9678
  maxIterations,
9463
- evaluator
9679
+ evaluator,
9680
+ sessionId
9464
9681
  });
9465
9682
  if (ok) {
9466
9683
  const existingQueue = getMetadata().messageQueue || [];
@@ -9922,7 +10139,7 @@ async function startDaemon(options) {
9922
10139
  const list = loadExposedTunnels().filter((t) => t.name !== name);
9923
10140
  saveExposedTunnels(list);
9924
10141
  }
9925
- const { ServeManager } = await import('./serveManager-DfETVSOb.mjs');
10142
+ const { ServeManager } = await import('./serveManager-D6lGn8jh.mjs');
9926
10143
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
9927
10144
  ensureAutoInstalledSkills(logger).catch(() => {
9928
10145
  });
@@ -10034,7 +10251,7 @@ async function startDaemon(options) {
10034
10251
  }
10035
10252
  }, shouldAutoAllow2 = function(toolName, toolInput) {
10036
10253
  if (toolName === "AskUserQuestion") {
10037
- return isLoopActive(directory);
10254
+ return isLoopActiveForSession(directory, sessionId);
10038
10255
  }
10039
10256
  if (toolName === "Bash") {
10040
10257
  const inputObj = toolInput;
@@ -10047,7 +10264,7 @@ async function startDaemon(options) {
10047
10264
  } else if (allowedTools.has(toolName)) {
10048
10265
  return true;
10049
10266
  }
10050
- if (isLoopActive(directory)) return true;
10267
+ if (isLoopActiveForSession(directory, sessionId)) return true;
10051
10268
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
10052
10269
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
10053
10270
  return false;
@@ -10412,6 +10629,12 @@ async function startDaemon(options) {
10412
10629
  logger.log(`[Session ${sessionId}] Permission request: ${requestId} (corr=${correlationId}) tool=${toolName}`);
10413
10630
  if (shouldAutoAllow2(toolName, toolInput)) {
10414
10631
  logger.log(`[Session ${sessionId}] Auto-allowing ${toolName} (mode=${currentPermissionMode})`);
10632
+ if (toolName === "AskUserQuestion") {
10633
+ sessionService.pushMessage(
10634
+ { type: "message", message: "\u{1F501} Question auto-dismissed \u2014 a loop is running, so there is no human to prompt. The agent will proceed as if the questions were skipped.", level: "warning" },
10635
+ "event"
10636
+ );
10637
+ }
10415
10638
  if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
10416
10639
  const controlResponse = JSON.stringify({
10417
10640
  type: "control_response",
@@ -10582,6 +10805,10 @@ async function startDaemon(options) {
10582
10805
  }
10583
10806
  if (msg.session_id) {
10584
10807
  claudeResumeId = msg.session_id;
10808
+ if (sessionMetadata.claudeSessionId !== msg.session_id) {
10809
+ sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
10810
+ sessionService.updateMetadata(sessionMetadata);
10811
+ }
10585
10812
  }
10586
10813
  signalProcessing(false);
10587
10814
  sessionWasProcessing = false;
@@ -11485,7 +11712,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11485
11712
  } else if (allowedTools.has(toolName)) {
11486
11713
  return true;
11487
11714
  }
11488
- if (isLoopActive(directory)) return true;
11715
+ if (isLoopActiveForSession(directory, sessionId)) return true;
11489
11716
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
11490
11717
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
11491
11718
  return false;
@@ -12212,7 +12439,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12212
12439
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
12213
12440
  if (channelHttpPort > 0) {
12214
12441
  try {
12215
- const { createChannelHttpServer } = await import('./httpServer-wwHHk1EM.mjs');
12442
+ const { createChannelHttpServer } = await import('./httpServer-D9qLS8ed.mjs');
12216
12443
  const channelHttpServer = createChannelHttpServer({
12217
12444
  getSessionIds: () => {
12218
12445
  const ids = [];
@@ -12233,7 +12460,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12233
12460
  const specs = loadExposedTunnels();
12234
12461
  if (specs.length === 0) return;
12235
12462
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12236
- const { FrpcTunnel } = await import('./frpc-WVnBbyjf.mjs');
12463
+ const { FrpcTunnel } = await import('./frpc-Dn5pmk_f.mjs');
12237
12464
  for (const spec of specs) {
12238
12465
  if (tunnels.has(spec.name)) continue;
12239
12466
  try {
@@ -12315,6 +12542,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12315
12542
  }
12316
12543
  const sessionsToAutoContinue = [];
12317
12544
  const sessionsToLoopResume = [];
12545
+ {
12546
+ const knownSessionIds = new Set(persistedSessions.map((p) => p.sessionId));
12547
+ const sweptDirs = /* @__PURE__ */ new Set();
12548
+ for (const p of persistedSessions) {
12549
+ if (sweptDirs.has(p.directory)) continue;
12550
+ sweptDirs.add(p.directory);
12551
+ const owner = loopOwnerSession(p.directory);
12552
+ if (owner && !knownSessionIds.has(owner)) {
12553
+ deactivateLoop(p.directory);
12554
+ logger.log(`[loop] Deactivated stale loop-state in ${p.directory} (owner session ${owner} no longer known)`);
12555
+ }
12556
+ }
12557
+ }
12318
12558
  if (persistedSessions.length > 0) {
12319
12559
  logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
12320
12560
  for (const persisted of persistedSessions) {
@@ -12359,7 +12599,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12359
12599
  if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
12360
12600
  sessionsToAutoContinue.push(persisted.sessionId);
12361
12601
  }
12362
- if (!isOrphaned && !persisted.wasProcessing && isLoopActive(persisted.directory)) {
12602
+ if (!isOrphaned && !persisted.wasProcessing && isLoopActiveForSession(persisted.directory, persisted.sessionId)) {
12363
12603
  sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
12364
12604
  }
12365
12605
  } else {
@@ -12406,7 +12646,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12406
12646
  }
12407
12647
  setTimeout(async () => {
12408
12648
  try {
12409
- if (!isLoopActive(sessDir)) return;
12649
+ if (!isLoopActiveForSession(sessDir, sessionId)) return;
12410
12650
  const prompt = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
12411
12651
  await rpc.sendMessage(
12412
12652
  JSON.stringify({
@@ -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-CsMTSngP.mjs';
5
+ import { c as connectToHypha, a as createSessionStore, r as registerMachineService, P as generateHookSettings } from './run-BmZjAEob.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-CdxEOPUt.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-C_DlMpl7.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-CdxEOPUt.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-C_DlMpl7.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-CdxEOPUt.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-C_DlMpl7.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-CdxEOPUt.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-C_DlMpl7.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-CdxEOPUt.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-C_DlMpl7.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();