svamp-cli 0.2.323 → 0.2.324

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/{adminCommands-Bmgpks9U.mjs → adminCommands-CHE1wq5m.mjs} +1 -1
  2. package/dist/{agentCommands-CjbndvZ2.mjs → agentCommands-D7CV6DKL.mjs} +5 -5
  3. package/dist/{auth-7A9m78i5.mjs → auth-BfimMIpL.mjs} +1 -1
  4. package/dist/{cli-C3GwCq0z.mjs → cli-Dv0vM2P7.mjs} +82 -77
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-LMbH3V-v.mjs → commands-4hjEGB-I.mjs} +11 -11
  7. package/dist/{commands-DLQwUjpD.mjs → commands-BA4LVlQW.mjs} +2 -2
  8. package/dist/{commands-CZLE29kn.mjs → commands-BQ7UFowe.mjs} +3 -3
  9. package/dist/{commands-BARTdhcw.mjs → commands-CQK2nD3o.mjs} +7 -4
  10. package/dist/{commands-DVvR4X4G.mjs → commands-CdbvHh2p.mjs} +2 -2
  11. package/dist/{commands-CygFvR0a.mjs → commands-CuyCq5eZ.mjs} +3 -3
  12. package/dist/{commands-C8im5Q2J.mjs → commands-GRfwAHgR.mjs} +17 -5
  13. package/dist/{commands-gQM3K8Yf.mjs → commands-jaaQooYW.mjs} +1 -1
  14. package/dist/{fleet-C68s3qVt.mjs → fleet-CSh3MUcM.mjs} +3 -3
  15. package/dist/{headlessCli-0_H-JwW2.mjs → headlessCli-Zl5Yu4CL.mjs} +2 -2
  16. package/dist/index.mjs +1 -1
  17. package/dist/{notifyCommands-r0r8eyxA.mjs → notifyCommands-BQZXhfLt.mjs} +1 -1
  18. package/dist/{package-BGIfEYpX.mjs → package-DgboZg2o.mjs} +2 -2
  19. package/dist/{pinnedClaudeCode-C8CmQv3y.mjs → pinnedClaudeCode-8SZDu7O-.mjs} +1 -1
  20. package/dist/{rpc-BosOiRY-.mjs → rpc-BIzMNjqC.mjs} +1 -1
  21. package/dist/{rpc-DP4CiR0g.mjs → rpc-C1KIoUyz.mjs} +1 -1
  22. package/dist/{run-BiwqH1hw.mjs → run-CEp9YpUZ.mjs} +334 -53
  23. package/dist/{run-DAo9Lw4D.mjs → run-DKAROgSg.mjs} +1 -1
  24. package/dist/{scheduler-BN9GuvGN.mjs → scheduler-DIYulmdV.mjs} +1 -1
  25. package/dist/{serveCommands-C_E-RAiL.mjs → serveCommands-Bq5Cw3mE.mjs} +10 -10
  26. package/dist/{sideband-L0YNMJhj.mjs → sideband-C83128hf.mjs} +1 -1
  27. package/package.json +2 -2
  28. package/dist/supervisorLock-DmfzJx7B.mjs +0 -159
@@ -9,7 +9,7 @@ import { execFile, execSync, spawn, exec as exec$1 } from 'child_process';
9
9
  import * as crypto from 'crypto';
10
10
  import { randomUUID, createHash } from 'crypto';
11
11
  import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
12
- import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync as unlinkSync$1 } from 'node:fs';
12
+ import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync as unlinkSync$1, writeSync } from 'node:fs';
13
13
  import { exec, spawn as spawn$1, execSync as execSync$1, execFile as execFile$1, execFileSync } from 'node:child_process';
14
14
  import { promisify } from 'util';
15
15
  import * as http from 'http';
@@ -3811,7 +3811,7 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3811
3811
  const normRel = this.toSessionRelative(rootDir, m[2] || "");
3812
3812
  if (method === "GET" || method === "HEAD") {
3813
3813
  if (url.searchParams.get("list") === "1") {
3814
- const lex = containedPath(rootDir, normRel);
3814
+ const lex = this.resolveContainedPath(rootDir, normRel);
3815
3815
  if (!lex) {
3816
3816
  deny(403, "Forbidden");
3817
3817
  return;
@@ -4414,6 +4414,29 @@ var serveManager = /*#__PURE__*/Object.freeze({
4414
4414
  sanitizeMountForRole: sanitizeMountForRole
4415
4415
  });
4416
4416
 
4417
+ const EXEC_MAX_BUFFER = 16 * 1024 * 1024;
4418
+ function execFailureOutcome(err, stdout, stderr) {
4419
+ const e = err || {};
4420
+ const rawCode = e.code;
4421
+ const enobufs = rawCode === "ENOBUFS";
4422
+ const timedOut = rawCode === "ETIMEDOUT" || e.killed === true && !!e.signal;
4423
+ let exitCode;
4424
+ if (typeof rawCode === "number") exitCode = rawCode;
4425
+ else if (timedOut) exitCode = 124;
4426
+ else exitCode = 1;
4427
+ const base = stderr || e.message || "";
4428
+ let text = base;
4429
+ if (enobufs) {
4430
+ text = `output exceeded the ${Math.round(EXEC_MAX_BUFFER / (1024 * 1024))} MB buffer and was truncated (the command may have completed): ${base}`;
4431
+ } else if (timedOut) {
4432
+ text = `command timed out and was killed (it may have partially completed): ${base}`;
4433
+ }
4434
+ return { success: false, stdout: stdout || "", stderr: text, exitCode };
4435
+ }
4436
+ function execSuccessOutcome(stdout, stderr) {
4437
+ return { success: true, stdout: stdout || "", stderr: stderr || "", exitCode: 0 };
4438
+ }
4439
+
4417
4440
  const EXAMPLE_HYPHA_PROXY_URL = "https://proxy.hypha.aicell.io";
4418
4441
  const MODE_KEY = "SVAMP_CLAUDE_PROXY";
4419
4442
  const HYPHA_PROXY_URL_KEY = "SVAMP_HYPHA_PROXY_URL";
@@ -4844,6 +4867,13 @@ function stripV1(url) {
4844
4867
  }
4845
4868
  return trimmed;
4846
4869
  }
4870
+ function promptCacheTtlEnv(env, subscription) {
4871
+ if (env.FORCE_PROMPT_CACHING_5M) return void 0;
4872
+ if (!subscription) {
4873
+ return env.ENABLE_PROMPT_CACHING_1H === "1" ? "1" : void 0;
4874
+ }
4875
+ return env.ENABLE_PROMPT_CACHING_1H === "0" ? void 0 : "1";
4876
+ }
4847
4877
  function resolveAccountEnv(account, env = process.env, svampHome) {
4848
4878
  switch (account.method) {
4849
4879
  case "anthropic-api-key":
@@ -4852,7 +4882,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4852
4882
  claudeEnv: {
4853
4883
  ANTHROPIC_API_KEY: account.secret,
4854
4884
  ANTHROPIC_BASE_URL: account.baseUrl ? stripV1(account.baseUrl) : void 0,
4855
- CLAUDE_CODE_OAUTH_TOKEN: void 0
4885
+ CLAUDE_CODE_OAUTH_TOKEN: void 0,
4886
+ // #0986: metered first-party key — the 1h TTL costs 2.0x per cache write.
4887
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, false)
4856
4888
  },
4857
4889
  describe: `anthropic API key (${account.label})`
4858
4890
  };
@@ -4863,7 +4895,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4863
4895
  claudeEnv: {
4864
4896
  ANTHROPIC_BASE_URL: url,
4865
4897
  ANTHROPIC_API_KEY: account.secret,
4866
- CLAUDE_CODE_OAUTH_TOKEN: void 0
4898
+ CLAUDE_CODE_OAUTH_TOKEN: void 0,
4899
+ // #0986: an arbitrary gateway's billing model is unknown — opt-in only.
4900
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, false)
4867
4901
  },
4868
4902
  describe: `anthropic gateway ${url} (${account.label})`
4869
4903
  };
@@ -4880,7 +4914,7 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4880
4914
  ANTHROPIC_API_KEY: token,
4881
4915
  CLAUDE_CODE_OAUTH_TOKEN: void 0,
4882
4916
  // #0203: subscription backend qualifies for the free 1h prompt-cache TTL.
4883
- ENABLE_PROMPT_CACHING_1H: env.FORCE_PROMPT_CACHING_5M ? void 0 : "1"
4917
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, true)
4884
4918
  },
4885
4919
  describe: `hypha proxy ${url} (${account.label})`
4886
4920
  };
@@ -4891,7 +4925,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4891
4925
  claudeEnv: {
4892
4926
  CLAUDE_CODE_OAUTH_TOKEN: account.secret,
4893
4927
  ANTHROPIC_BASE_URL: void 0,
4894
- ANTHROPIC_API_KEY: void 0
4928
+ ANTHROPIC_API_KEY: void 0,
4929
+ // #0986: subscription token — free 1h TTL.
4930
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, true)
4895
4931
  },
4896
4932
  describe: `claude subscription token (${account.label})`
4897
4933
  };
@@ -4901,7 +4937,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4901
4937
  claudeEnv: {
4902
4938
  CLAUDE_CODE_OAUTH_TOKEN: account.accessToken,
4903
4939
  ANTHROPIC_BASE_URL: void 0,
4904
- ANTHROPIC_API_KEY: void 0
4940
+ ANTHROPIC_API_KEY: void 0,
4941
+ // #0986: subscription OAuth — free 1h TTL.
4942
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, true)
4905
4943
  },
4906
4944
  describe: `claude subscription oauth (${account.label})`
4907
4945
  };
@@ -5521,10 +5559,16 @@ const genKey = () => "ck_" + randomBytes(18).toString("base64url");
5521
5559
  const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
5522
5560
  \${body.message}
5523
5561
  </inbound-message>`;
5562
+ const CHANNEL_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
5563
+ function isValidChannelId(id) {
5564
+ return typeof id === "string" && CHANNEL_ID_RE.test(id) && id !== "." && id !== "..";
5565
+ }
5524
5566
  function validateChannel(c) {
5525
5567
  const errs = [];
5526
5568
  if (!c || typeof c !== "object") return ["channel must be an object"];
5527
5569
  if (!c.name) errs.push("name required");
5570
+ if (c.id !== void 0 && !isValidChannelId(c.id))
5571
+ errs.push("id must match [A-Za-z0-9_.-]{1,64} (no path separators)");
5528
5572
  const m = c.identity?.mode;
5529
5573
  if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
5530
5574
  if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
@@ -5591,10 +5635,14 @@ class ChannelStore {
5591
5635
  } catch {
5592
5636
  }
5593
5637
  }
5638
+ // #0982: reject a traversing/oversized id at the path boundary itself, so no
5639
+ // caller (RPC, HTTP, CLI) can reach outside this.dir regardless of its own checks.
5594
5640
  _path(id) {
5641
+ if (!isValidChannelId(id)) throw new Error(`invalid channel id: ${String(id).slice(0, 64)}`);
5595
5642
  return join$1(this.dir, `${id}.json`);
5596
5643
  }
5597
5644
  _lock(id) {
5645
+ if (!isValidChannelId(id)) throw new Error(`invalid channel id: ${String(id).slice(0, 64)}`);
5598
5646
  return join$1(this.dir, `${id}.json.lock`);
5599
5647
  }
5600
5648
  // #0679: the actual validate + atomic write, WITHOUT the lock, so a locked RMW mutator can
@@ -6463,6 +6511,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6463
6511
  serveCallerTrusted(context),
6464
6512
  process.env.SVAMP_OWNER_EMAIL
6465
6513
  );
6514
+ const assertMayReplaceMount = (sm, name, context) => {
6515
+ const existingMount = sm.getMount(name);
6516
+ if (!existingMount || serveCallerTrusted(context)) return;
6517
+ const callerEmail = (context?.user?.email || "").toLowerCase();
6518
+ const mountOwner = (existingMount.ownerEmail || "").toLowerCase();
6519
+ if (!callerEmail || !mountOwner || callerEmail !== mountOwner) {
6520
+ throw new Error(`Not authorized to replace mount '${name}' (owned by another user)`);
6521
+ }
6522
+ };
6466
6523
  let lastInboundRpcAt = Date.now();
6467
6524
  const trackInbound = () => {
6468
6525
  lastInboundRpcAt = Date.now();
@@ -6603,6 +6660,21 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6603
6660
  }
6604
6661
  return result;
6605
6662
  },
6663
+ /**
6664
+ * #0970: list ARCHIVED (stopped-but-persisted) sessions.
6665
+ *
6666
+ * Kept as its own method rather than a flag on listSessions so no existing caller's
6667
+ * view silently gains stopped rows. Machine-level `view` only: an archived session has
6668
+ * no live RPC handler, so the per-session sharing check listSessions falls back to
6669
+ * cannot run — rather than guess, this returns nothing to a caller without machine
6670
+ * access. That is the conservative direction (a session-shared user simply keeps
6671
+ * today's behaviour).
6672
+ */
6673
+ listArchivedSessions: async (context) => {
6674
+ trackInbound();
6675
+ authorizeRequest(context, currentMetadata.sharing, "view");
6676
+ return handlers.getArchivedSessions?.() || [];
6677
+ },
6606
6678
  /**
6607
6679
  * Get summary info for all sessions (metadata, agent state, activity).
6608
6680
  * Replaces the need to discover and query N individual session services.
@@ -7080,14 +7152,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7080
7152
  const { exec } = await import('child_process');
7081
7153
  const { homedir } = await import('os');
7082
7154
  return new Promise((resolve) => {
7083
- exec(command, { cwd: cwd || homedir(), timeout: 3e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
7084
- if (err) {
7085
- const enobufs = err.code === "ENOBUFS";
7086
- const errText = enobufs ? `output exceeded the 16 MB buffer and was truncated (the command may have completed): ${stderr || err.message}` : stderr || err.message;
7087
- resolve({ success: false, stdout: stdout || "", stderr: errText, exitCode: err.code ?? 1 });
7088
- } else {
7089
- resolve({ success: true, stdout, stderr: stderr || "", exitCode: 0 });
7090
- }
7155
+ exec(command, { cwd: cwd || homedir(), timeout: 3e4, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
7156
+ if (err) resolve(execFailureOutcome(err, stdout, stderr));
7157
+ else resolve(execSuccessOutcome(stdout, stderr));
7091
7158
  });
7092
7159
  });
7093
7160
  },
@@ -7632,6 +7699,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7632
7699
  if (params.sessionId && !serveCallerTrusted(context)) {
7633
7700
  await authorizeSessionAccess(params.sessionId, "admin", context);
7634
7701
  }
7702
+ assertMayReplaceMount(sm, params.name, context);
7635
7703
  const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7636
7704
  const access = params.access || "owner";
7637
7705
  return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
@@ -7652,14 +7720,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7652
7720
  if (params.sessionId && !serveCallerTrusted(context)) {
7653
7721
  await authorizeSessionAccess(params.sessionId, "admin", context);
7654
7722
  }
7655
- const existingMount = sm.getMount(params.name);
7656
- if (existingMount && !serveCallerTrusted(context)) {
7657
- const callerEmail = (context?.user?.email || "").toLowerCase();
7658
- const mountOwner = (existingMount.ownerEmail || "").toLowerCase();
7659
- if (!callerEmail || !mountOwner || callerEmail !== mountOwner) {
7660
- throw new Error(`Not authorized to replace mount '${params.name}' (owned by another user)`);
7661
- }
7662
- }
7723
+ assertMayReplaceMount(sm, params.name, context);
7663
7724
  const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7664
7725
  const access = params.access ?? "owner";
7665
7726
  return sm.applyMount({
@@ -8030,7 +8091,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8030
8091
  }
8031
8092
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
8032
8093
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
8033
- const { toolsForRole } = await import('./sideband-L0YNMJhj.mjs');
8094
+ const { toolsForRole } = await import('./sideband-C83128hf.mjs');
8034
8095
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
8035
8096
  return fmt(r2);
8036
8097
  }
@@ -8135,7 +8196,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8135
8196
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
8136
8197
  }
8137
8198
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
8138
- const { queryCore } = await import('./commands-C8im5Q2J.mjs');
8199
+ const { queryCore } = await import('./commands-GRfwAHgR.mjs');
8139
8200
  const timeout = c.reply?.timeout_sec || 120;
8140
8201
  let result;
8141
8202
  try {
@@ -10242,7 +10303,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
10242
10303
  return { ok: result.status === "completed", call_id: callId, correlationId: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
10243
10304
  }
10244
10305
  if (c.action?.kind === "loop") return { error: "loop channels are served by the channel server, not channelSend" };
10245
- if (params.no_reply && c.action?.kind === "message") {
10306
+ if (params.no_reply && c.action?.kind === "message" && r.sender.verified) {
10246
10307
  const envelope = renderMessage(c, { sender: r.sender, body: { message: params.message }, callId });
10247
10308
  const wire = JSON.stringify({ role: "user", content: { type: "text", text: envelope } });
10248
10309
  await deliverUserMessageCore(wire, params.localId || callId, void 0, true);
@@ -17851,6 +17912,197 @@ function shouldForceReconnect(consecutiveHeartbeatFailures) {
17851
17912
  return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
17852
17913
  }
17853
17914
 
17915
+ function acquireSupervisorLock(pidFile, pid = process.pid) {
17916
+ try {
17917
+ const fd = openSync(pidFile, "wx");
17918
+ try {
17919
+ writeSync(fd, String(pid));
17920
+ } finally {
17921
+ closeSync(fd);
17922
+ }
17923
+ return { acquired: true };
17924
+ } catch (err) {
17925
+ if (err?.code !== "EEXIST") throw err;
17926
+ }
17927
+ let existingPid = 0;
17928
+ try {
17929
+ existingPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
17930
+ } catch {
17931
+ }
17932
+ if (!existingPid || Number.isNaN(existingPid) || existingPid <= 0) {
17933
+ try {
17934
+ unlinkSync$1(pidFile);
17935
+ } catch {
17936
+ }
17937
+ return { acquired: false, staleCleaned: true };
17938
+ }
17939
+ if (isPidAlive(existingPid)) {
17940
+ return { acquired: false, heldBy: existingPid };
17941
+ }
17942
+ try {
17943
+ unlinkSync$1(pidFile);
17944
+ } catch {
17945
+ }
17946
+ return { acquired: false, staleCleaned: true };
17947
+ }
17948
+ function acquireSupervisorLockWithRetry(pidFile, pid = process.pid) {
17949
+ let result = acquireSupervisorLock(pidFile, pid);
17950
+ if (!result.acquired && result.staleCleaned) {
17951
+ result = acquireSupervisorLock(pidFile, pid);
17952
+ }
17953
+ return { acquired: result.acquired, heldBy: result.heldBy };
17954
+ }
17955
+ function releaseSupervisorLock(pidFile, pid = process.pid) {
17956
+ try {
17957
+ if (!existsSync(pidFile)) return;
17958
+ const content = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
17959
+ if (content === pid) unlinkSync$1(pidFile);
17960
+ } catch {
17961
+ }
17962
+ }
17963
+ function isPidAlive(pid) {
17964
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
17965
+ try {
17966
+ process.kill(pid, 0);
17967
+ return true;
17968
+ } catch (err) {
17969
+ return err?.code === "EPERM";
17970
+ }
17971
+ }
17972
+ function isSupervisorPid(pid) {
17973
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
17974
+ if (process.platform === "win32") return null;
17975
+ try {
17976
+ const out = execFileSync("ps", ["-o", "command=", "-p", String(pid)], {
17977
+ encoding: "utf-8",
17978
+ timeout: 5e3,
17979
+ stdio: ["ignore", "pipe", "ignore"]
17980
+ }).trim();
17981
+ if (!out) return false;
17982
+ return /daemon\s+start-supervised/.test(out);
17983
+ } catch {
17984
+ return null;
17985
+ }
17986
+ }
17987
+ function shouldSignalSupervisor(opts) {
17988
+ const { pid, alive, isSupervisor } = opts;
17989
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
17990
+ if (!alive) return false;
17991
+ if (isSupervisor === false) return false;
17992
+ return true;
17993
+ }
17994
+ function findOrphanedSyncPids(currentSupervisorPid = process.pid) {
17995
+ if (process.platform === "win32") return [];
17996
+ const pids = [];
17997
+ try {
17998
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
17999
+ const pgrepArgs = uid !== void 0 ? ["-u", String(uid), "-af", "svamp daemon start-sync"] : ["-af", "svamp daemon start-sync"];
18000
+ const output = execFileSync("pgrep", pgrepArgs, {
18001
+ encoding: "utf-8",
18002
+ timeout: 5e3
18003
+ });
18004
+ for (const line of output.split("\n")) {
18005
+ const trimmed = line.trim();
18006
+ if (!trimmed) continue;
18007
+ const match = trimmed.match(/^(\d+)\s/);
18008
+ if (!match) continue;
18009
+ const pid = parseInt(match[1], 10);
18010
+ if (Number.isNaN(pid) || pid <= 0) continue;
18011
+ if (pid === process.pid) continue;
18012
+ const ppid = getPpid(pid);
18013
+ if (ppid === currentSupervisorPid) continue;
18014
+ pids.push(pid);
18015
+ }
18016
+ } catch (err) {
18017
+ if (err?.status !== 1) ;
18018
+ }
18019
+ return pids;
18020
+ }
18021
+ function getPpid(pid) {
18022
+ if (process.platform === "win32") return 0;
18023
+ try {
18024
+ const out = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
18025
+ encoding: "utf-8",
18026
+ timeout: 2e3
18027
+ }).trim();
18028
+ const ppid = parseInt(out, 10);
18029
+ return Number.isNaN(ppid) ? 0 : ppid;
18030
+ } catch {
18031
+ return 0;
18032
+ }
18033
+ }
18034
+ async function killOrphanedSyncs(pids, opts = {}) {
18035
+ if (pids.length === 0) return;
18036
+ const log = opts.log || (() => {
18037
+ });
18038
+ const gracePeriodMs = opts.gracePeriodMs ?? 3e3;
18039
+ log(`Killing ${pids.length} orphan sync daemon(s): ${pids.join(", ")}`);
18040
+ for (const pid of pids) {
18041
+ try {
18042
+ process.kill(pid, "SIGTERM");
18043
+ } catch {
18044
+ }
18045
+ }
18046
+ const pollStep = 100;
18047
+ const steps = Math.max(1, Math.ceil(gracePeriodMs / pollStep));
18048
+ for (let i = 0; i < steps; i++) {
18049
+ await new Promise((r) => setTimeout(r, pollStep));
18050
+ if (pids.every((p) => !isPidAlive(p))) {
18051
+ log("All orphan daemons exited cleanly");
18052
+ return;
18053
+ }
18054
+ }
18055
+ const survivors = pids.filter(isPidAlive);
18056
+ if (survivors.length > 0) {
18057
+ log(`Force-killing survivors: ${survivors.join(", ")}`);
18058
+ for (const pid of survivors) {
18059
+ try {
18060
+ process.kill(pid, "SIGKILL");
18061
+ } catch {
18062
+ }
18063
+ }
18064
+ }
18065
+ }
18066
+ function watchParentLiveness(opts) {
18067
+ if (process.env.SVAMP_SUPERVISED !== "1") {
18068
+ return () => {
18069
+ };
18070
+ }
18071
+ const supervisorPid = process.ppid;
18072
+ if (!supervisorPid || supervisorPid === 1) {
18073
+ return () => {
18074
+ };
18075
+ }
18076
+ const intervalMs = opts.intervalMs ?? 5e3;
18077
+ let fired = false;
18078
+ const timer = setInterval(() => {
18079
+ if (fired) return;
18080
+ if (!isPidAlive(supervisorPid)) {
18081
+ fired = true;
18082
+ clearInterval(timer);
18083
+ opts.onParentDeath();
18084
+ }
18085
+ }, intervalMs);
18086
+ timer.unref?.();
18087
+ return () => {
18088
+ clearInterval(timer);
18089
+ };
18090
+ }
18091
+
18092
+ var supervisorLock = /*#__PURE__*/Object.freeze({
18093
+ __proto__: null,
18094
+ acquireSupervisorLock: acquireSupervisorLock,
18095
+ acquireSupervisorLockWithRetry: acquireSupervisorLockWithRetry,
18096
+ findOrphanedSyncPids: findOrphanedSyncPids,
18097
+ getPpid: getPpid,
18098
+ isPidAlive: isPidAlive,
18099
+ isSupervisorPid: isSupervisorPid,
18100
+ killOrphanedSyncs: killOrphanedSyncs,
18101
+ releaseSupervisorLock: releaseSupervisorLock,
18102
+ shouldSignalSupervisor: shouldSignalSupervisor,
18103
+ watchParentLiveness: watchParentLiveness
18104
+ });
18105
+
17854
18106
  const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
17855
18107
  function generateHookSettings(portOrOptions = {}) {
17856
18108
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
@@ -18811,7 +19063,9 @@ function spawnHeadlessEvaluator(prompt, opts) {
18811
19063
  const evalArgs = [
18812
19064
  "--print",
18813
19065
  prompt,
18814
- // Read-only reviewer (mirrors .claude/agents/loop-evaluator.md): it inspects real repo
19066
+ // Read-only reviewer (#0995: NOT the retired .claude/agents/loop-evaluator.md subagent
19067
+ // this is a separate `claude --print` process whose prompt buildEvaluatorPrompt owns,
19068
+ // which is what keeps the #0189 independence invariant): it inspects real repo
18815
19069
  // state but must not mutate the working tree it's judging. The allowedTools whitelist
18816
19070
  // already excludes mutation tools; --disallowedTools is belt-and-suspenders. Kept to
18817
19071
  // Write/Edit only — MultiEdit/NotebookEdit aren't known tool names on some claude builds
@@ -19337,6 +19591,26 @@ function clearSessionArchivedFlag(sessionId) {
19337
19591
  return null;
19338
19592
  }
19339
19593
  }
19594
+ function loadArchivedSessionInfos(isLive) {
19595
+ const out = [];
19596
+ const index = loadSessionIndex();
19597
+ for (const [sessionId, entry] of Object.entries(index)) {
19598
+ if (isLive(sessionId)) continue;
19599
+ try {
19600
+ const data = JSON.parse(readFileSync$1(getSessionFilePath(entry.directory, sessionId), "utf-8"));
19601
+ if (!data?.sessionId) continue;
19602
+ out.push({
19603
+ sessionId: data.sessionId,
19604
+ startedBy: data.startedBy || "unknown",
19605
+ directory: data.directory || entry.directory,
19606
+ active: false,
19607
+ metadata: data.metadata
19608
+ });
19609
+ } catch {
19610
+ }
19611
+ }
19612
+ return out;
19613
+ }
19340
19614
  function loadPersistedSessions() {
19341
19615
  const sessions = [];
19342
19616
  const index = loadSessionIndex();
@@ -19487,7 +19761,7 @@ async function startDaemon(options) {
19487
19761
  process.on("SIGINT", () => requestShutdown("os-signal"));
19488
19762
  process.on("SIGTERM", () => requestShutdown("os-signal"));
19489
19763
  process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
19490
- const { watchParentLiveness } = await import('./supervisorLock-DmfzJx7B.mjs');
19764
+ const { watchParentLiveness } = await Promise.resolve().then(function () { return supervisorLock; });
19491
19765
  const cancelParentWatchdog = watchParentLiveness({
19492
19766
  intervalMs: 5e3,
19493
19767
  onParentDeath: () => {
@@ -19680,7 +19954,7 @@ async function startDaemon(options) {
19680
19954
  try {
19681
19955
  const dir = loadSessionIndex()[sessionId]?.directory;
19682
19956
  if (!dir) return;
19683
- const { reconcileServiceLinks } = await import('./agentCommands-CjbndvZ2.mjs');
19957
+ const { reconcileServiceLinks } = await import('./agentCommands-D7CV6DKL.mjs');
19684
19958
  const configPath = getSvampConfigPath(dir, sessionId);
19685
19959
  const config = readSvampConfig(configPath);
19686
19960
  const entries = Array.from(urls.entries());
@@ -19702,7 +19976,7 @@ async function startDaemon(options) {
19702
19976
  try {
19703
19977
  const dir = loadSessionIndex()[sessionId]?.directory;
19704
19978
  if (!dir) return;
19705
- const { reconcileServiceLinks } = await import('./agentCommands-CjbndvZ2.mjs');
19979
+ const { reconcileServiceLinks } = await import('./agentCommands-D7CV6DKL.mjs');
19706
19980
  const configPath = getSvampConfigPath(dir, sessionId);
19707
19981
  const config = readSvampConfig(configPath);
19708
19982
  const incoming = [{
@@ -19723,7 +19997,7 @@ async function startDaemon(options) {
19723
19997
  try {
19724
19998
  const dir = loadSessionIndex()[sessionId]?.directory;
19725
19999
  if (!dir) return;
19726
- const { dropServiceLinks } = await import('./agentCommands-CjbndvZ2.mjs');
20000
+ const { dropServiceLinks } = await import('./agentCommands-D7CV6DKL.mjs');
19727
20001
  const configPath = getSvampConfigPath(dir, sessionId);
19728
20002
  const config = readSvampConfig(configPath);
19729
20003
  if (dropServiceLinks(config, "serve", mountName)) {
@@ -19766,7 +20040,7 @@ async function startDaemon(options) {
19766
20040
  ensureAutoInstalledCommands(logger);
19767
20041
  (async () => {
19768
20042
  try {
19769
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-C8CmQv3y.mjs');
20043
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-8SZDu7O-.mjs');
19770
20044
  beginClaudeVersionReconcile((msg) => logger.log(msg));
19771
20045
  } catch (e) {
19772
20046
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -22086,21 +22360,18 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22086
22360
  logger.log(`[Session ${sessionId}] Bash: ${command} (cwd: ${cwd || directory}, timeout: ${execTimeout}ms)`);
22087
22361
  const { exec: exec2 } = await import('child_process');
22088
22362
  return new Promise((resolve2) => {
22089
- exec2(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
22090
- if (err) {
22091
- resolve2({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
22092
- } else {
22093
- resolve2({ success: true, stdout, stderr: stderr || "", exitCode: 0 });
22094
- }
22363
+ exec2(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
22364
+ if (err) resolve2(execFailureOutcome(err, stdout, stderr));
22365
+ else resolve2(execSuccessOutcome(stdout, stderr));
22095
22366
  });
22096
22367
  });
22097
22368
  },
22098
22369
  onIssue: async (params) => {
22099
- const { issueRpc } = await import('./rpc-BosOiRY-.mjs');
22370
+ const { issueRpc } = await import('./rpc-BIzMNjqC.mjs');
22100
22371
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22101
22372
  },
22102
22373
  onWorkflow: async (params) => {
22103
- const { workflowRpc } = await import('./rpc-DP4CiR0g.mjs');
22374
+ const { workflowRpc } = await import('./rpc-C1KIoUyz.mjs');
22104
22375
  return workflowRpc(params?.cwd || directory, params || {});
22105
22376
  },
22106
22377
  onRipgrep: async (args, cwd) => {
@@ -22797,21 +23068,18 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22797
23068
  const execTimeout = timeout || 12e4;
22798
23069
  const { exec: exec2 } = await import('child_process');
22799
23070
  return new Promise((resolve2) => {
22800
- exec2(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
22801
- if (err) {
22802
- resolve2({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
22803
- } else {
22804
- resolve2({ success: true, stdout, stderr: stderr || "", exitCode: 0 });
22805
- }
23071
+ exec2(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
23072
+ if (err) resolve2(execFailureOutcome(err, stdout, stderr));
23073
+ else resolve2(execSuccessOutcome(stdout, stderr));
22806
23074
  });
22807
23075
  });
22808
23076
  },
22809
23077
  onIssue: async (params) => {
22810
- const { issueRpc } = await import('./rpc-BosOiRY-.mjs');
23078
+ const { issueRpc } = await import('./rpc-BIzMNjqC.mjs');
22811
23079
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22812
23080
  },
22813
23081
  onWorkflow: async (params) => {
22814
- const { workflowRpc } = await import('./rpc-DP4CiR0g.mjs');
23082
+ const { workflowRpc } = await import('./rpc-C1KIoUyz.mjs');
22815
23083
  return workflowRpc(params?.cwd || directory, params || {});
22816
23084
  },
22817
23085
  onRipgrep: async (args, cwd) => {
@@ -23640,6 +23908,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23640
23908
  logger.log("Shutdown requested via hypha-app (ignored \u2014 daemon never self-terminates)");
23641
23909
  },
23642
23910
  getTrackedSessions: getCurrentChildren,
23911
+ // #0970: archived (stopped-but-persisted) sessions, so CLI id resolution can find them.
23912
+ getArchivedSessions: () => {
23913
+ const live = new Set(getCurrentChildren().map((c) => c.sessionId));
23914
+ return loadArchivedSessionInfos((id) => live.has(id));
23915
+ },
23643
23916
  getSessionRPCHandlers: (sessionId) => {
23644
23917
  for (const [, session] of pidToTrackedSession) {
23645
23918
  if (session.svampSessionId === sessionId && !session.stopped && session.sessionRPCHandlers) {
@@ -23863,7 +24136,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23863
24136
  }
23864
24137
  if (persistedSessions.length > 0) {
23865
24138
  try {
23866
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-C8CmQv3y.mjs');
24139
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-8SZDu7O-.mjs');
23867
24140
  await awaitClaudeVersionReady();
23868
24141
  } catch {
23869
24142
  }
@@ -23964,8 +24237,16 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23964
24237
  const supPidFile = join(SVAMP_HOME, "supervisor.pid");
23965
24238
  const supPid = existsSync$1(supPidFile) ? parseInt(readFileSync$1(supPidFile, "utf-8").trim(), 10) : NaN;
23966
24239
  if (supPid && !isNaN(supPid)) {
23967
- process.kill(supPid, "SIGUSR2");
23968
- return;
24240
+ const ok = shouldSignalSupervisor({
24241
+ pid: supPid,
24242
+ alive: isPidAlive(supPid),
24243
+ isSupervisor: isSupervisorPid(supPid)
24244
+ });
24245
+ if (ok) {
24246
+ process.kill(supPid, "SIGUSR2");
24247
+ return;
24248
+ }
24249
+ logger.log(`[graceful-restart] supervisor.pid ${supPid} is stale or not a supervisor \u2014 not signalling`);
23969
24250
  }
23970
24251
  } catch {
23971
24252
  }
@@ -24112,7 +24393,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24112
24393
  const PING_TIMEOUT_MS = 15e3;
24113
24394
  const POST_RECONNECT_GRACE_MS = 2e4;
24114
24395
  const RECONNECT_JITTER_MS = 2500;
24115
- const { WorkflowScheduler } = await import('./scheduler-BN9GuvGN.mjs');
24396
+ const { WorkflowScheduler } = await import('./scheduler-DIYulmdV.mjs');
24116
24397
  const workflowProjectRoots = () => {
24117
24398
  const dirs = /* @__PURE__ */ new Set();
24118
24399
  for (const s of pidToTrackedSession.values()) {
@@ -24745,4 +25026,4 @@ var run = /*#__PURE__*/Object.freeze({
24745
25026
  writeStopMarker: writeStopMarker
24746
25027
  });
24747
25028
 
24748
- export { listSkillFiles as $, saveWorkflow as A, rawWorkflow as B, listWorkflows as C, isWorkflowEnabled as D, workflowSchedules as E, inZone as F, cronMatches as G, summarize as H, workflowSteps as I, parseJwtEmail as J, computeCollectionConfigUpdate as K, SYSTEM_COLLECTION_CONFIG as L, loadMachineContext as M, buildMachineInstructions as N, machineToolsForRole as O, buildMachineTools as P, parseFrontmatter as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, getSkillsServer as T, getSkillsWorkspaceName as U, getSkillsCollectionName as V, fetchWithTimeout as W, searchSkills as X, SKILLS_DIR as Y, getSkillInfo as Z, downloadSkillFile as _, createSessionStore as a, resolveModel as a0, formatHandle as a1, normalizeAllowedUser as a2, loadSecurityContextConfig as a3, resolveSecurityContext as a4, buildSecurityContextFromFlags as a5, mergeSecurityContexts as a6, buildSessionShareUrl as a7, computeOutboundHop as a8, registerAwaitingReply as a9, kimiSetup as aA, api as aB, run as aC, buildMachineShareUrl as aa, parseHandle as ab, handleMatchesMetadata as ac, PINNED_CODEX_VERSION as ad, clearStopMarker as ae, stopMarkerExists as af, withFileLock as ag, describeMisconfiguration as ah, buildMachineDeps as ai, applyClaudeProxyEnv as aj, composeSessionId as ak, generateFriendlyName as al, generateHookSettings as am, instanceConfig as an, frpc as ao, staticFileServer as ap, claudeAuth as aq, codexProvider as ar, projectInfo as as, DefaultTransport$1 as at, acpBackend as au, acpAgentConfig as av, codexAppServerBackend as aw, GeminiTransport$1 as ax, KimiTransport$1 as ay, kimiProvider as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, shortId as e, resolveProjectRoot as f, getHyphaServerUrl$1 as g, getIssue as h, resumeIssue as i, addComment as j, addIssue as k, listIssues as l, isPendingIssue as m, searchIssues as n, isVisibleTo as o, pauseIssue as p, getRun as q, registerMachineService as r, startDaemon as s, listRuns as t, updateIssue as u, getWorkflow as v, runWorkflow as w, setWorkflowEnabled as x, removeWorkflow as y, validateWorkflowName as z };
25029
+ export { listSkillFiles as $, saveWorkflow as A, rawWorkflow as B, listWorkflows as C, isWorkflowEnabled as D, workflowSchedules as E, inZone as F, cronMatches as G, summarize as H, workflowSteps as I, parseJwtEmail as J, computeCollectionConfigUpdate as K, SYSTEM_COLLECTION_CONFIG as L, loadMachineContext as M, buildMachineInstructions as N, machineToolsForRole as O, buildMachineTools as P, parseFrontmatter as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, getSkillsServer as T, getSkillsWorkspaceName as U, getSkillsCollectionName as V, fetchWithTimeout as W, searchSkills as X, SKILLS_DIR as Y, getSkillInfo as Z, downloadSkillFile as _, createSessionStore as a, resolveModel as a0, formatHandle as a1, normalizeAllowedUser as a2, loadSecurityContextConfig as a3, resolveSecurityContext as a4, buildSecurityContextFromFlags as a5, mergeSecurityContexts as a6, buildSessionShareUrl as a7, computeOutboundHop as a8, registerAwaitingReply as a9, kimiSetup as aA, api as aB, supervisorLock as aC, run as aD, buildMachineShareUrl as aa, parseHandle as ab, handleMatchesMetadata as ac, PINNED_CODEX_VERSION as ad, clearStopMarker as ae, stopMarkerExists as af, withFileLock as ag, describeMisconfiguration as ah, buildMachineDeps as ai, applyClaudeProxyEnv as aj, composeSessionId as ak, generateFriendlyName as al, generateHookSettings as am, instanceConfig as an, frpc as ao, staticFileServer as ap, claudeAuth as aq, codexProvider as ar, projectInfo as as, DefaultTransport$1 as at, acpBackend as au, acpAgentConfig as av, codexAppServerBackend as aw, GeminiTransport$1 as ax, KimiTransport$1 as ay, kimiProvider as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, shortId as e, resolveProjectRoot as f, getHyphaServerUrl$1 as g, getIssue as h, resumeIssue as i, addComment as j, addIssue as k, listIssues as l, isPendingIssue as m, searchIssues as n, isVisibleTo as o, pauseIssue as p, getRun as q, registerMachineService as r, startDaemon as s, listRuns as t, updateIssue as u, getWorkflow as v, runWorkflow as w, setWorkflowEnabled as x, removeWorkflow as y, validateWorkflowName as z };
@@ -1,4 +1,4 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { aj as applyClaudeProxyEnv, ak as composeSessionId, al as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, am as generateHookSettings } from './run-BiwqH1hw.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { aj as applyClaudeProxyEnv, ak as composeSessionId, al as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, am as generateHookSettings } from './run-CEp9YpUZ.mjs';
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
@@ -1,4 +1,4 @@
1
- import { f as resolveProjectRoot, C as listWorkflows, D as isWorkflowEnabled, E as workflowSchedules, F as inZone, w as runWorkflow, G as cronMatches } from './run-BiwqH1hw.mjs';
1
+ import { f as resolveProjectRoot, C as listWorkflows, D as isWorkflowEnabled, E as workflowSchedules, F as inZone, w as runWorkflow, G as cronMatches } from './run-CEp9YpUZ.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';