svamp-cli 0.2.102 → 0.2.104

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,15 +1,15 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir as homedir$1 } from 'os';
2
2
  import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
3
- import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
- import path__default, { join, dirname, basename, resolve } from 'path';
3
+ import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
+ import path__default, { join as join$1, dirname, basename, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
- import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
8
+ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, renameSync, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
9
  import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
10
10
  import { promisify } from 'util';
11
+ import { join } from 'node:path';
11
12
  import { randomBytes, randomUUID, createHash } from 'node:crypto';
12
- import { join as join$1 } from 'node:path';
13
13
  import os, { homedir, platform } from 'node:os';
14
14
  import { EventEmitter } from 'node:events';
15
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
@@ -1121,6 +1121,362 @@ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
1121
1121
  }
1122
1122
  }
1123
1123
 
1124
+ const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
1125
+ function parseField(token, [min, max]) {
1126
+ const set = /* @__PURE__ */ new Set();
1127
+ for (const part of token.split(",")) {
1128
+ let m;
1129
+ if (part === "*") {
1130
+ for (let i = min; i <= max; i++) set.add(i);
1131
+ } else if (m = part.match(/^\*\/(\d+)$/)) {
1132
+ const s = +m[1];
1133
+ for (let i = min; i <= max; i += s) set.add(i);
1134
+ } else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
1135
+ for (let i = +m[1]; i <= +m[2]; i += +m[3]) set.add(i);
1136
+ } else if (m = part.match(/^(\d+)-(\d+)$/)) {
1137
+ for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
1138
+ } else if (m = part.match(/^(\d+)$/)) {
1139
+ set.add(+m[1]);
1140
+ } else throw new Error(`invalid cron field: "${token}"`);
1141
+ }
1142
+ for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
1143
+ return set;
1144
+ }
1145
+ function parseCron(expr) {
1146
+ const fields = String(expr).trim().split(/\s+/);
1147
+ if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
1148
+ const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
1149
+ return { minute, hour, dom, month, dow, domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
1150
+ }
1151
+ function cronMatches(expr, date) {
1152
+ const c = typeof expr === "string" ? parseCron(expr) : expr;
1153
+ if (!c.minute.has(date.getMinutes())) return false;
1154
+ if (!c.hour.has(date.getHours())) return false;
1155
+ if (!c.month.has(date.getMonth() + 1)) return false;
1156
+ const domOk = c.dom.has(date.getDate());
1157
+ const dowOk = c.dow.has(date.getDay());
1158
+ if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
1159
+ return domOk && dowOk;
1160
+ }
1161
+ function inZone(date, tz) {
1162
+ if (!tz) return date;
1163
+ try {
1164
+ const p = new Intl.DateTimeFormat("en-US", {
1165
+ timeZone: tz,
1166
+ hour12: false,
1167
+ year: "numeric",
1168
+ month: "2-digit",
1169
+ day: "2-digit",
1170
+ hour: "2-digit",
1171
+ minute: "2-digit"
1172
+ }).formatToParts(date).reduce((o, x) => {
1173
+ o[x.type] = x.value;
1174
+ return o;
1175
+ }, {});
1176
+ return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
1177
+ } catch {
1178
+ return date;
1179
+ }
1180
+ }
1181
+ function nextFire(expr, from, tz) {
1182
+ const c = parseCron(expr);
1183
+ const d = new Date(from.getTime());
1184
+ d.setSeconds(0, 0);
1185
+ d.setMinutes(d.getMinutes() + 1);
1186
+ for (let i = 0; i < 366 * 24 * 60; i++) {
1187
+ if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
1188
+ d.setMinutes(d.getMinutes() + 1);
1189
+ }
1190
+ return null;
1191
+ }
1192
+ function resolvePath(ctx, path) {
1193
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
1194
+ }
1195
+ function renderTemplate(template, ctx) {
1196
+ return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
1197
+ const v = resolvePath(ctx, p);
1198
+ return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
1199
+ });
1200
+ }
1201
+ const TRIGGER_TYPES = ["manual", "schedule", "webhook", "api"];
1202
+ const ACTION_KINDS = ["message", "loop"];
1203
+ const OVERLAP = ["queue", "skip", "replace"];
1204
+ function validateRoutine(r) {
1205
+ const errs = [];
1206
+ if (!r || typeof r !== "object") return ["routine must be an object"];
1207
+ if (!r.session_id) errs.push("session_id required");
1208
+ if (!r.name) errs.push("name required");
1209
+ const t = r.trigger;
1210
+ if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join("|")}`);
1211
+ if (t?.type === "schedule") {
1212
+ try {
1213
+ parseCron(t.cron);
1214
+ } catch (e) {
1215
+ errs.push(`trigger.cron: ${e.message}`);
1216
+ }
1217
+ if (t.missed && !["catchup", "skip"].includes(t.missed)) errs.push("trigger.missed must be catchup|skip");
1218
+ }
1219
+ const a = r.action;
1220
+ if (!a || !ACTION_KINDS.includes(a.kind)) errs.push(`action.kind must be one of ${ACTION_KINDS.join("|")}`);
1221
+ if (a?.kind === "message" && !a.template) errs.push("action.template required for message action");
1222
+ if (a?.kind === "loop" && !a.loop && !a.task_template) errs.push("action.loop or action.task_template required for loop action");
1223
+ if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join("|")}`);
1224
+ if ((t?.type === "webhook" || t?.type === "api") && t.public && a?.kind === "loop")
1225
+ errs.push("a public webhook/api may not use a loop action (unauthenticated task injection) \u2014 use a message action or require a key");
1226
+ return errs;
1227
+ }
1228
+
1229
+ const genId$1 = () => "c_" + randomBytes(5).toString("hex");
1230
+ const genKey$1 = () => "ck_" + randomBytes(18).toString("base64url");
1231
+ const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
1232
+ \${body.message}
1233
+ </inbound-message>`;
1234
+ function validateChannel(c) {
1235
+ const errs = [];
1236
+ if (!c || typeof c !== "object") return ["channel must be an object"];
1237
+ if (!c.name) errs.push("name required");
1238
+ const m = c.identity?.mode;
1239
+ if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
1240
+ if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
1241
+ if (!["message", "loop", "agent"].includes(c.action?.kind)) errs.push("action.kind must be message|loop|agent");
1242
+ const b = c.bind;
1243
+ const bindOk = b === "dynamic" || b === "stateless" || b && typeof b.session === "string" && !!b.session;
1244
+ if (!bindOk) errs.push('bind must be "dynamic", "stateless", or { session }');
1245
+ if (b === "stateless" && c.reply?.mode === "queue")
1246
+ errs.push('a stateless channel cannot use reply.mode "queue" (no persistent session to answer later)');
1247
+ if (b === "stateless" && (c.action?.kind === "loop" || c.action?.kind === "agent"))
1248
+ errs.push(`a stateless channel cannot use a ${c.action?.kind} action (needs a live session); use dynamic or { session }`);
1249
+ if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
1250
+ errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
1251
+ if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
1252
+ const MUTATING = ["run_bash", "send_to_session"];
1253
+ const ag = c.action.agent || {};
1254
+ const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
1255
+ if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
1256
+ }
1257
+ const unsafe = /[<>"'&\r\n]/;
1258
+ if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
1259
+ if (c.description && unsafe.test(c.description)) errs.push(`description must be single-line and not contain < > " ' &`);
1260
+ if (c.skill?.name && unsafe.test(c.skill.name)) errs.push(`skill.name must be single-line and not contain < > " ' &`);
1261
+ if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
1262
+ if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
1263
+ for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
1264
+ return errs;
1265
+ }
1266
+ function normalizeBind(c) {
1267
+ const b = c.bind;
1268
+ if (b === "dynamic" || b === "stateless" || b && typeof b.session === "string" && b.session) return c;
1269
+ c.bind = "dynamic";
1270
+ return c;
1271
+ }
1272
+ function bindMode(c) {
1273
+ const b = normalizeBind(c).bind;
1274
+ if (b === "stateless") return "stateless";
1275
+ if (b && typeof b.session === "string") return "fixed";
1276
+ return "dynamic";
1277
+ }
1278
+ function fixedSessionId(c) {
1279
+ const b = c.bind;
1280
+ return b && typeof b.session === "string" ? b.session : void 0;
1281
+ }
1282
+ class ChannelStore {
1283
+ dir;
1284
+ constructor(projectDir) {
1285
+ this.dir = join(projectDir, ".svamp", "channels");
1286
+ try {
1287
+ mkdirSync(this.dir, { recursive: true });
1288
+ } catch {
1289
+ }
1290
+ }
1291
+ _path(id) {
1292
+ return join(this.dir, `${id}.json`);
1293
+ }
1294
+ list() {
1295
+ if (!existsSync(this.dir)) return [];
1296
+ return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
1297
+ try {
1298
+ return normalizeBind(JSON.parse(readFileSync(join(this.dir, f), "utf8")));
1299
+ } catch {
1300
+ return null;
1301
+ }
1302
+ }).filter((c) => !!c);
1303
+ }
1304
+ get(id) {
1305
+ try {
1306
+ return normalizeBind(JSON.parse(readFileSync(this._path(id), "utf8")));
1307
+ } catch {
1308
+ return null;
1309
+ }
1310
+ }
1311
+ save(channel) {
1312
+ const c = { enabled: true, bind: "dynamic", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
1313
+ if (!c.id) c.id = genId$1();
1314
+ const errs = validateChannel(c);
1315
+ if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
1316
+ mkdirSync(this.dir, { recursive: true });
1317
+ const tmp = this._path(c.id) + ".tmp";
1318
+ writeFileSync(tmp, JSON.stringify(c, null, 2));
1319
+ renameSync(tmp, this._path(c.id));
1320
+ return c;
1321
+ }
1322
+ remove(id) {
1323
+ const p = this._path(id);
1324
+ if (existsSync(p)) {
1325
+ rmSync(p);
1326
+ return true;
1327
+ }
1328
+ return false;
1329
+ }
1330
+ setEnabled(id, enabled) {
1331
+ const c = this.get(id);
1332
+ if (!c) return null;
1333
+ c.enabled = enabled;
1334
+ return this.save(c);
1335
+ }
1336
+ recordCall(id, entry) {
1337
+ const c = this.get(id);
1338
+ if (!c) return;
1339
+ c.last_calls = c.last_calls || [];
1340
+ c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
1341
+ c.last_calls = c.last_calls.slice(0, 20);
1342
+ this.save(c);
1343
+ }
1344
+ addCaller(id, name, kind = "agent") {
1345
+ const c = this.get(id);
1346
+ if (!c) return null;
1347
+ c.identity.callers = c.identity.callers || [];
1348
+ const caller = { name, kind, key: genKey$1() };
1349
+ c.identity.callers.push(caller);
1350
+ this.save(c);
1351
+ return caller;
1352
+ }
1353
+ }
1354
+ function routingSession(channel, ctx) {
1355
+ const mode = bindMode(channel);
1356
+ if (mode === "fixed") return fixedSessionId(channel);
1357
+ if (mode === "dynamic") return ctx?.session;
1358
+ return void 0;
1359
+ }
1360
+ function gatewayBase(channelsServiceId, baseUrl) {
1361
+ const slash = channelsServiceId.indexOf("/");
1362
+ if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
1363
+ const ws = channelsServiceId.slice(0, slash);
1364
+ const clientSvc = channelsServiceId.slice(slash + 1);
1365
+ return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
1366
+ }
1367
+ function generateSkillBody(channel, ctx) {
1368
+ const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
1369
+ const base = ctx?.baseUrl || "https://hypha.aicell.io";
1370
+ const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
1371
+ const skillUrl = `${gw}/skill?channel=${channel.id}`;
1372
+ const sendUrl = `${gw}/send`;
1373
+ const key = ctx?.key || "<your-key>";
1374
+ const isAgent = channel.action?.kind === "agent";
1375
+ const isQueue = channel.reply?.mode === "queue";
1376
+ const recvUrl = `${gw}/receive`;
1377
+ const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
1378
+ const mode = bindMode(channel);
1379
+ const rSession = routingSession(channel, ctx);
1380
+ const sessionLineJs = rSession ? `
1381
+ session: "${rSession}",` : "";
1382
+ const sessionKv = rSession ? `, "session": "${rSession}"` : "";
1383
+ 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.`;
1384
+ const name = channel.skill?.name || channel.name;
1385
+ const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
1386
+ 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\`).`;
1387
+ const queueSection = isQueue ? `
1388
+
1389
+ ## Getting the reply (async)
1390
+ \`send()\` returns \`{ correlationId }\`. The agent answers later; retrieve replies addressed
1391
+ to you by **long-polling** \`receive\` with a cursor you advance each call:
1392
+ \`\`\`js
1393
+ const { correlationId } = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
1394
+ let cursor = 0;
1395
+ while (true) {
1396
+ const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
1397
+ cursor = r.cursor;
1398
+ for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
1399
+ }
1400
+ \`\`\`
1401
+ **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).` : "";
1402
+ const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
1403
+ return `---
1404
+ name: ${name}
1405
+ description: ${desc}
1406
+ ---
1407
+ # ${name}
1408
+ ${channel.description || ""}
1409
+
1410
+ Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
1411
+ ${bindNote}
1412
+ This skill (with every value below already filled in) is served at:
1413
+ ${skillUrl}
1414
+
1415
+ ${rpcLine}
1416
+ \`\`\`js
1417
+ await get_service("${svc}").send({
1418
+ channel: "${channel.id}",
1419
+ message: "your message here",
1420
+ from: "your-name",${sessionLineJs}
1421
+ });
1422
+ \`\`\`
1423
+
1424
+ **HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
1425
+ \`\`\`
1426
+ POST ${sendUrl}
1427
+ Content-Type: application/json
1428
+
1429
+ {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"${sessionKv}}}
1430
+ \`\`\`${queueSection}`;
1431
+ }
1432
+
1433
+ function resolveSender(channel, input = {}) {
1434
+ const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
1435
+ const id = channel.identity || {};
1436
+ if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
1437
+ if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
1438
+ return { sender: { name: hyphaUser, kind: "agent", verified: true } };
1439
+ return { error: "caller not in hypha_allow" };
1440
+ }
1441
+ if (id.mode === "fixed") {
1442
+ if (!id.fixed?.name) return { error: "fixed identity not configured" };
1443
+ return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
1444
+ }
1445
+ if (id.mode === "per-key") {
1446
+ const caller = (id.callers || []).find((c) => c.key && c.key === key);
1447
+ if (!caller) return { error: "invalid or missing key" };
1448
+ return { sender: { name: caller.name, kind: caller.kind, verified: true } };
1449
+ }
1450
+ if (id.mode === "caller-supplied") {
1451
+ if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
1452
+ return { sender: { name: from || "anonymous", kind: "user", verified: false } };
1453
+ }
1454
+ return { error: "unsupported identity mode" };
1455
+ }
1456
+ const MAX_BODY = 16 * 1024;
1457
+ function xmlEscape(s) {
1458
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1459
+ }
1460
+ const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
1461
+ function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
1462
+ const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
1463
+ const escVal = (v) => xmlEscape(obj(v));
1464
+ const escAttr = (v) => xmlEscape(stripControl(obj(v)));
1465
+ const bodyEsc = {};
1466
+ for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
1467
+ const queryEsc = {};
1468
+ for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
1469
+ const ctx = {
1470
+ sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
1471
+ body: bodyEsc,
1472
+ query: queryEsc,
1473
+ channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
1474
+ call: { id: escAttr(callId) },
1475
+ now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
1476
+ };
1477
+ return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
1478
+ }
1479
+
1124
1480
  function getParamNames(fn) {
1125
1481
  const src = fn.toString();
1126
1482
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -1151,7 +1507,7 @@ function filterTerminalResponses(data) {
1151
1507
  return filtered;
1152
1508
  }
1153
1509
  function getMachineMetadataPath(svampHomeDir) {
1154
- return join(svampHomeDir, "machine-metadata.json");
1510
+ return join$1(svampHomeDir, "machine-metadata.json");
1155
1511
  }
1156
1512
  async function mintRealtimeEphemeralKey(baseUrl, apiKey, opts) {
1157
1513
  const realtimeBase = baseUrl || "https://api.openai.com";
@@ -1212,11 +1568,11 @@ function loadPersistedMachineMetadata(svampHomeDir) {
1212
1568
  }
1213
1569
  function savePersistedMachineMetadata(svampHomeDir, data) {
1214
1570
  try {
1215
- mkdirSync(svampHomeDir, { recursive: true });
1571
+ mkdirSync$1(svampHomeDir, { recursive: true });
1216
1572
  const filePath = getMachineMetadataPath(svampHomeDir);
1217
1573
  const tmpPath = filePath + ".tmp";
1218
- writeFileSync(tmpPath, JSON.stringify(data, null, 2));
1219
- renameSync(tmpPath, filePath);
1574
+ writeFileSync$1(tmpPath, JSON.stringify(data, null, 2));
1575
+ renameSync$1(tmpPath, filePath);
1220
1576
  } catch (err) {
1221
1577
  console.error("[HYPHA MACHINE] Failed to persist machine metadata:", err);
1222
1578
  }
@@ -1663,11 +2019,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1663
2019
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
1664
2020
  newSharing = { ...newSharing, owner: context.user.email };
1665
2021
  }
1666
- const ownerEmail = newSharing.owner || context?.user?.email || "";
2022
+ const ownerEmail2 = newSharing.owner || context?.user?.email || "";
1667
2023
  newSharing = {
1668
2024
  ...newSharing,
1669
2025
  allowedUsers: (newSharing.allowedUsers || []).map(
1670
- (u) => normalizeAllowedUser(u, ownerEmail)
2026
+ (u) => normalizeAllowedUser(u, ownerEmail2)
1671
2027
  ),
1672
2028
  publicAccess: null
1673
2029
  };
@@ -2166,7 +2522,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2166
2522
  const tunnels = handlers.tunnels;
2167
2523
  if (!tunnels) throw new Error("Tunnel management not available");
2168
2524
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2169
- const { FrpcTunnel } = await import('./frpc-cJUGFtWY.mjs');
2525
+ const { FrpcTunnel } = await import('./frpc-B8ORdlOO.mjs');
2170
2526
  const tunnel = new FrpcTunnel({
2171
2527
  name: params.name,
2172
2528
  ports: params.ports,
@@ -2222,9 +2578,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2222
2578
  authorizeRequest(context, currentMetadata.sharing, "interact");
2223
2579
  const sm = handlers.serveManager;
2224
2580
  if (!sm) throw new Error("Serve manager not available");
2225
- const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
2581
+ const ownerEmail2 = params.ownerEmail || context?.user?.email || void 0;
2226
2582
  const access = params.access || "owner";
2227
- return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail);
2583
+ return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
2228
2584
  },
2229
2585
  /**
2230
2586
  * Apply a mount declaratively. Replaces existing mount with same name.
@@ -2235,7 +2591,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2235
2591
  authorizeRequest(context, currentMetadata.sharing, "interact");
2236
2592
  const sm = handlers.serveManager;
2237
2593
  if (!sm) throw new Error("Serve manager not available");
2238
- const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
2594
+ const ownerEmail2 = params.ownerEmail || context?.user?.email || void 0;
2239
2595
  const access = params.access ?? "owner";
2240
2596
  return sm.applyMount({
2241
2597
  name: params.name,
@@ -2243,7 +2599,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2243
2599
  process: params.process,
2244
2600
  sessionId: params.sessionId,
2245
2601
  access,
2246
- ownerEmail
2602
+ ownerEmail: ownerEmail2
2247
2603
  });
2248
2604
  },
2249
2605
  /** Remove a mount from the shared static file server. */
@@ -2427,7 +2783,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2427
2783
  }
2428
2784
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2429
2785
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2430
- const { toolsForRole } = await import('./sideband-DYhbiCEA.mjs');
2786
+ const { toolsForRole } = await import('./sideband-CO0bdYO_.mjs');
2431
2787
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2432
2788
  return fmt(r2);
2433
2789
  }
@@ -2462,6 +2818,89 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2462
2818
  }
2463
2819
  return void 0;
2464
2820
  };
2821
+ const readChannelFromDisk = (channelId) => {
2822
+ const seen = /* @__PURE__ */ new Set();
2823
+ for (const t of handlers.getTrackedSessions?.() || []) {
2824
+ const dir = t.directory;
2825
+ if (!dir || seen.has(dir)) continue;
2826
+ seen.add(dir);
2827
+ try {
2828
+ const c = new ChannelStore(dir).get(channelId);
2829
+ if (c && c.enabled !== false) return { c, dir };
2830
+ } catch {
2831
+ }
2832
+ }
2833
+ return void 0;
2834
+ };
2835
+ const liveSessionsInDir = (dir) => (handlers.getTrackedSessions?.() || []).filter((t) => t.directory === dir && handlers.getSessionRPCHandlers?.(t.sessionId)).map((t) => t.sessionId);
2836
+ const resolveChannel = (channelId, targetSession) => {
2837
+ const found = readChannelFromDisk(channelId);
2838
+ if (!found) return { error: "channel not found" };
2839
+ const { c, dir } = found;
2840
+ const mode = bindMode(c);
2841
+ const kind = c.action?.kind;
2842
+ if (kind === "agent" || kind === "loop") {
2843
+ const prefer = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? targetSession : void 0;
2844
+ const sid = prefer && handlers.getSessionRPCHandlers?.(prefer) ? prefer : liveSessionsInDir(dir)[0];
2845
+ if (!sid) {
2846
+ if (mode === "fixed") return { tier: "session", sessionId: fixedSessionId(c) || "?", stopped: true, c, dir };
2847
+ return { error: `no live session to host this ${kind} channel \u2014 start a session in ${dir}` };
2848
+ }
2849
+ return { tier: "session", sessionId: sid, rpc: handlers.getSessionRPCHandlers(sid), c, dir };
2850
+ }
2851
+ const target = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? targetSession : void 0;
2852
+ if (target) {
2853
+ const rpc = handlers.getSessionRPCHandlers?.(target);
2854
+ if (rpc) return { tier: "session", sessionId: target, rpc, c, dir };
2855
+ if (mode === "fixed") return { tier: "session", sessionId: target, stopped: true, c, dir };
2856
+ }
2857
+ return { tier: "stateless", c, dir };
2858
+ };
2859
+ const ownerEmail = currentMetadata.sharing?.owner;
2860
+ const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
2861
+ const selfMachine = {
2862
+ spawnSession: (opts) => handlers.spawnSession(opts),
2863
+ sessionRPC: async (sessionId, method, kwargs) => {
2864
+ const rpc = handlers.getSessionRPCHandlers?.(sessionId);
2865
+ if (!rpc) throw new Error(`Session ${sessionId} not found on this machine`);
2866
+ const handler = rpc[method];
2867
+ if (typeof handler !== "function") throw new Error(`Unknown session method: ${method}`);
2868
+ const paramNames = getParamNames(handler);
2869
+ const callArgs = paramNames.map((n) => n === "context" ? ownerCtx : kwargs?.[n] ?? void 0);
2870
+ return handler(...callArgs);
2871
+ }
2872
+ };
2873
+ const dispatchStateless = async (c, dir, kwargs, context) => {
2874
+ const u = context?.user;
2875
+ const r = resolveSender(c, {
2876
+ key: kwargs.key,
2877
+ from: kwargs.from,
2878
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
2879
+ hyphaAnonymous: u?.is_anonymous === true,
2880
+ hyphaWorkspace: u?.scope?.current_workspace
2881
+ });
2882
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
2883
+ const callId = "call_" + Math.random().toString(16).slice(2, 12);
2884
+ const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
2885
+ const { queryCore } = await import('./commands-o0MBJocy.mjs');
2886
+ const timeout = c.reply?.timeout_sec || 120;
2887
+ let result;
2888
+ try {
2889
+ result = await queryCore(selfMachine, dir, rendered, { permissionMode: "bypassPermissions", tag: "svamp-channel", timeout });
2890
+ } catch (e) {
2891
+ return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
2892
+ } finally {
2893
+ if (result?.sessionId) {
2894
+ try {
2895
+ handlers.deleteSession?.(result.sessionId);
2896
+ } catch {
2897
+ }
2898
+ }
2899
+ }
2900
+ if (result.status === "error") return { ok: false, call_id: callId, status: "error", error: result.error };
2901
+ if (result.status === "permission-pending") return { ok: false, call_id: callId, status: "permission-pending", error: result.error };
2902
+ return { ok: true, call_id: callId, status: "completed", reply: result.response };
2903
+ };
2465
2904
  const channelsServiceInfo = await server.registerService(
2466
2905
  {
2467
2906
  id: "channels",
@@ -2501,17 +2940,22 @@ ${d?.error || "not found"}`;
2501
2940
  },
2502
2941
  send: async (kwargs = {}, context) => {
2503
2942
  trackInbound();
2504
- const rpc = await findChannelOwner(kwargs.channel);
2505
- if (!rpc?.channelSend) return { error: "channel not found" };
2506
- return rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key, reply_to: kwargs.reply_to }, context);
2943
+ const res = resolveChannel(kwargs.channel, kwargs.session);
2944
+ if ("error" in res) return { error: res.error };
2945
+ if (res.tier === "stateless") return dispatchStateless(res.c, res.dir, kwargs, context);
2946
+ if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped \u2014 resume it to reach this channel` };
2947
+ return res.rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key, session: kwargs.session, reply_to: kwargs.reply_to }, context);
2507
2948
  },
2508
2949
  // Async reply retrieval for queue-mode channels — long-poll the channel outbox
2509
- // for replies addressed to the caller. Channel-identity auth (key/from).
2950
+ // for replies addressed to the caller. Routed to the session the message landed
2951
+ // in (the `session` target); stateless channels can't queue. Channel-identity auth.
2510
2952
  receive: async (kwargs = {}, context) => {
2511
2953
  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);
2954
+ const res = resolveChannel(kwargs.channel, kwargs.session);
2955
+ if ("error" in res) return { error: res.error };
2956
+ if (res.tier === "stateless") return { error: "stateless channels have no async replies (no persistent session)" };
2957
+ if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped` };
2958
+ return res.rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
2515
2959
  }
2516
2960
  },
2517
2961
  { overwrite: true }
@@ -2522,158 +2966,53 @@ ${d?.error || "not found"}`;
2522
2966
  notifySessionEvent: notifyListeners,
2523
2967
  updateMetadata: (newMetadata) => {
2524
2968
  currentMetadata = newMetadata;
2525
- metadataVersion++;
2526
- notifyListeners({
2527
- type: "update-machine",
2528
- machineId,
2529
- metadata: { value: currentMetadata, version: metadataVersion }
2530
- });
2531
- },
2532
- updateDaemonState: (newState) => {
2533
- currentDaemonState = newState;
2534
- daemonStateVersion++;
2535
- notifyListeners({
2536
- type: "update-machine",
2537
- machineId,
2538
- daemonState: { value: currentDaemonState, version: daemonStateVersion }
2539
- });
2540
- },
2541
- getLastInboundRpcAt: () => lastInboundRpcAt,
2542
- disconnect: async () => {
2543
- const toRemove = [...listeners];
2544
- for (const listener of toRemove) {
2545
- removeListener(listener, "disconnect");
2546
- }
2547
- await server.unregisterService(serviceInfo.id);
2548
- await server.unregisterService(channelsServiceInfo.id).catch(() => {
2549
- });
2550
- }
2551
- };
2552
- }
2553
-
2554
- const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
2555
- function parseField(token, [min, max]) {
2556
- const set = /* @__PURE__ */ new Set();
2557
- for (const part of token.split(",")) {
2558
- let m;
2559
- if (part === "*") {
2560
- for (let i = min; i <= max; i++) set.add(i);
2561
- } else if (m = part.match(/^\*\/(\d+)$/)) {
2562
- const s = +m[1];
2563
- for (let i = min; i <= max; i += s) set.add(i);
2564
- } else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
2565
- for (let i = +m[1]; i <= +m[2]; i += +m[3]) set.add(i);
2566
- } else if (m = part.match(/^(\d+)-(\d+)$/)) {
2567
- for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
2568
- } else if (m = part.match(/^(\d+)$/)) {
2569
- set.add(+m[1]);
2570
- } else throw new Error(`invalid cron field: "${token}"`);
2571
- }
2572
- for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
2573
- return set;
2574
- }
2575
- function parseCron(expr) {
2576
- const fields = String(expr).trim().split(/\s+/);
2577
- if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
2578
- const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
2579
- return { minute, hour, dom, month, dow, domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
2580
- }
2581
- function cronMatches(expr, date) {
2582
- const c = typeof expr === "string" ? parseCron(expr) : expr;
2583
- if (!c.minute.has(date.getMinutes())) return false;
2584
- if (!c.hour.has(date.getHours())) return false;
2585
- if (!c.month.has(date.getMonth() + 1)) return false;
2586
- const domOk = c.dom.has(date.getDate());
2587
- const dowOk = c.dow.has(date.getDay());
2588
- if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
2589
- return domOk && dowOk;
2590
- }
2591
- function inZone(date, tz) {
2592
- if (!tz) return date;
2593
- try {
2594
- const p = new Intl.DateTimeFormat("en-US", {
2595
- timeZone: tz,
2596
- hour12: false,
2597
- year: "numeric",
2598
- month: "2-digit",
2599
- day: "2-digit",
2600
- hour: "2-digit",
2601
- minute: "2-digit"
2602
- }).formatToParts(date).reduce((o, x) => {
2603
- o[x.type] = x.value;
2604
- return o;
2605
- }, {});
2606
- return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
2607
- } catch {
2608
- return date;
2609
- }
2610
- }
2611
- function nextFire(expr, from, tz) {
2612
- const c = parseCron(expr);
2613
- const d = new Date(from.getTime());
2614
- d.setSeconds(0, 0);
2615
- d.setMinutes(d.getMinutes() + 1);
2616
- for (let i = 0; i < 366 * 24 * 60; i++) {
2617
- if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
2618
- d.setMinutes(d.getMinutes() + 1);
2619
- }
2620
- return null;
2621
- }
2622
- function resolvePath(ctx, path) {
2623
- return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
2624
- }
2625
- function renderTemplate(template, ctx) {
2626
- return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
2627
- const v = resolvePath(ctx, p);
2628
- return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
2629
- });
2630
- }
2631
- const TRIGGER_TYPES = ["manual", "schedule", "webhook", "api"];
2632
- const ACTION_KINDS = ["message", "loop"];
2633
- const OVERLAP = ["queue", "skip", "replace"];
2634
- function validateRoutine(r) {
2635
- const errs = [];
2636
- if (!r || typeof r !== "object") return ["routine must be an object"];
2637
- if (!r.session_id) errs.push("session_id required");
2638
- if (!r.name) errs.push("name required");
2639
- const t = r.trigger;
2640
- if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join("|")}`);
2641
- if (t?.type === "schedule") {
2642
- try {
2643
- parseCron(t.cron);
2644
- } catch (e) {
2645
- errs.push(`trigger.cron: ${e.message}`);
2969
+ metadataVersion++;
2970
+ notifyListeners({
2971
+ type: "update-machine",
2972
+ machineId,
2973
+ metadata: { value: currentMetadata, version: metadataVersion }
2974
+ });
2975
+ },
2976
+ updateDaemonState: (newState) => {
2977
+ currentDaemonState = newState;
2978
+ daemonStateVersion++;
2979
+ notifyListeners({
2980
+ type: "update-machine",
2981
+ machineId,
2982
+ daemonState: { value: currentDaemonState, version: daemonStateVersion }
2983
+ });
2984
+ },
2985
+ getLastInboundRpcAt: () => lastInboundRpcAt,
2986
+ disconnect: async () => {
2987
+ const toRemove = [...listeners];
2988
+ for (const listener of toRemove) {
2989
+ removeListener(listener, "disconnect");
2990
+ }
2991
+ await server.unregisterService(serviceInfo.id);
2992
+ await server.unregisterService(channelsServiceInfo.id).catch(() => {
2993
+ });
2646
2994
  }
2647
- if (t.missed && !["catchup", "skip"].includes(t.missed)) errs.push("trigger.missed must be catchup|skip");
2648
- }
2649
- const a = r.action;
2650
- if (!a || !ACTION_KINDS.includes(a.kind)) errs.push(`action.kind must be one of ${ACTION_KINDS.join("|")}`);
2651
- if (a?.kind === "message" && !a.template) errs.push("action.template required for message action");
2652
- if (a?.kind === "loop" && !a.loop && !a.task_template) errs.push("action.loop or action.task_template required for loop action");
2653
- if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join("|")}`);
2654
- if ((t?.type === "webhook" || t?.type === "api") && t.public && a?.kind === "loop")
2655
- errs.push("a public webhook/api may not use a loop action (unauthenticated task injection) \u2014 use a message action or require a key");
2656
- return errs;
2995
+ };
2657
2996
  }
2658
2997
 
2659
2998
  function defaultRoutinesDir() {
2660
- return process.env.SVAMP_ROUTINES_DIR || join$1(homedir(), ".svamp", "routines");
2999
+ return process.env.SVAMP_ROUTINES_DIR || join(homedir(), ".svamp", "routines");
2661
3000
  }
2662
- const genId$1 = () => "rt_" + randomBytes(5).toString("hex");
2663
- const genKey$1 = () => randomBytes(18).toString("base64url");
3001
+ const genId = () => "rt_" + randomBytes(5).toString("hex");
3002
+ const genKey = () => randomBytes(18).toString("base64url");
2664
3003
  class RoutineStore {
2665
3004
  dir;
2666
3005
  constructor(dir = defaultRoutinesDir()) {
2667
3006
  this.dir = dir;
2668
- mkdirSync$1(dir, { recursive: true });
3007
+ mkdirSync(dir, { recursive: true });
2669
3008
  }
2670
3009
  _path(id) {
2671
- return join$1(this.dir, `${id}.json`);
3010
+ return join(this.dir, `${id}.json`);
2672
3011
  }
2673
3012
  list(sessionId) {
2674
3013
  const all = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
2675
3014
  try {
2676
- return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
3015
+ return JSON.parse(readFileSync(join(this.dir, f), "utf8"));
2677
3016
  } catch {
2678
3017
  return null;
2679
3018
  }
@@ -2689,13 +3028,13 @@ class RoutineStore {
2689
3028
  }
2690
3029
  save(routine) {
2691
3030
  const r = { overlap: "queue", enabled: true, last_runs: [], ...routine };
2692
- if (!r.id) r.id = genId$1();
2693
- if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey$1();
3031
+ if (!r.id) r.id = genId();
3032
+ if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey();
2694
3033
  const errs = validateRoutine(r);
2695
3034
  if (errs.length) throw new Error("invalid routine: " + errs.join("; "));
2696
3035
  const tmp = this._path(r.id) + ".tmp";
2697
- writeFileSync$1(tmp, JSON.stringify(r, null, 2));
2698
- renameSync$1(tmp, this._path(r.id));
3036
+ writeFileSync(tmp, JSON.stringify(r, null, 2));
3037
+ renameSync(tmp, this._path(r.id));
2699
3038
  return r;
2700
3039
  }
2701
3040
  remove(id) {
@@ -2851,175 +3190,6 @@ class RoutineRunner {
2851
3190
  }
2852
3191
  }
2853
3192
 
2854
- const genId = () => "c_" + randomBytes(5).toString("hex");
2855
- const genKey = () => "ck_" + randomBytes(18).toString("base64url");
2856
- const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
2857
- \${body.message}
2858
- </inbound-message>`;
2859
- function validateChannel(c) {
2860
- const errs = [];
2861
- if (!c || typeof c !== "object") return ["channel must be an object"];
2862
- if (!c.name) errs.push("name required");
2863
- const m = c.identity?.mode;
2864
- if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
2865
- if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
2866
- if (!["message", "loop", "agent"].includes(c.action?.kind)) errs.push("action.kind must be message|loop|agent");
2867
- if (c.bind !== "active" && !(c.bind && (c.bind.tag || c.bind.session))) errs.push('bind must be "active", {tag}, or {session}');
2868
- if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
2869
- errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
2870
- if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
2871
- const MUTATING = ["run_bash", "send_to_session"];
2872
- const ag = c.action.agent || {};
2873
- const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
2874
- if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
2875
- }
2876
- const unsafe = /[<>"'&\r\n]/;
2877
- if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
2878
- if (c.description && unsafe.test(c.description)) errs.push(`description must be single-line and not contain < > " ' &`);
2879
- if (c.skill?.name && unsafe.test(c.skill.name)) errs.push(`skill.name must be single-line and not contain < > " ' &`);
2880
- if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
2881
- if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
2882
- for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
2883
- return errs;
2884
- }
2885
- class ChannelStore {
2886
- dir;
2887
- constructor(projectDir) {
2888
- this.dir = join$1(projectDir, ".svamp", "channels");
2889
- try {
2890
- mkdirSync$1(this.dir, { recursive: true });
2891
- } catch {
2892
- }
2893
- }
2894
- _path(id) {
2895
- return join$1(this.dir, `${id}.json`);
2896
- }
2897
- list() {
2898
- if (!existsSync(this.dir)) return [];
2899
- return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
2900
- try {
2901
- return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
2902
- } catch {
2903
- return null;
2904
- }
2905
- }).filter((c) => !!c);
2906
- }
2907
- get(id) {
2908
- try {
2909
- return JSON.parse(readFileSync(this._path(id), "utf8"));
2910
- } catch {
2911
- return null;
2912
- }
2913
- }
2914
- save(channel) {
2915
- const c = { enabled: true, bind: "active", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
2916
- if (!c.id) c.id = genId();
2917
- const errs = validateChannel(c);
2918
- if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
2919
- mkdirSync$1(this.dir, { recursive: true });
2920
- const tmp = this._path(c.id) + ".tmp";
2921
- writeFileSync$1(tmp, JSON.stringify(c, null, 2));
2922
- renameSync$1(tmp, this._path(c.id));
2923
- return c;
2924
- }
2925
- remove(id) {
2926
- const p = this._path(id);
2927
- if (existsSync(p)) {
2928
- rmSync(p);
2929
- return true;
2930
- }
2931
- return false;
2932
- }
2933
- setEnabled(id, enabled) {
2934
- const c = this.get(id);
2935
- if (!c) return null;
2936
- c.enabled = enabled;
2937
- return this.save(c);
2938
- }
2939
- recordCall(id, entry) {
2940
- const c = this.get(id);
2941
- if (!c) return;
2942
- c.last_calls = c.last_calls || [];
2943
- c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
2944
- c.last_calls = c.last_calls.slice(0, 20);
2945
- this.save(c);
2946
- }
2947
- addCaller(id, name, kind = "agent") {
2948
- const c = this.get(id);
2949
- if (!c) return null;
2950
- c.identity.callers = c.identity.callers || [];
2951
- const caller = { name, kind, key: genKey() };
2952
- c.identity.callers.push(caller);
2953
- this.save(c);
2954
- return caller;
2955
- }
2956
- }
2957
- function gatewayBase(channelsServiceId, baseUrl) {
2958
- const slash = channelsServiceId.indexOf("/");
2959
- if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
2960
- const ws = channelsServiceId.slice(0, slash);
2961
- const clientSvc = channelsServiceId.slice(slash + 1);
2962
- return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
2963
- }
2964
- function generateSkillBody(channel, ctx) {
2965
- const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
2966
- const base = ctx?.baseUrl || "https://hypha.aicell.io";
2967
- const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
2968
- const skillUrl = `${gw}/skill?channel=${channel.id}`;
2969
- const sendUrl = `${gw}/send`;
2970
- const key = ctx?.key || "<your-key>";
2971
- const isAgent = channel.action?.kind === "agent";
2972
- const isQueue = channel.reply?.mode === "queue";
2973
- const recvUrl = `${gw}/receive`;
2974
- const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
2975
- const name = channel.skill?.name || channel.name;
2976
- const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
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).` : "";
2993
- const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
2994
- return `---
2995
- name: ${name}
2996
- description: ${desc}
2997
- ---
2998
- # ${name}
2999
- ${channel.description || ""}
3000
-
3001
- Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
3002
- This skill (with every value below already filled in) is served at:
3003
- ${skillUrl}
3004
-
3005
- ${rpcLine}
3006
- \`\`\`js
3007
- await get_service("${svc}").send({
3008
- channel: "${channel.id}",
3009
- message: "your message here",
3010
- from: "your-name",
3011
- });
3012
- \`\`\`
3013
-
3014
- **HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
3015
- \`\`\`
3016
- POST ${sendUrl}
3017
- Content-Type: application/json
3018
-
3019
- {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"}}
3020
- \`\`\`${queueSection}`;
3021
- }
3022
-
3023
3193
  const MAX_PER_CHANNEL = 200;
3024
3194
  const TTL_MS = 60 * 60 * 1e3;
3025
3195
  class ChannelOutbox {
@@ -3028,11 +3198,11 @@ class ChannelOutbox {
3028
3198
  seqByChannel = /* @__PURE__ */ new Map();
3029
3199
  emitter = new EventEmitter();
3030
3200
  constructor(projectDir) {
3031
- const dir = join$1(projectDir, ".svamp", "channels");
3032
- this.file = join$1(dir, "_outbox.jsonl");
3201
+ const dir = join(projectDir, ".svamp", "channels");
3202
+ this.file = join(dir, "_outbox.jsonl");
3033
3203
  this.emitter.setMaxListeners(0);
3034
3204
  try {
3035
- mkdirSync$1(dir, { recursive: true });
3205
+ mkdirSync(dir, { recursive: true });
3036
3206
  } catch {
3037
3207
  }
3038
3208
  this._load();
@@ -3081,7 +3251,7 @@ class ChannelOutbox {
3081
3251
  appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3082
3252
  } catch {
3083
3253
  try {
3084
- mkdirSync$1(join$1(this.file, ".."), { recursive: true });
3254
+ mkdirSync(join(this.file, ".."), { recursive: true });
3085
3255
  appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3086
3256
  } catch {
3087
3257
  }
@@ -3145,62 +3315,15 @@ class ChannelOutbox {
3145
3315
  }
3146
3316
  });
3147
3317
  const tmp = this.file + ".tmp";
3148
- writeFileSync$1(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
3149
- renameSync$1(tmp, this.file);
3318
+ writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
3319
+ renameSync(tmp, this.file);
3150
3320
  } catch {
3151
3321
  }
3152
3322
  }
3153
3323
  }
3154
3324
 
3155
- function resolveSender(channel, input = {}) {
3156
- const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
3157
- const id = channel.identity || {};
3158
- if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
3159
- if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
3160
- return { sender: { name: hyphaUser, kind: "agent", verified: true } };
3161
- return { error: "caller not in hypha_allow" };
3162
- }
3163
- if (id.mode === "fixed") {
3164
- if (!id.fixed?.name) return { error: "fixed identity not configured" };
3165
- return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
3166
- }
3167
- if (id.mode === "per-key") {
3168
- const caller = (id.callers || []).find((c) => c.key && c.key === key);
3169
- if (!caller) return { error: "invalid or missing key" };
3170
- return { sender: { name: caller.name, kind: caller.kind, verified: true } };
3171
- }
3172
- if (id.mode === "caller-supplied") {
3173
- if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
3174
- return { sender: { name: from || "anonymous", kind: "user", verified: false } };
3175
- }
3176
- return { error: "unsupported identity mode" };
3177
- }
3178
- const MAX_BODY = 16 * 1024;
3179
- function xmlEscape(s) {
3180
- return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3181
- }
3182
- const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
3183
- function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
3184
- const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
3185
- const escVal = (v) => xmlEscape(obj(v));
3186
- const escAttr = (v) => xmlEscape(stripControl(obj(v)));
3187
- const bodyEsc = {};
3188
- for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
3189
- const queryEsc = {};
3190
- for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
3191
- const ctx = {
3192
- sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
3193
- body: bodyEsc,
3194
- query: queryEsc,
3195
- channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
3196
- call: { id: escAttr(callId) },
3197
- now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
3198
- };
3199
- return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
3200
- }
3201
-
3202
3325
  function channelPublicView(c) {
3203
- return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind };
3326
+ return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind };
3204
3327
  }
3205
3328
  function isStructuredMessage(msg) {
3206
3329
  return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
@@ -3227,7 +3350,7 @@ ${msg.body}
3227
3350
  </svamp-message>`;
3228
3351
  }
3229
3352
  function loadMessages(messagesDir, sessionId) {
3230
- const filePath = join$1(messagesDir, "messages.jsonl");
3353
+ const filePath = join(messagesDir, "messages.jsonl");
3231
3354
  if (!existsSync(filePath)) return [];
3232
3355
  try {
3233
3356
  const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
@@ -3244,7 +3367,7 @@ function loadMessages(messagesDir, sessionId) {
3244
3367
  }
3245
3368
  }
3246
3369
  function loadMessagesFromDisk(messagesDir, afterSeq, limit) {
3247
- const filePath = join$1(messagesDir, "messages.jsonl");
3370
+ const filePath = join(messagesDir, "messages.jsonl");
3248
3371
  if (!existsSync(filePath)) return { messages: [], hasMore: false };
3249
3372
  try {
3250
3373
  const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
@@ -3263,7 +3386,7 @@ function loadMessagesFromDisk(messagesDir, afterSeq, limit) {
3263
3386
  }
3264
3387
  }
3265
3388
  function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
3266
- const filePath = join$1(messagesDir, "messages.jsonl");
3389
+ const filePath = join(messagesDir, "messages.jsonl");
3267
3390
  if (!existsSync(filePath)) return { messages: [], hasMore: false };
3268
3391
  try {
3269
3392
  const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
@@ -3282,7 +3405,7 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
3282
3405
  }
3283
3406
  }
3284
3407
  function countMessagesOnDisk(messagesDir, fallbackInMemory) {
3285
- const filePath = join$1(messagesDir, "messages.jsonl");
3408
+ const filePath = join(messagesDir, "messages.jsonl");
3286
3409
  if (!existsSync(filePath)) return fallbackInMemory;
3287
3410
  try {
3288
3411
  const data = readFileSync(filePath, "utf-8");
@@ -3296,9 +3419,9 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
3296
3419
  }
3297
3420
  function appendMessage(messagesDir, sessionId, msg) {
3298
3421
  try {
3299
- const filePath = join$1(messagesDir, "messages.jsonl");
3422
+ const filePath = join(messagesDir, "messages.jsonl");
3300
3423
  if (!existsSync(messagesDir)) {
3301
- mkdirSync$1(messagesDir, { recursive: true });
3424
+ mkdirSync(messagesDir, { recursive: true });
3302
3425
  }
3303
3426
  appendFileSync(filePath, JSON.stringify(msg) + "\n");
3304
3427
  } catch (err) {
@@ -3434,7 +3557,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3434
3557
  const skillCtxFor = (c) => ({
3435
3558
  channelsServiceId,
3436
3559
  baseUrl: channelsBaseUrl,
3437
- key: c.identity?.shared_key || c.identity?.callers?.[0]?.key
3560
+ key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
3561
+ // The session this skill is being copied FROM — baked into `dynamic` channel
3562
+ // instructions as the routing target so the copied link lands in THIS session.
3563
+ session: sessionId
3438
3564
  });
3439
3565
  const skillUrlsFor = (c) => {
3440
3566
  if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
@@ -3625,6 +3751,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3625
3751
  saveChannel: async (channel, context) => {
3626
3752
  authorizeRequest(context, metadata.sharing, "admin");
3627
3753
  try {
3754
+ const b = channel.bind;
3755
+ if (b && typeof b === "object" && (b.session === "current" || b.session === "" || b.session == null)) {
3756
+ channel = { ...channel, bind: { session: sessionId } };
3757
+ }
3628
3758
  const saved = channelStore.save(channel);
3629
3759
  syncChannelsToMetadata();
3630
3760
  return { success: true, channel: saved };
@@ -3656,6 +3786,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3656
3786
  const c = channelStore.get(id);
3657
3787
  if (!c) return { error: "not found" };
3658
3788
  const { skillUrl, sendUrl } = skillUrlsFor(c);
3789
+ const mode = bindMode(c);
3790
+ const routeSession = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? sessionId : void 0;
3659
3791
  return {
3660
3792
  skill: generateSkillBody(c, skillCtxFor(c)),
3661
3793
  channelsServiceId,
@@ -3664,7 +3796,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3664
3796
  skillUrl,
3665
3797
  sendUrl,
3666
3798
  key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
3667
- hyphaKeyless: (c.identity?.hypha_allow || []).length > 0
3799
+ hyphaKeyless: (c.identity?.hypha_allow || []).length > 0,
3800
+ bind: c.bind,
3801
+ bindMode: mode,
3802
+ session: routeSession
3668
3803
  };
3669
3804
  },
3670
3805
  // Public channel discovery (no session authz — channels are deliberately
@@ -4186,9 +4321,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4186
4321
  messages.length = 0;
4187
4322
  nextSeq = 1;
4188
4323
  if (options?.messagesDir) {
4189
- const filePath = join$1(options.messagesDir, "messages.jsonl");
4324
+ const filePath = join(options.messagesDir, "messages.jsonl");
4190
4325
  try {
4191
- writeFileSync$1(filePath, "");
4326
+ writeFileSync(filePath, "");
4192
4327
  } catch {
4193
4328
  }
4194
4329
  }
@@ -4216,7 +4351,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4216
4351
  return { store, rpcHandlers };
4217
4352
  }
4218
4353
 
4219
- const SVAMP_HOME$2 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
4354
+ const SVAMP_HOME$2 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
4220
4355
  const num = (key, def) => {
4221
4356
  const v = Number(process.env[key]);
4222
4357
  return Number.isFinite(v) && v > 0 ? v : def;
@@ -4229,19 +4364,19 @@ const BREAKER_MAX_WAKES = num("SVAMP_INBOX_BREAKER_MAX_WAKES", 30);
4229
4364
  const BREAKER_COOLDOWN_MS = num("SVAMP_INBOX_BREAKER_COOLDOWN_MS", 12e4);
4230
4365
  const CTX_STALE_MS = num("SVAMP_INBOX_CTX_STALE_MS", 30 * 6e4);
4231
4366
  function ctxPath(sessionId) {
4232
- return join$1(SVAMP_HOME$2, "inbound", `${sessionId}.json`);
4367
+ return join(SVAMP_HOME$2, "inbound", `${sessionId}.json`);
4233
4368
  }
4234
4369
  function writeInboundContext(sessionId, ctx) {
4235
4370
  try {
4236
4371
  const p = ctxPath(sessionId);
4237
- mkdirSync$1(join$1(SVAMP_HOME$2, "inbound"), { recursive: true });
4372
+ mkdirSync(join(SVAMP_HOME$2, "inbound"), { recursive: true });
4238
4373
  const tmp = p + ".tmp";
4239
- writeFileSync$1(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
4374
+ writeFileSync(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
4240
4375
  try {
4241
4376
  rmSync(p, { force: true });
4242
4377
  } catch {
4243
4378
  }
4244
- writeFileSync$1(p, readFileSync(tmp));
4379
+ writeFileSync(p, readFileSync(tmp));
4245
4380
  try {
4246
4381
  rmSync(tmp, { force: true });
4247
4382
  } catch {
@@ -4528,8 +4663,8 @@ class SessionArtifactSync {
4528
4663
  this.syncing = true;
4529
4664
  try {
4530
4665
  const artifactAlias = `session-${sessionId}`;
4531
- const sessionJsonPath = join$1(sessionsDir, "session.json");
4532
- const messagesPath = join$1(sessionsDir, "messages.jsonl");
4666
+ const sessionJsonPath = join(sessionsDir, "session.json");
4667
+ const messagesPath = join(sessionsDir, "messages.jsonl");
4533
4668
  let sessionData = null;
4534
4669
  if (existsSync(sessionJsonPath)) {
4535
4670
  try {
@@ -4639,18 +4774,18 @@ class SessionArtifactSync {
4639
4774
  artifact_id: artifactAlias,
4640
4775
  _rkwargs: true
4641
4776
  });
4642
- if (!existsSync(targetDir)) mkdirSync$1(targetDir, { recursive: true });
4777
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
4643
4778
  try {
4644
4779
  const data = await this.downloadFile(artifact.id, "session.json");
4645
4780
  if (data) {
4646
- writeFileSync$1(join$1(targetDir, "session.json"), data);
4781
+ writeFileSync(join(targetDir, "session.json"), data);
4647
4782
  }
4648
4783
  } catch {
4649
4784
  }
4650
4785
  try {
4651
4786
  const data = await this.downloadFile(artifact.id, "messages.jsonl");
4652
4787
  if (data) {
4653
- writeFileSync$1(join$1(targetDir, "messages.jsonl"), data);
4788
+ writeFileSync(join(targetDir, "messages.jsonl"), data);
4654
4789
  }
4655
4790
  } catch {
4656
4791
  }
@@ -4703,13 +4838,13 @@ class SessionArtifactSync {
4703
4838
  */
4704
4839
  async syncAll(svampHome, machineId) {
4705
4840
  if (!this.initialized) return;
4706
- const indexFile = join$1(svampHome, "sessions-index.json");
4841
+ const indexFile = join(svampHome, "sessions-index.json");
4707
4842
  if (!existsSync(indexFile)) return;
4708
4843
  try {
4709
4844
  const index = JSON.parse(readFileSync(indexFile, "utf-8"));
4710
4845
  for (const [sessionId, entry] of Object.entries(index)) {
4711
- const sessionDir = join$1(entry.directory, ".svamp", sessionId);
4712
- if (existsSync(join$1(sessionDir, "session.json"))) {
4846
+ const sessionDir = join(entry.directory, ".svamp", sessionId);
4847
+ if (existsSync(join(sessionDir, "session.json"))) {
4713
4848
  await this.syncSession(sessionId, sessionDir, void 0, machineId);
4714
4849
  }
4715
4850
  }
@@ -5025,7 +5160,7 @@ var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
5025
5160
 
5026
5161
  function expandHome(p) {
5027
5162
  if (p === "~") return homedir();
5028
- if (p.startsWith("~/")) return join$1(homedir(), p.slice(2));
5163
+ if (p.startsWith("~/")) return join(homedir(), p.slice(2));
5029
5164
  return p;
5030
5165
  }
5031
5166
  function wrapWithIsolation(originalCommand, originalArgs, config) {
@@ -5052,11 +5187,11 @@ function wrapWithNono(command, args, config) {
5052
5187
  nonoArgs.push("--allow-cwd");
5053
5188
  if (config.credentialStagingPath) {
5054
5189
  env.HOME = config.credentialStagingPath;
5055
- const realLocalDir = join$1(homedir(), ".local");
5190
+ const realLocalDir = join(homedir(), ".local");
5056
5191
  if (existsSync(realLocalDir)) {
5057
5192
  nonoArgs.push("--read", realLocalDir);
5058
5193
  }
5059
- const realKeychainDir = join$1(homedir(), "Library", "Keychains");
5194
+ const realKeychainDir = join(homedir(), "Library", "Keychains");
5060
5195
  if (existsSync(realKeychainDir)) {
5061
5196
  nonoArgs.push("--read", realKeychainDir);
5062
5197
  }
@@ -5117,9 +5252,9 @@ function wrapWithContainer(runtime, command, args, config) {
5117
5252
  config.workspacePath
5118
5253
  ];
5119
5254
  if (config.credentialStagingPath) {
5120
- const stagedClaudeDir = join$1(config.credentialStagingPath, ".claude");
5255
+ const stagedClaudeDir = join(config.credentialStagingPath, ".claude");
5121
5256
  containerArgs.push("-v", `${stagedClaudeDir}:/root/.claude:ro`);
5122
- const stagedGitconfig = join$1(config.credentialStagingPath, ".gitconfig");
5257
+ const stagedGitconfig = join(config.credentialStagingPath, ".gitconfig");
5123
5258
  containerArgs.push("-v", `${stagedGitconfig}:/root/.gitconfig:ro`);
5124
5259
  containerArgs.push("-e", "HOME=/root");
5125
5260
  }
@@ -6868,8 +7003,8 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
6868
7003
  });
6869
7004
 
6870
7005
  const execFileAsync = promisify$1(execFile$1);
6871
- const SVAMP_TOOLS_DIR = join$1(homedir(), ".svamp", "tools");
6872
- const SVAMP_BIN_DIR = join$1(SVAMP_TOOLS_DIR, "bin");
7006
+ const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
7007
+ const SVAMP_BIN_DIR = join(SVAMP_TOOLS_DIR, "bin");
6873
7008
  async function checkCommand(command, versionArgs) {
6874
7009
  try {
6875
7010
  const { stdout } = await execFileAsync(command, versionArgs, {
@@ -6879,7 +7014,7 @@ async function checkCommand(command, versionArgs) {
6879
7014
  return { found: true, version, path: command };
6880
7015
  } catch {
6881
7016
  }
6882
- const localPath = join$1(SVAMP_BIN_DIR, command);
7017
+ const localPath = join(SVAMP_BIN_DIR, command);
6883
7018
  try {
6884
7019
  const { stdout } = await execFileAsync(localPath, versionArgs, {
6885
7020
  timeout: 5e3
@@ -6946,7 +7081,7 @@ async function installNono() {
6946
7081
  const downloadUrl = asset.browser_download_url;
6947
7082
  console.log(`[isolation] Downloading nono ${version} from ${downloadUrl}...`);
6948
7083
  await mkdir(SVAMP_BIN_DIR, { recursive: true });
6949
- const tarball = join$1(SVAMP_BIN_DIR, assetName);
7084
+ const tarball = join(SVAMP_BIN_DIR, assetName);
6950
7085
  await execFileAsync("curl", [
6951
7086
  "-fsSL",
6952
7087
  "-o",
@@ -6963,7 +7098,7 @@ async function installNono() {
6963
7098
  ], { timeout: 15e3 });
6964
7099
  await rm(tarball, { force: true }).catch(() => {
6965
7100
  });
6966
- const nonoPath = join$1(SVAMP_BIN_DIR, "nono");
7101
+ const nonoPath = join(SVAMP_BIN_DIR, "nono");
6967
7102
  await chmod(nonoPath, 493);
6968
7103
  try {
6969
7104
  await access(nonoPath);
@@ -7005,8 +7140,8 @@ async function parseIsolationTestOutput(stdout, probeFile) {
7005
7140
  }
7006
7141
  async function verifyNonoIsolation(binaryPath) {
7007
7142
  const testBase = "/tmp";
7008
- const workDir = await mkdtemp(join$1(testBase, "svamp-iso-work-"));
7009
- const probeFile = join$1(homedir(), `.svamp-iso-probe-${process.pid}`);
7143
+ const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
7144
+ const probeFile = join(homedir(), `.svamp-iso-probe-${process.pid}`);
7010
7145
  try {
7011
7146
  const testScript = [
7012
7147
  `echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
@@ -7132,7 +7267,7 @@ async function detectIsolationCapabilities() {
7132
7267
  return { available, preferred, details };
7133
7268
  }
7134
7269
 
7135
- const STAGED_HOMES_DIR = join$1(homedir(), ".svamp", "staged-homes");
7270
+ const STAGED_HOMES_DIR = join(homedir(), ".svamp", "staged-homes");
7136
7271
  const SENSITIVE_ENV_VARS = [
7137
7272
  "HYPHA_TOKEN",
7138
7273
  "HYPHA_CLIENT_ID",
@@ -7148,23 +7283,23 @@ const SENSITIVE_ENV_VARS = [
7148
7283
  ];
7149
7284
  async function stageCredentialsForSharing(sessionId) {
7150
7285
  const realHome = homedir();
7151
- const realClaudeDir = join$1(realHome, ".claude");
7286
+ const realClaudeDir = join(realHome, ".claude");
7152
7287
  await mkdir(STAGED_HOMES_DIR, { recursive: true });
7153
- const tmpHome = join$1(STAGED_HOMES_DIR, sessionId);
7288
+ const tmpHome = join(STAGED_HOMES_DIR, sessionId);
7154
7289
  await mkdir(tmpHome, { recursive: true });
7155
- const stagedClaudeDir = join$1(tmpHome, ".claude");
7290
+ const stagedClaudeDir = join(tmpHome, ".claude");
7156
7291
  await mkdir(stagedClaudeDir, { recursive: true });
7157
7292
  const credentialFiles = ["credentials.json", ".credentials.json"];
7158
7293
  let credentialsCopied = false;
7159
7294
  for (const file of credentialFiles) {
7160
7295
  try {
7161
- await copyFile(join$1(realClaudeDir, file), join$1(stagedClaudeDir, file));
7296
+ await copyFile(join(realClaudeDir, file), join(stagedClaudeDir, file));
7162
7297
  credentialsCopied = true;
7163
7298
  } catch {
7164
7299
  }
7165
7300
  }
7166
7301
  if (!credentialsCopied && platform() === "darwin") {
7167
- const stagedCredFile = join$1(stagedClaudeDir, ".credentials.json");
7302
+ const stagedCredFile = join(stagedClaudeDir, ".credentials.json");
7168
7303
  const hasExistingCredentials = existsSync(stagedCredFile);
7169
7304
  if (!hasExistingCredentials) {
7170
7305
  try {
@@ -7182,25 +7317,25 @@ async function stageCredentialsForSharing(sessionId) {
7182
7317
  }
7183
7318
  }
7184
7319
  try {
7185
- await copyFile(join$1(realHome, ".gitconfig"), join$1(tmpHome, ".gitconfig"));
7320
+ await copyFile(join(realHome, ".gitconfig"), join(tmpHome, ".gitconfig"));
7186
7321
  } catch {
7187
7322
  }
7188
7323
  try {
7189
7324
  await copyFile(
7190
- join$1(realHome, ".gitignore_global"),
7191
- join$1(tmpHome, ".gitignore_global")
7325
+ join(realHome, ".gitignore_global"),
7326
+ join(tmpHome, ".gitignore_global")
7192
7327
  );
7193
7328
  } catch {
7194
7329
  }
7195
- const claudeJsonPath = join$1(tmpHome, ".claude.json");
7330
+ const claudeJsonPath = join(tmpHome, ".claude.json");
7196
7331
  if (!existsSync(claudeJsonPath)) {
7197
7332
  try {
7198
7333
  await writeFile(claudeJsonPath, "{}");
7199
7334
  } catch {
7200
7335
  }
7201
7336
  }
7202
- const realSkillsDir = join$1(realClaudeDir, "skills");
7203
- const stagedSkillsDir = join$1(stagedClaudeDir, "skills");
7337
+ const realSkillsDir = join(realClaudeDir, "skills");
7338
+ const stagedSkillsDir = join(stagedClaudeDir, "skills");
7204
7339
  try {
7205
7340
  await copyDirRecursive(realSkillsDir, stagedSkillsDir);
7206
7341
  } catch {
@@ -7234,7 +7369,7 @@ async function sweepOrphanedStagedHomes(activeSessionIds) {
7234
7369
  for (const entry of entries) {
7235
7370
  if (!entry.isDirectory()) continue;
7236
7371
  if (active.has(entry.name)) continue;
7237
- const path = join$1(STAGED_HOMES_DIR, entry.name);
7372
+ const path = join(STAGED_HOMES_DIR, entry.name);
7238
7373
  try {
7239
7374
  await rm(path, { recursive: true, force: true });
7240
7375
  removed.push(entry.name);
@@ -7249,8 +7384,8 @@ async function copyDirRecursive(src, dest) {
7249
7384
  await mkdir(dest, { recursive: true });
7250
7385
  const entries = await readdir(src, { withFileTypes: true });
7251
7386
  for (const entry of entries) {
7252
- const srcPath = join$1(src, entry.name);
7253
- const destPath = join$1(dest, entry.name);
7387
+ const srcPath = join(src, entry.name);
7388
+ const destPath = join(dest, entry.name);
7254
7389
  if (entry.isDirectory()) {
7255
7390
  await copyDirRecursive(srcPath, destPath);
7256
7391
  } else if (entry.isFile()) {
@@ -7302,8 +7437,8 @@ function resolveHyphaProxyUrl() {
7302
7437
  }
7303
7438
  }
7304
7439
  function envFilePath() {
7305
- const svampHome = process.env.SVAMP_HOME || join$1(homedir(), ".svamp");
7306
- return join$1(svampHome, ".env");
7440
+ const svampHome = process.env.SVAMP_HOME || join(homedir(), ".svamp");
7441
+ return join(svampHome, ".env");
7307
7442
  }
7308
7443
  function readEnvLines() {
7309
7444
  const file = envFilePath();
@@ -7312,12 +7447,12 @@ function readEnvLines() {
7312
7447
  }
7313
7448
  function writeEnvLines(lines) {
7314
7449
  const file = envFilePath();
7315
- const dir = join$1(file, "..");
7316
- if (!existsSync(dir)) mkdirSync$1(dir, { recursive: true });
7450
+ const dir = join(file, "..");
7451
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
7317
7452
  while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
7318
7453
  lines.pop();
7319
7454
  }
7320
- writeFileSync$1(file, lines.join("\n") + "\n", "utf-8");
7455
+ writeFileSync(file, lines.join("\n") + "\n", "utf-8");
7321
7456
  }
7322
7457
  function updateEnvFile(updates) {
7323
7458
  const lines = readEnvLines();
@@ -7468,10 +7603,10 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
7468
7603
  });
7469
7604
 
7470
7605
  function svampHome() {
7471
- return process.env.SVAMP_HOME || join(homedir$1(), ".svamp");
7606
+ return process.env.SVAMP_HOME || join$1(homedir$1(), ".svamp");
7472
7607
  }
7473
7608
  function cacheFile() {
7474
- return join(svampHome(), "instance-config.json");
7609
+ return join$1(svampHome(), "instance-config.json");
7475
7610
  }
7476
7611
  const CONFIG_FILENAME = "svamp.json";
7477
7612
  let _config = null;
@@ -7514,8 +7649,8 @@ function readCache() {
7514
7649
  }
7515
7650
  function writeCache(cfg) {
7516
7651
  try {
7517
- mkdirSync(svampHome(), { recursive: true });
7518
- writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
7652
+ mkdirSync$1(svampHome(), { recursive: true });
7653
+ writeFileSync$1(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
7519
7654
  } catch {
7520
7655
  }
7521
7656
  }
@@ -8033,7 +8168,7 @@ function escapeHtml(s) {
8033
8168
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8034
8169
  }
8035
8170
 
8036
- const SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
8171
+ const SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
8037
8172
  function getSkillsWorkspaceName() {
8038
8173
  return getSkillsWorkspace();
8039
8174
  }
@@ -8764,7 +8899,7 @@ const REFRESH_BUFFER_MS = 60 * 60 * 1e3;
8764
8899
  const OAUTH_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
8765
8900
  const REFRESH_TIMEOUT_MS = 15e3;
8766
8901
  function getCredentialsPath() {
8767
- return join$1(homedir(), ".claude", ".credentials.json");
8902
+ return join(homedir(), ".claude", ".credentials.json");
8768
8903
  }
8769
8904
  async function readCredentials() {
8770
8905
  const path = getCredentialsPath();
@@ -8925,14 +9060,14 @@ function resolveContextWindow(opts) {
8925
9060
  return candidate;
8926
9061
  }
8927
9062
 
8928
- const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
9063
+ const SVAMP_HOME$1 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
8929
9064
  function generateHookSettings(portOrOptions = {}) {
8930
9065
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
8931
- const hooksDir = join$1(SVAMP_HOME$1, "tmp", "hooks");
8932
- mkdirSync$1(hooksDir, { recursive: true });
9066
+ const hooksDir = join(SVAMP_HOME$1, "tmp", "hooks");
9067
+ mkdirSync(hooksDir, { recursive: true });
8933
9068
  const id = opts.id || String(process.pid);
8934
- const validatorPath = join$1(hooksDir, `image-validator-${id}.cjs`);
8935
- writeFileSync$1(validatorPath, IMAGE_VALIDATOR_SCRIPT, { mode: 493 });
9069
+ const validatorPath = join(hooksDir, `image-validator-${id}.cjs`);
9070
+ writeFileSync(validatorPath, IMAGE_VALIDATOR_SCRIPT, { mode: 493 });
8936
9071
  const cleanupPaths = [validatorPath];
8937
9072
  const hooks = {
8938
9073
  PreToolUse: [
@@ -8949,7 +9084,7 @@ function generateHookSettings(portOrOptions = {}) {
8949
9084
  ]
8950
9085
  };
8951
9086
  if (typeof opts.sessionStartPort === "number" && opts.sessionStartPort > 0) {
8952
- const forwarderPath = join$1(hooksDir, `forwarder-${id}.cjs`);
9087
+ const forwarderPath = join(hooksDir, `forwarder-${id}.cjs`);
8953
9088
  const forwarderCode = `#!/usr/bin/env node
8954
9089
  const http = require('http');
8955
9090
  const port = parseInt(process.argv[2], 10);
@@ -8968,7 +9103,7 @@ process.stdin.on('end', () => {
8968
9103
  });
8969
9104
  process.stdin.resume();
8970
9105
  `;
8971
- writeFileSync$1(forwarderPath, forwarderCode, { mode: 493 });
9106
+ writeFileSync(forwarderPath, forwarderCode, { mode: 493 });
8972
9107
  cleanupPaths.push(forwarderPath);
8973
9108
  hooks.SessionStart = [
8974
9109
  {
@@ -8982,8 +9117,8 @@ process.stdin.resume();
8982
9117
  }
8983
9118
  ];
8984
9119
  }
8985
- const settingsPath = join$1(hooksDir, `session-hook-${id}.json`);
8986
- writeFileSync$1(settingsPath, JSON.stringify({ hooks }, null, 2));
9120
+ const settingsPath = join(hooksDir, `session-hook-${id}.json`);
9121
+ writeFileSync(settingsPath, JSON.stringify({ hooks }, null, 2));
8987
9122
  cleanupPaths.push(settingsPath);
8988
9123
  const cleanup = () => {
8989
9124
  for (const p of cleanupPaths) {
@@ -9145,7 +9280,7 @@ async function readSessionFileBase64(resolvedPath) {
9145
9280
 
9146
9281
  const __filename$1 = fileURLToPath(import.meta.url);
9147
9282
  const __dirname$1 = dirname(__filename$1);
9148
- const CLAUDE_SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
9283
+ const CLAUDE_SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
9149
9284
  function looksLikeClaudeError(line) {
9150
9285
  const l = line.toLowerCase();
9151
9286
  return l.includes("api error") || l.includes("request rejected") || l.includes("error:") || l.includes("overloaded") || l.includes("rate limit") || l.includes("unauthorized") || l.includes("forbidden") || /\b(4\d\d|5\d\d)\b/.test(l) && (l.includes("status") || l.includes("error") || l.includes("rejected"));
@@ -9185,7 +9320,7 @@ function buildClaudeErrorHint(text, apiErrorStatus) {
9185
9320
  }
9186
9321
  function readSkillVersion(skillDir) {
9187
9322
  try {
9188
- const md = readFileSync$1(join(skillDir, "SKILL.md"), "utf-8");
9323
+ const md = readFileSync$1(join$1(skillDir, "SKILL.md"), "utf-8");
9189
9324
  const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
9190
9325
  if (!m) return null;
9191
9326
  const versionLine = m[1].split("\n").find((l) => /^\s*version\s*:/.test(l));
@@ -9211,18 +9346,18 @@ async function installSkillFromEndpoint(name, baseUrl) {
9211
9346
  const index = await resp.json();
9212
9347
  const files = index.files || [];
9213
9348
  if (files.length === 0) throw new Error(`Skill index at ${baseUrl} has no files`);
9214
- const targetDir = join(CLAUDE_SKILLS_DIR, name);
9215
- mkdirSync(targetDir, { recursive: true });
9349
+ const targetDir = join$1(CLAUDE_SKILLS_DIR, name);
9350
+ mkdirSync$1(targetDir, { recursive: true });
9216
9351
  for (const filePath of files) {
9217
9352
  if (!filePath) continue;
9218
9353
  const url = `${baseUrl}${filePath}`;
9219
9354
  const fileResp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
9220
9355
  if (!fileResp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${fileResp.status}`);
9221
9356
  const content = await fileResp.text();
9222
- const localPath = join(targetDir, filePath);
9357
+ const localPath = join$1(targetDir, filePath);
9223
9358
  if (!localPath.startsWith(targetDir + "/")) continue;
9224
- mkdirSync(dirname(localPath), { recursive: true });
9225
- writeFileSync(localPath, content, "utf-8");
9359
+ mkdirSync$1(dirname(localPath), { recursive: true });
9360
+ writeFileSync$1(localPath, content, "utf-8");
9226
9361
  }
9227
9362
  }
9228
9363
  async function installSkillFromMarketplace(name) {
@@ -9246,26 +9381,26 @@ async function installSkillFromMarketplace(name) {
9246
9381
  }
9247
9382
  const files = await collectFiles();
9248
9383
  if (files.length === 0) throw new Error(`Skill ${name} has no files in marketplace`);
9249
- const targetDir = join(CLAUDE_SKILLS_DIR, name);
9250
- mkdirSync(targetDir, { recursive: true });
9384
+ const targetDir = join$1(CLAUDE_SKILLS_DIR, name);
9385
+ mkdirSync$1(targetDir, { recursive: true });
9251
9386
  for (const filePath of files) {
9252
9387
  const url = `${BASE}/files/${filePath}`;
9253
9388
  const resp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
9254
9389
  if (!resp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${resp.status}`);
9255
9390
  const content = await resp.text();
9256
- const localPath = join(targetDir, filePath);
9391
+ const localPath = join$1(targetDir, filePath);
9257
9392
  if (!localPath.startsWith(targetDir + "/")) continue;
9258
- mkdirSync(dirname(localPath), { recursive: true });
9259
- writeFileSync(localPath, content, "utf-8");
9393
+ mkdirSync$1(dirname(localPath), { recursive: true });
9394
+ writeFileSync$1(localPath, content, "utf-8");
9260
9395
  }
9261
9396
  }
9262
9397
  function getBundledSkillsDir() {
9263
9398
  try {
9264
9399
  const here = fileURLToPath(import.meta.url);
9265
9400
  const candidates = [
9266
- join(dirname(here), "..", "bin", "skills"),
9401
+ join$1(dirname(here), "..", "bin", "skills"),
9267
9402
  // built dist/ layout
9268
- join(dirname(here), "..", "..", "bin", "skills")
9403
+ join$1(dirname(here), "..", "..", "bin", "skills")
9269
9404
  // src/daemon → bin layout via tsx
9270
9405
  ];
9271
9406
  for (const c of candidates) {
@@ -9278,17 +9413,17 @@ function getBundledSkillsDir() {
9278
9413
  function installBundledSkill(name) {
9279
9414
  const bundledDir = getBundledSkillsDir();
9280
9415
  if (!bundledDir) throw new Error(`Bundled skills directory not found`);
9281
- const src = join(bundledDir, name);
9416
+ const src = join$1(bundledDir, name);
9282
9417
  if (!existsSync$1(src)) throw new Error(`Bundled skill not found: ${src}`);
9283
- const dst = join(CLAUDE_SKILLS_DIR, name);
9284
- mkdirSync(dst, { recursive: true });
9418
+ const dst = join$1(CLAUDE_SKILLS_DIR, name);
9419
+ mkdirSync$1(dst, { recursive: true });
9285
9420
  function copyDir(s, d) {
9286
- mkdirSync(d, { recursive: true });
9421
+ mkdirSync$1(d, { recursive: true });
9287
9422
  for (const entry of readdirSync$1(s, { withFileTypes: true })) {
9288
- const sp = join(s, entry.name);
9289
- const dp = join(d, entry.name);
9423
+ const sp = join$1(s, entry.name);
9424
+ const dp = join$1(d, entry.name);
9290
9425
  if (entry.isDirectory()) copyDir(sp, dp);
9291
- else if (entry.isFile()) writeFileSync(dp, readFileSync$1(sp));
9426
+ else if (entry.isFile()) writeFileSync$1(dp, readFileSync$1(sp));
9292
9427
  }
9293
9428
  }
9294
9429
  copyDir(src, dst);
@@ -9296,7 +9431,7 @@ function installBundledSkill(name) {
9296
9431
  function readBundledSkillVersion(name) {
9297
9432
  const bundledDir = getBundledSkillsDir();
9298
9433
  if (!bundledDir) return null;
9299
- return readSkillVersion(join(bundledDir, name));
9434
+ return readSkillVersion(join$1(bundledDir, name));
9300
9435
  }
9301
9436
  function preventMachineSleep(logger) {
9302
9437
  if (process.platform === "darwin") {
@@ -9404,7 +9539,7 @@ async function ensureAutoInstalledSkills(logger) {
9404
9539
  }
9405
9540
  ];
9406
9541
  for (const task of tasks) {
9407
- const targetDir = join(CLAUDE_SKILLS_DIR, task.name);
9542
+ const targetDir = join$1(CLAUDE_SKILLS_DIR, task.name);
9408
9543
  const installed = existsSync$1(targetDir);
9409
9544
  if (!installed) {
9410
9545
  try {
@@ -9445,20 +9580,20 @@ function loadEnvFile(path) {
9445
9580
  return true;
9446
9581
  }
9447
9582
  function loadDotEnv() {
9448
- const svampEnv = join(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
9583
+ const svampEnv = join$1(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
9449
9584
  if (!loadEnvFile(svampEnv)) {
9450
- const hyphaEnv = join(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
9585
+ const hyphaEnv = join$1(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
9451
9586
  loadEnvFile(hyphaEnv);
9452
9587
  }
9453
9588
  }
9454
9589
  loadDotEnv();
9455
- const SVAMP_HOME = process.env.SVAMP_HOME || join(os$1.homedir(), ".svamp");
9456
- const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
9457
- const DAEMON_LOCK_FILE = join(SVAMP_HOME, "daemon.lock");
9458
- const DAEMON_STOP_MARKER_FILE = join(SVAMP_HOME, "daemon.stop");
9590
+ const SVAMP_HOME = process.env.SVAMP_HOME || join$1(os$1.homedir(), ".svamp");
9591
+ const DAEMON_STATE_FILE = join$1(SVAMP_HOME, "daemon.state.json");
9592
+ const DAEMON_LOCK_FILE = join$1(SVAMP_HOME, "daemon.lock");
9593
+ const DAEMON_STOP_MARKER_FILE = join$1(SVAMP_HOME, "daemon.stop");
9459
9594
  function writeStopMarker(reason) {
9460
9595
  try {
9461
- writeFileSync(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
9596
+ writeFileSync$1(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
9462
9597
  `, "utf-8");
9463
9598
  } catch {
9464
9599
  }
@@ -9476,11 +9611,11 @@ function stopMarkerExists() {
9476
9611
  return false;
9477
9612
  }
9478
9613
  }
9479
- const LOGS_DIR = join(SVAMP_HOME, "logs");
9480
- const SESSION_INDEX_FILE = join(SVAMP_HOME, "sessions-index.json");
9614
+ const LOGS_DIR = join$1(SVAMP_HOME, "logs");
9615
+ const SESSION_INDEX_FILE = join$1(SVAMP_HOME, "sessions-index.json");
9481
9616
  function readPackageVersion() {
9482
9617
  try {
9483
- const pkgPath = join(__dirname$1, "../package.json");
9618
+ const pkgPath = join$1(__dirname$1, "../package.json");
9484
9619
  if (existsSync$1(pkgPath)) {
9485
9620
  return JSON.parse(readFileSync$1(pkgPath, "utf-8")).version || "unknown";
9486
9621
  }
@@ -9490,7 +9625,7 @@ function readPackageVersion() {
9490
9625
  }
9491
9626
  const DAEMON_VERSION = readPackageVersion();
9492
9627
  function loadAgentConfig() {
9493
- const configPath = join(SVAMP_HOME, "agent-config.json");
9628
+ const configPath = join$1(SVAMP_HOME, "agent-config.json");
9494
9629
  if (existsSync$1(configPath)) {
9495
9630
  try {
9496
9631
  return JSON.parse(readFileSync$1(configPath, "utf-8"));
@@ -9501,19 +9636,19 @@ function loadAgentConfig() {
9501
9636
  return {};
9502
9637
  }
9503
9638
  function getSessionSvampDir(directory) {
9504
- return join(directory, ".svamp");
9639
+ return join$1(directory, ".svamp");
9505
9640
  }
9506
9641
  function getSessionDir(directory, sessionId) {
9507
- return join(getSessionSvampDir(directory), sessionId);
9642
+ return join$1(getSessionSvampDir(directory), sessionId);
9508
9643
  }
9509
9644
  function getSessionFilePath(directory, sessionId) {
9510
- return join(getSessionDir(directory, sessionId), "session.json");
9645
+ return join$1(getSessionDir(directory, sessionId), "session.json");
9511
9646
  }
9512
9647
  function getSessionMessagesPath(directory, sessionId) {
9513
- return join(getSessionDir(directory, sessionId), "messages.jsonl");
9648
+ return join$1(getSessionDir(directory, sessionId), "messages.jsonl");
9514
9649
  }
9515
9650
  function getSvampConfigPath(directory, sessionId) {
9516
- return join(getSessionDir(directory, sessionId), "config.json");
9651
+ return join$1(getSessionDir(directory, sessionId), "config.json");
9517
9652
  }
9518
9653
  function readSvampConfig(configPath) {
9519
9654
  try {
@@ -9523,19 +9658,19 @@ function readSvampConfig(configPath) {
9523
9658
  return {};
9524
9659
  }
9525
9660
  function writeSvampConfig(configPath, config) {
9526
- mkdirSync(dirname(configPath), { recursive: true });
9661
+ mkdirSync$1(dirname(configPath), { recursive: true });
9527
9662
  const content = JSON.stringify(config, null, 2);
9528
9663
  const tmpPath = configPath + ".tmp";
9529
- writeFileSync(tmpPath, content);
9530
- renameSync(tmpPath, configPath);
9664
+ writeFileSync$1(tmpPath, content);
9665
+ renameSync$1(tmpPath, configPath);
9531
9666
  return content;
9532
9667
  }
9533
9668
  function getLoopDir(directory) {
9534
- return join(directory, ".claude", "loop");
9669
+ return join$1(directory, ".claude", "loop");
9535
9670
  }
9536
9671
  function readLoopState(directory) {
9537
9672
  try {
9538
- const p = join(getLoopDir(directory), "loop-state.json");
9673
+ const p = join$1(getLoopDir(directory), "loop-state.json");
9539
9674
  if (!existsSync$1(p)) return null;
9540
9675
  return JSON.parse(readFileSync$1(p, "utf-8"));
9541
9676
  } catch {
@@ -9546,10 +9681,21 @@ function isLoopActive(directory) {
9546
9681
  const s = readLoopState(directory);
9547
9682
  return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
9548
9683
  }
9684
+ function loopOwnerSession(directory) {
9685
+ const s = readLoopState(directory);
9686
+ if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return null;
9687
+ return typeof s.session_id === "string" ? s.session_id : null;
9688
+ }
9689
+ function isLoopActiveForSession(directory, sessionId) {
9690
+ const s = readLoopState(directory);
9691
+ if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return false;
9692
+ if (typeof s.session_id !== "string") return true;
9693
+ return s.session_id === sessionId;
9694
+ }
9549
9695
  function resolveLoopInit() {
9550
9696
  const candidates = [
9551
- join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
9552
- ...getBundledSkillsDir() ? [join(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
9697
+ join$1(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
9698
+ ...getBundledSkillsDir() ? [join$1(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
9553
9699
  ];
9554
9700
  for (const c of candidates) if (existsSync$1(c)) return c;
9555
9701
  return null;
@@ -9563,19 +9709,20 @@ function initLoop(directory, cfg) {
9563
9709
  if (typeof cfg.maxIterations === "number") args.push("--max", String(cfg.maxIterations));
9564
9710
  args.push("--evaluator", cfg.evaluator === false ? "off" : "on");
9565
9711
  if (cfg.model) args.push("--model", cfg.model);
9712
+ if (cfg.sessionId) args.push("--session", cfg.sessionId);
9566
9713
  const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
9567
9714
  return res.status === 0;
9568
9715
  }
9569
9716
  function deactivateLoop(directory) {
9570
9717
  try {
9571
- const p = join(getLoopDir(directory), "loop-state.json");
9718
+ const p = join$1(getLoopDir(directory), "loop-state.json");
9572
9719
  if (!existsSync$1(p)) return;
9573
9720
  const s = JSON.parse(readFileSync$1(p, "utf-8"));
9574
9721
  s.active = false;
9575
9722
  s.phase = "cancelled";
9576
9723
  const tmp = p + ".tmp";
9577
- writeFileSync(tmp, JSON.stringify(s, null, 2));
9578
- renameSync(tmp, p);
9724
+ writeFileSync$1(tmp, JSON.stringify(s, null, 2));
9725
+ renameSync$1(tmp, p);
9579
9726
  } catch {
9580
9727
  }
9581
9728
  }
@@ -9664,7 +9811,8 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9664
9811
  criteria: typeof lp.criteria === "string" && lp.criteria.trim() ? lp.criteria.trim() : void 0,
9665
9812
  oracle,
9666
9813
  maxIterations,
9667
- evaluator
9814
+ evaluator,
9815
+ sessionId
9668
9816
  });
9669
9817
  if (ok) {
9670
9818
  const existingQueue = getMetadata().messageQueue || [];
@@ -9712,7 +9860,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9712
9860
  let watcher = null;
9713
9861
  try {
9714
9862
  const configDir = dirname(configPath);
9715
- mkdirSync(configDir, { recursive: true });
9863
+ mkdirSync$1(configDir, { recursive: true });
9716
9864
  watcher = watch(configDir, (eventType, filename) => {
9717
9865
  if (filename === "config.json") configChecker();
9718
9866
  });
@@ -9738,18 +9886,18 @@ function loadSessionIndex() {
9738
9886
  }
9739
9887
  function saveSessionIndex(index) {
9740
9888
  const tmp = SESSION_INDEX_FILE + ".tmp";
9741
- writeFileSync(tmp, JSON.stringify(index, null, 2), "utf-8");
9742
- renameSync(tmp, SESSION_INDEX_FILE);
9889
+ writeFileSync$1(tmp, JSON.stringify(index, null, 2), "utf-8");
9890
+ renameSync$1(tmp, SESSION_INDEX_FILE);
9743
9891
  }
9744
9892
  function saveSession(session) {
9745
9893
  const sessionDir = getSessionDir(session.directory, session.sessionId);
9746
9894
  if (!existsSync$1(sessionDir)) {
9747
- mkdirSync(sessionDir, { recursive: true });
9895
+ mkdirSync$1(sessionDir, { recursive: true });
9748
9896
  }
9749
9897
  const filePath = getSessionFilePath(session.directory, session.sessionId);
9750
9898
  const tmpPath = filePath + ".tmp";
9751
- writeFileSync(tmpPath, JSON.stringify(session, null, 2), "utf-8");
9752
- renameSync(tmpPath, filePath);
9899
+ writeFileSync$1(tmpPath, JSON.stringify(session, null, 2), "utf-8");
9900
+ renameSync$1(tmpPath, filePath);
9753
9901
  const index = loadSessionIndex();
9754
9902
  index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
9755
9903
  saveSessionIndex(index);
@@ -9793,8 +9941,8 @@ function markSessionAsArchived(sessionId) {
9793
9941
  if (data.stopped === true) return true;
9794
9942
  data.stopped = true;
9795
9943
  const tmpPath = filePath + ".tmp";
9796
- writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9797
- renameSync(tmpPath, filePath);
9944
+ writeFileSync$1(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9945
+ renameSync$1(tmpPath, filePath);
9798
9946
  return true;
9799
9947
  } catch {
9800
9948
  return false;
@@ -9811,8 +9959,8 @@ function clearSessionArchivedFlag(sessionId) {
9811
9959
  if (data.stopped) {
9812
9960
  delete data.stopped;
9813
9961
  const tmpPath = filePath + ".tmp";
9814
- writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9815
- renameSync(tmpPath, filePath);
9962
+ writeFileSync$1(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9963
+ renameSync$1(tmpPath, filePath);
9816
9964
  }
9817
9965
  return data;
9818
9966
  } catch {
@@ -9844,15 +9992,15 @@ function loadPersistedSessions() {
9844
9992
  }
9845
9993
  function ensureHomeDir() {
9846
9994
  if (!existsSync$1(SVAMP_HOME)) {
9847
- mkdirSync(SVAMP_HOME, { recursive: true });
9995
+ mkdirSync$1(SVAMP_HOME, { recursive: true });
9848
9996
  }
9849
9997
  if (!existsSync$1(LOGS_DIR)) {
9850
- mkdirSync(LOGS_DIR, { recursive: true });
9998
+ mkdirSync$1(LOGS_DIR, { recursive: true });
9851
9999
  }
9852
10000
  }
9853
10001
  function createLogger() {
9854
10002
  ensureHomeDir();
9855
- const logFile = join(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
10003
+ const logFile = join$1(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
9856
10004
  return {
9857
10005
  logFilePath: logFile,
9858
10006
  log: (...args) => {
@@ -9876,8 +10024,8 @@ function createLogger() {
9876
10024
  function writeDaemonStateFile(state) {
9877
10025
  ensureHomeDir();
9878
10026
  const tmpPath = DAEMON_STATE_FILE + ".tmp";
9879
- writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
9880
- renameSync(tmpPath, DAEMON_STATE_FILE);
10027
+ writeFileSync$1(tmpPath, JSON.stringify(state, null, 2), "utf-8");
10028
+ renameSync$1(tmpPath, DAEMON_STATE_FILE);
9881
10029
  }
9882
10030
  function readDaemonStateFile() {
9883
10031
  try {
@@ -10076,7 +10224,7 @@ async function startDaemon(options) {
10076
10224
  logger.log('Warning: No HYPHA_TOKEN set. Run "svamp login" to authenticate.');
10077
10225
  logger.log("Connecting anonymously...");
10078
10226
  }
10079
- const machineIdFile = join(SVAMP_HOME, "machine-id");
10227
+ const machineIdFile = join$1(SVAMP_HOME, "machine-id");
10080
10228
  let machineId = process.env.SVAMP_MACHINE_ID;
10081
10229
  if (!machineId) {
10082
10230
  if (existsSync$1(machineIdFile)) {
@@ -10085,7 +10233,7 @@ async function startDaemon(options) {
10085
10233
  if (!machineId) {
10086
10234
  machineId = `machine-${os$1.hostname()}-${randomUUID$1().slice(0, 8)}`;
10087
10235
  try {
10088
- writeFileSync(machineIdFile, machineId, "utf-8");
10236
+ writeFileSync$1(machineIdFile, machineId, "utf-8");
10089
10237
  } catch {
10090
10238
  }
10091
10239
  }
@@ -10095,10 +10243,10 @@ async function startDaemon(options) {
10095
10243
  logger.log(` Workspace: ${hyphaWorkspace || "(default)"}`);
10096
10244
  logger.log(` Machine ID: ${machineId}`);
10097
10245
  let server = null;
10098
- const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
10246
+ const supervisor = new ProcessSupervisor(join$1(SVAMP_HOME, "processes"));
10099
10247
  await supervisor.init();
10100
10248
  const tunnels = /* @__PURE__ */ new Map();
10101
- const EXPOSED_TUNNELS_FILE = join(SVAMP_HOME, "exposed-tunnels.json");
10249
+ const EXPOSED_TUNNELS_FILE = join$1(SVAMP_HOME, "exposed-tunnels.json");
10102
10250
  function loadExposedTunnels() {
10103
10251
  try {
10104
10252
  if (!existsSync$1(EXPOSED_TUNNELS_FILE)) return [];
@@ -10111,8 +10259,8 @@ async function startDaemon(options) {
10111
10259
  }
10112
10260
  function saveExposedTunnels(specs) {
10113
10261
  try {
10114
- mkdirSync(SVAMP_HOME, { recursive: true });
10115
- writeFileSync(EXPOSED_TUNNELS_FILE, JSON.stringify({ tunnels: specs }, null, 2));
10262
+ mkdirSync$1(SVAMP_HOME, { recursive: true });
10263
+ writeFileSync$1(EXPOSED_TUNNELS_FILE, JSON.stringify({ tunnels: specs }, null, 2));
10116
10264
  } catch (err) {
10117
10265
  logger.log(`[exposed-tunnels] Persist failed: ${err.message}`);
10118
10266
  }
@@ -10126,7 +10274,7 @@ async function startDaemon(options) {
10126
10274
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10127
10275
  saveExposedTunnels(list);
10128
10276
  }
10129
- const { ServeManager } = await import('./serveManager-B757hHGd.mjs');
10277
+ const { ServeManager } = await import('./serveManager-Bq33kB5r.mjs');
10130
10278
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10131
10279
  ensureAutoInstalledSkills(logger).catch(() => {
10132
10280
  });
@@ -10238,7 +10386,7 @@ async function startDaemon(options) {
10238
10386
  }
10239
10387
  }, shouldAutoAllow2 = function(toolName, toolInput) {
10240
10388
  if (toolName === "AskUserQuestion") {
10241
- return isLoopActive(directory);
10389
+ return isLoopActiveForSession(directory, sessionId);
10242
10390
  }
10243
10391
  if (toolName === "Bash") {
10244
10392
  const inputObj = toolInput;
@@ -10251,7 +10399,7 @@ async function startDaemon(options) {
10251
10399
  } else if (allowedTools.has(toolName)) {
10252
10400
  return true;
10253
10401
  }
10254
- if (isLoopActive(directory)) return true;
10402
+ if (isLoopActiveForSession(directory, sessionId)) return true;
10255
10403
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
10256
10404
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
10257
10405
  return false;
@@ -10317,8 +10465,8 @@ async function startDaemon(options) {
10317
10465
  machineId,
10318
10466
  homeDir: os$1.homedir(),
10319
10467
  svampHomeDir: SVAMP_HOME,
10320
- svampLibDir: join(__dirname$1, ".."),
10321
- svampToolsDir: join(__dirname$1, "..", "tools"),
10468
+ svampLibDir: join$1(__dirname$1, ".."),
10469
+ svampToolsDir: join$1(__dirname$1, "..", "tools"),
10322
10470
  startedFromDaemon: true,
10323
10471
  startedBy: "daemon",
10324
10472
  lifecycleState: resumeSessionId ? "idle" : "starting",
@@ -10425,7 +10573,7 @@ async function startDaemon(options) {
10425
10573
  if (persisted && persisted.sessionId !== sessionId) {
10426
10574
  const oldDir = persisted.directory || directory;
10427
10575
  const newSessionDir = getSessionDir(directory, sessionId);
10428
- if (!existsSync$1(newSessionDir)) mkdirSync(newSessionDir, { recursive: true });
10576
+ if (!existsSync$1(newSessionDir)) mkdirSync$1(newSessionDir, { recursive: true });
10429
10577
  const oldMsgFile = getSessionMessagesPath(oldDir, persisted.sessionId);
10430
10578
  const newMsgFile = getSessionMessagesPath(directory, sessionId);
10431
10579
  try {
@@ -10616,6 +10764,12 @@ async function startDaemon(options) {
10616
10764
  logger.log(`[Session ${sessionId}] Permission request: ${requestId} (corr=${correlationId}) tool=${toolName}`);
10617
10765
  if (shouldAutoAllow2(toolName, toolInput)) {
10618
10766
  logger.log(`[Session ${sessionId}] Auto-allowing ${toolName} (mode=${currentPermissionMode})`);
10767
+ if (toolName === "AskUserQuestion") {
10768
+ sessionService.pushMessage(
10769
+ { 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" },
10770
+ "event"
10771
+ );
10772
+ }
10619
10773
  if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
10620
10774
  const controlResponse = JSON.stringify({
10621
10775
  type: "control_response",
@@ -11543,7 +11697,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11543
11697
  const children = [];
11544
11698
  await Promise.all(entries.map(async (entry) => {
11545
11699
  if (entry.isSymbolicLink()) return;
11546
- const childPath = join(p, entry.name);
11700
+ const childPath = join$1(p, entry.name);
11547
11701
  const childNode = await buildTree(childPath, entry.name, depth + 1);
11548
11702
  if (childNode) children.push(childNode);
11549
11703
  }));
@@ -11693,7 +11847,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11693
11847
  } else if (allowedTools.has(toolName)) {
11694
11848
  return true;
11695
11849
  }
11696
- if (isLoopActive(directory)) return true;
11850
+ if (isLoopActiveForSession(directory, sessionId)) return true;
11697
11851
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
11698
11852
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
11699
11853
  return false;
@@ -11706,8 +11860,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11706
11860
  machineId,
11707
11861
  homeDir: os$1.homedir(),
11708
11862
  svampHomeDir: SVAMP_HOME,
11709
- svampLibDir: join(__dirname$1, ".."),
11710
- svampToolsDir: join(__dirname$1, "..", "tools"),
11863
+ svampLibDir: join$1(__dirname$1, ".."),
11864
+ svampToolsDir: join$1(__dirname$1, "..", "tools"),
11711
11865
  startedFromDaemon: true,
11712
11866
  startedBy: "daemon",
11713
11867
  lifecycleState: "starting",
@@ -12012,7 +12166,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12012
12166
  const children = [];
12013
12167
  await Promise.all(entries.map(async (entry) => {
12014
12168
  if (entry.isSymbolicLink()) return;
12015
- const childPath = join(p, entry.name);
12169
+ const childPath = join$1(p, entry.name);
12016
12170
  const childNode = await buildTree(childPath, entry.name, depth + 1);
12017
12171
  if (childNode) children.push(childNode);
12018
12172
  }));
@@ -12248,8 +12402,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12248
12402
  };
12249
12403
  const archiveSession = (sessionId) => {
12250
12404
  logger.log(`Archiving session: ${sessionId}`);
12405
+ let loopDir;
12406
+ for (const s of pidToTrackedSession.values()) {
12407
+ if (s.svampSessionId === sessionId) {
12408
+ loopDir = s.directory;
12409
+ break;
12410
+ }
12411
+ }
12412
+ if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
12251
12413
  const wasInMemory = teardownTrackedSession(sessionId);
12252
12414
  const markedArchived = markSessionAsArchived(sessionId);
12415
+ if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
12416
+ deactivateLoop(loopDir);
12417
+ logger.log(`Deactivated loop for archived session ${sessionId}`);
12418
+ }
12253
12419
  if (wasInMemory || markedArchived) {
12254
12420
  logger.log(`Session ${sessionId} archived (inMemory=${wasInMemory}, persisted=${markedArchived})`);
12255
12421
  return true;
@@ -12293,8 +12459,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12293
12459
  };
12294
12460
  const deleteSession = (sessionId) => {
12295
12461
  logger.log(`Deleting session: ${sessionId}`);
12462
+ let loopDir;
12463
+ for (const s of pidToTrackedSession.values()) {
12464
+ if (s.svampSessionId === sessionId) {
12465
+ loopDir = s.directory;
12466
+ break;
12467
+ }
12468
+ }
12469
+ if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
12296
12470
  teardownTrackedSession(sessionId);
12297
12471
  deletePersistedSession(sessionId);
12472
+ if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
12473
+ deactivateLoop(loopDir);
12474
+ }
12298
12475
  logger.log(`Session ${sessionId} deleted`);
12299
12476
  return true;
12300
12477
  };
@@ -12351,7 +12528,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12351
12528
  svampVersion: "0.1.0 (hypha)",
12352
12529
  homeDir: defaultHomeDir,
12353
12530
  svampHomeDir: SVAMP_HOME,
12354
- svampLibDir: join(__dirname$1, ".."),
12531
+ svampLibDir: join$1(__dirname$1, ".."),
12355
12532
  displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
12356
12533
  isolationCapabilities,
12357
12534
  // Restore persisted sharing (possibly augmented with --share seed above),
@@ -12420,7 +12597,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12420
12597
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
12421
12598
  if (channelHttpPort > 0) {
12422
12599
  try {
12423
- const { createChannelHttpServer } = await import('./httpServer-D9qLS8ed.mjs');
12600
+ const { createChannelHttpServer } = await import('./httpServer-CWn3F-0t.mjs');
12424
12601
  const channelHttpServer = createChannelHttpServer({
12425
12602
  getSessionIds: () => {
12426
12603
  const ids = [];
@@ -12441,7 +12618,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12441
12618
  const specs = loadExposedTunnels();
12442
12619
  if (specs.length === 0) return;
12443
12620
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12444
- const { FrpcTunnel } = await import('./frpc-cJUGFtWY.mjs');
12621
+ const { FrpcTunnel } = await import('./frpc-B8ORdlOO.mjs');
12445
12622
  for (const spec of specs) {
12446
12623
  if (tunnels.has(spec.name)) continue;
12447
12624
  try {
@@ -12523,6 +12700,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12523
12700
  }
12524
12701
  const sessionsToAutoContinue = [];
12525
12702
  const sessionsToLoopResume = [];
12703
+ {
12704
+ const knownSessionIds = new Set(persistedSessions.map((p) => p.sessionId));
12705
+ const sweptDirs = /* @__PURE__ */ new Set();
12706
+ for (const p of persistedSessions) {
12707
+ if (sweptDirs.has(p.directory)) continue;
12708
+ sweptDirs.add(p.directory);
12709
+ const owner = loopOwnerSession(p.directory);
12710
+ if (owner && !knownSessionIds.has(owner)) {
12711
+ deactivateLoop(p.directory);
12712
+ logger.log(`[loop] Deactivated stale loop-state in ${p.directory} (owner session ${owner} no longer known)`);
12713
+ }
12714
+ }
12715
+ }
12526
12716
  if (persistedSessions.length > 0) {
12527
12717
  logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
12528
12718
  for (const persisted of persistedSessions) {
@@ -12567,7 +12757,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12567
12757
  if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
12568
12758
  sessionsToAutoContinue.push(persisted.sessionId);
12569
12759
  }
12570
- if (!isOrphaned && !persisted.wasProcessing && isLoopActive(persisted.directory)) {
12760
+ if (!isOrphaned && !persisted.wasProcessing && isLoopActiveForSession(persisted.directory, persisted.sessionId)) {
12571
12761
  sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
12572
12762
  }
12573
12763
  } else {
@@ -12614,7 +12804,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12614
12804
  }
12615
12805
  setTimeout(async () => {
12616
12806
  try {
12617
- if (!isLoopActive(sessDir)) return;
12807
+ if (!isLoopActiveForSession(sessDir, sessionId)) return;
12618
12808
  const prompt = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
12619
12809
  await rpc.sendMessage(
12620
12810
  JSON.stringify({
@@ -12892,8 +13082,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12892
13082
  if (existsSync$1(filePath)) {
12893
13083
  const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
12894
13084
  const tmpPath = filePath + ".tmp";
12895
- writeFileSync(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
12896
- renameSync(tmpPath, filePath);
13085
+ writeFileSync$1(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
13086
+ renameSync$1(tmpPath, filePath);
12897
13087
  markedCount++;
12898
13088
  }
12899
13089
  } catch {
@@ -12956,7 +13146,7 @@ async function stopDaemon(options) {
12956
13146
  const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
12957
13147
  writeStopMarker(`stopDaemon (${options?.cleanup ? "cleanup" : "quick"})`);
12958
13148
  const pidsToSignal = [];
12959
- const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
13149
+ const supervisorPidFile = join$1(SVAMP_HOME, "supervisor.pid");
12960
13150
  try {
12961
13151
  if (existsSync$1(supervisorPidFile)) {
12962
13152
  const supervisorPid = parseInt(readFileSync$1(supervisorPidFile, "utf-8").trim(), 10);
@@ -13061,7 +13251,7 @@ async function stopDaemon(options) {
13061
13251
  }
13062
13252
  }
13063
13253
  async function restartDaemon() {
13064
- const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
13254
+ const supervisorPidFile = join$1(SVAMP_HOME, "supervisor.pid");
13065
13255
  let supervisorPid = null;
13066
13256
  try {
13067
13257
  if (existsSync$1(supervisorPidFile)) {
@@ -13098,7 +13288,7 @@ async function restartDaemon() {
13098
13288
  });
13099
13289
  child.unref();
13100
13290
  }
13101
- const stateFile2 = join(SVAMP_HOME, "daemon.state.json");
13291
+ const stateFile2 = join$1(SVAMP_HOME, "daemon.state.json");
13102
13292
  for (let i = 0; i < 100; i++) {
13103
13293
  await new Promise((r) => setTimeout(r, 100));
13104
13294
  if (existsSync$1(stateFile2)) {
@@ -13122,7 +13312,7 @@ async function restartDaemon() {
13122
13312
  await doFullRestart("Failed to signal supervisor");
13123
13313
  return;
13124
13314
  }
13125
- const stateFile = join(SVAMP_HOME, "daemon.state.json");
13315
+ const stateFile = join$1(SVAMP_HOME, "daemon.state.json");
13126
13316
  for (let i = 0; i < 300; i++) {
13127
13317
  await new Promise((r) => setTimeout(r, 100));
13128
13318
  try {
@@ -13147,7 +13337,7 @@ async function restartDaemon() {
13147
13337
  function daemonStatus() {
13148
13338
  const state = readDaemonStateFile();
13149
13339
  if (!state) {
13150
- const plistPath = join(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
13340
+ const plistPath = join$1(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
13151
13341
  if (existsSync$1(plistPath)) {
13152
13342
  console.log("Status: Not running (launchd service installed \u2014 may be starting)");
13153
13343
  } else {