svamp-cli 0.2.323 → 0.2.325

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-CMSvATDd.mjs} +1 -1
  2. package/dist/{agentCommands-CjbndvZ2.mjs → agentCommands-BE3bFu8e.mjs} +5 -5
  3. package/dist/{auth-7A9m78i5.mjs → auth-BmFYRinf.mjs} +1 -1
  4. package/dist/{cli-C3GwCq0z.mjs → cli-DiCcPggc.mjs} +82 -77
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-DLQwUjpD.mjs → commands-7ExCePUv.mjs} +2 -2
  7. package/dist/{commands-CygFvR0a.mjs → commands-B6fVaVgM.mjs} +3 -3
  8. package/dist/{commands-LMbH3V-v.mjs → commands-C3Bkw-Km.mjs} +11 -11
  9. package/dist/{commands-BARTdhcw.mjs → commands-Coy5CnDf.mjs} +7 -4
  10. package/dist/{commands-C8im5Q2J.mjs → commands-D4R66JMe.mjs} +17 -5
  11. package/dist/{commands-DVvR4X4G.mjs → commands-WDZS1Clb.mjs} +2 -2
  12. package/dist/{commands-gQM3K8Yf.mjs → commands-Z_44kVdm.mjs} +1 -1
  13. package/dist/{commands-CZLE29kn.mjs → commands-uYZfcxk0.mjs} +3 -3
  14. package/dist/{fleet-C68s3qVt.mjs → fleet-DJj0ns7e.mjs} +3 -3
  15. package/dist/{headlessCli-0_H-JwW2.mjs → headlessCli-Bx41HZIx.mjs} +2 -2
  16. package/dist/index.mjs +1 -1
  17. package/dist/{notifyCommands-r0r8eyxA.mjs → notifyCommands-BV4ajmhc.mjs} +1 -1
  18. package/dist/{package-BGIfEYpX.mjs → package-C3Usc86E.mjs} +2 -2
  19. package/dist/{pinnedClaudeCode-C8CmQv3y.mjs → pinnedClaudeCode-8SZDu7O-.mjs} +1 -1
  20. package/dist/{rpc-DP4CiR0g.mjs → rpc-D9LFUW93.mjs} +1 -1
  21. package/dist/{rpc-BosOiRY-.mjs → rpc-D_pFjWr1.mjs} +1 -1
  22. package/dist/{run-BiwqH1hw.mjs → run-CO4F7ZaJ.mjs} +360 -62
  23. package/dist/{run-DAo9Lw4D.mjs → run-D7G5CDp7.mjs} +1 -1
  24. package/dist/{scheduler-BN9GuvGN.mjs → scheduler-bgEMu7PK.mjs} +1 -1
  25. package/dist/{serveCommands-C_E-RAiL.mjs → serveCommands-MbZuPWD4.mjs} +10 -10
  26. package/dist/{sideband-L0YNMJhj.mjs → sideband-CadJ3gg1.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';
@@ -3793,16 +3793,26 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3793
3793
  const ownerEmail = (target.ownerEmail || "").toLowerCase();
3794
3794
  const isOwner = !!ownerEmail && userEmail.toLowerCase() === ownerEmail;
3795
3795
  if (!isOwner) {
3796
+ const principal = { user: { email: userEmail } };
3797
+ let granted = false;
3796
3798
  const sharing = target.sharing;
3797
- if (!sharing || !sharing.enabled) {
3798
- this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and session not shared`);
3799
- deny(403, "Access denied");
3800
- return;
3799
+ if (sharing && sharing.enabled) {
3800
+ try {
3801
+ authorizeRequest(principal, sharing, "admin");
3802
+ granted = true;
3803
+ } catch {
3804
+ }
3801
3805
  }
3802
- try {
3803
- authorizeRequest({ user: { email: userEmail } }, sharing, "admin");
3804
- } catch (err) {
3805
- this.log(`Files-gateway DENY '${sessionId}': ${userEmail} \u2014 ${err?.message || "not admin"}`);
3806
+ const machineSharing = target.machineSharing;
3807
+ if (!granted && machineSharing && machineSharing.enabled) {
3808
+ try {
3809
+ authorizeRequest(principal, machineSharing, "admin");
3810
+ granted = true;
3811
+ } catch {
3812
+ }
3813
+ }
3814
+ if (!granted) {
3815
+ this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and not a session/machine admin`);
3806
3816
  deny(403, "Access denied");
3807
3817
  return;
3808
3818
  }
@@ -3811,7 +3821,7 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3811
3821
  const normRel = this.toSessionRelative(rootDir, m[2] || "");
3812
3822
  if (method === "GET" || method === "HEAD") {
3813
3823
  if (url.searchParams.get("list") === "1") {
3814
- const lex = containedPath(rootDir, normRel);
3824
+ const lex = this.resolveContainedPath(rootDir, normRel);
3815
3825
  if (!lex) {
3816
3826
  deny(403, "Forbidden");
3817
3827
  return;
@@ -4414,6 +4424,29 @@ var serveManager = /*#__PURE__*/Object.freeze({
4414
4424
  sanitizeMountForRole: sanitizeMountForRole
4415
4425
  });
4416
4426
 
4427
+ const EXEC_MAX_BUFFER = 16 * 1024 * 1024;
4428
+ function execFailureOutcome(err, stdout, stderr) {
4429
+ const e = err || {};
4430
+ const rawCode = e.code;
4431
+ const enobufs = rawCode === "ENOBUFS";
4432
+ const timedOut = rawCode === "ETIMEDOUT" || e.killed === true && !!e.signal;
4433
+ let exitCode;
4434
+ if (typeof rawCode === "number") exitCode = rawCode;
4435
+ else if (timedOut) exitCode = 124;
4436
+ else exitCode = 1;
4437
+ const base = stderr || e.message || "";
4438
+ let text = base;
4439
+ if (enobufs) {
4440
+ text = `output exceeded the ${Math.round(EXEC_MAX_BUFFER / (1024 * 1024))} MB buffer and was truncated (the command may have completed): ${base}`;
4441
+ } else if (timedOut) {
4442
+ text = `command timed out and was killed (it may have partially completed): ${base}`;
4443
+ }
4444
+ return { success: false, stdout: stdout || "", stderr: text, exitCode };
4445
+ }
4446
+ function execSuccessOutcome(stdout, stderr) {
4447
+ return { success: true, stdout: stdout || "", stderr: stderr || "", exitCode: 0 };
4448
+ }
4449
+
4417
4450
  const EXAMPLE_HYPHA_PROXY_URL = "https://proxy.hypha.aicell.io";
4418
4451
  const MODE_KEY = "SVAMP_CLAUDE_PROXY";
4419
4452
  const HYPHA_PROXY_URL_KEY = "SVAMP_HYPHA_PROXY_URL";
@@ -4844,6 +4877,13 @@ function stripV1(url) {
4844
4877
  }
4845
4878
  return trimmed;
4846
4879
  }
4880
+ function promptCacheTtlEnv(env, subscription) {
4881
+ if (env.FORCE_PROMPT_CACHING_5M) return void 0;
4882
+ if (!subscription) {
4883
+ return env.ENABLE_PROMPT_CACHING_1H === "1" ? "1" : void 0;
4884
+ }
4885
+ return env.ENABLE_PROMPT_CACHING_1H === "0" ? void 0 : "1";
4886
+ }
4847
4887
  function resolveAccountEnv(account, env = process.env, svampHome) {
4848
4888
  switch (account.method) {
4849
4889
  case "anthropic-api-key":
@@ -4852,7 +4892,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4852
4892
  claudeEnv: {
4853
4893
  ANTHROPIC_API_KEY: account.secret,
4854
4894
  ANTHROPIC_BASE_URL: account.baseUrl ? stripV1(account.baseUrl) : void 0,
4855
- CLAUDE_CODE_OAUTH_TOKEN: void 0
4895
+ CLAUDE_CODE_OAUTH_TOKEN: void 0,
4896
+ // #0986: metered first-party key — the 1h TTL costs 2.0x per cache write.
4897
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, false)
4856
4898
  },
4857
4899
  describe: `anthropic API key (${account.label})`
4858
4900
  };
@@ -4863,7 +4905,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4863
4905
  claudeEnv: {
4864
4906
  ANTHROPIC_BASE_URL: url,
4865
4907
  ANTHROPIC_API_KEY: account.secret,
4866
- CLAUDE_CODE_OAUTH_TOKEN: void 0
4908
+ CLAUDE_CODE_OAUTH_TOKEN: void 0,
4909
+ // #0986: an arbitrary gateway's billing model is unknown — opt-in only.
4910
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, false)
4867
4911
  },
4868
4912
  describe: `anthropic gateway ${url} (${account.label})`
4869
4913
  };
@@ -4880,7 +4924,7 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4880
4924
  ANTHROPIC_API_KEY: token,
4881
4925
  CLAUDE_CODE_OAUTH_TOKEN: void 0,
4882
4926
  // #0203: subscription backend qualifies for the free 1h prompt-cache TTL.
4883
- ENABLE_PROMPT_CACHING_1H: env.FORCE_PROMPT_CACHING_5M ? void 0 : "1"
4927
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, true)
4884
4928
  },
4885
4929
  describe: `hypha proxy ${url} (${account.label})`
4886
4930
  };
@@ -4891,7 +4935,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4891
4935
  claudeEnv: {
4892
4936
  CLAUDE_CODE_OAUTH_TOKEN: account.secret,
4893
4937
  ANTHROPIC_BASE_URL: void 0,
4894
- ANTHROPIC_API_KEY: void 0
4938
+ ANTHROPIC_API_KEY: void 0,
4939
+ // #0986: subscription token — free 1h TTL.
4940
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, true)
4895
4941
  },
4896
4942
  describe: `claude subscription token (${account.label})`
4897
4943
  };
@@ -4901,7 +4947,9 @@ function resolveAccountEnv(account, env = process.env, svampHome) {
4901
4947
  claudeEnv: {
4902
4948
  CLAUDE_CODE_OAUTH_TOKEN: account.accessToken,
4903
4949
  ANTHROPIC_BASE_URL: void 0,
4904
- ANTHROPIC_API_KEY: void 0
4950
+ ANTHROPIC_API_KEY: void 0,
4951
+ // #0986: subscription OAuth — free 1h TTL.
4952
+ ENABLE_PROMPT_CACHING_1H: promptCacheTtlEnv(env, true)
4905
4953
  },
4906
4954
  describe: `claude subscription oauth (${account.label})`
4907
4955
  };
@@ -5521,10 +5569,16 @@ const genKey = () => "ck_" + randomBytes(18).toString("base64url");
5521
5569
  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
5570
  \${body.message}
5523
5571
  </inbound-message>`;
5572
+ const CHANNEL_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
5573
+ function isValidChannelId(id) {
5574
+ return typeof id === "string" && CHANNEL_ID_RE.test(id) && id !== "." && id !== "..";
5575
+ }
5524
5576
  function validateChannel(c) {
5525
5577
  const errs = [];
5526
5578
  if (!c || typeof c !== "object") return ["channel must be an object"];
5527
5579
  if (!c.name) errs.push("name required");
5580
+ if (c.id !== void 0 && !isValidChannelId(c.id))
5581
+ errs.push("id must match [A-Za-z0-9_.-]{1,64} (no path separators)");
5528
5582
  const m = c.identity?.mode;
5529
5583
  if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
5530
5584
  if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
@@ -5591,10 +5645,14 @@ class ChannelStore {
5591
5645
  } catch {
5592
5646
  }
5593
5647
  }
5648
+ // #0982: reject a traversing/oversized id at the path boundary itself, so no
5649
+ // caller (RPC, HTTP, CLI) can reach outside this.dir regardless of its own checks.
5594
5650
  _path(id) {
5651
+ if (!isValidChannelId(id)) throw new Error(`invalid channel id: ${String(id).slice(0, 64)}`);
5595
5652
  return join$1(this.dir, `${id}.json`);
5596
5653
  }
5597
5654
  _lock(id) {
5655
+ if (!isValidChannelId(id)) throw new Error(`invalid channel id: ${String(id).slice(0, 64)}`);
5598
5656
  return join$1(this.dir, `${id}.json.lock`);
5599
5657
  }
5600
5658
  // #0679: the actual validate + atomic write, WITHOUT the lock, so a locked RMW mutator can
@@ -6463,6 +6521,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6463
6521
  serveCallerTrusted(context),
6464
6522
  process.env.SVAMP_OWNER_EMAIL
6465
6523
  );
6524
+ const assertMayReplaceMount = (sm, name, context) => {
6525
+ const existingMount = sm.getMount(name);
6526
+ if (!existingMount || serveCallerTrusted(context)) return;
6527
+ const callerEmail = (context?.user?.email || "").toLowerCase();
6528
+ const mountOwner = (existingMount.ownerEmail || "").toLowerCase();
6529
+ if (!callerEmail || !mountOwner || callerEmail !== mountOwner) {
6530
+ throw new Error(`Not authorized to replace mount '${name}' (owned by another user)`);
6531
+ }
6532
+ };
6466
6533
  let lastInboundRpcAt = Date.now();
6467
6534
  const trackInbound = () => {
6468
6535
  lastInboundRpcAt = Date.now();
@@ -6603,6 +6670,21 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6603
6670
  }
6604
6671
  return result;
6605
6672
  },
6673
+ /**
6674
+ * #0970: list ARCHIVED (stopped-but-persisted) sessions.
6675
+ *
6676
+ * Kept as its own method rather than a flag on listSessions so no existing caller's
6677
+ * view silently gains stopped rows. Machine-level `view` only: an archived session has
6678
+ * no live RPC handler, so the per-session sharing check listSessions falls back to
6679
+ * cannot run — rather than guess, this returns nothing to a caller without machine
6680
+ * access. That is the conservative direction (a session-shared user simply keeps
6681
+ * today's behaviour).
6682
+ */
6683
+ listArchivedSessions: async (context) => {
6684
+ trackInbound();
6685
+ authorizeRequest(context, currentMetadata.sharing, "view");
6686
+ return handlers.getArchivedSessions?.() || [];
6687
+ },
6606
6688
  /**
6607
6689
  * Get summary info for all sessions (metadata, agent state, activity).
6608
6690
  * Replaces the need to discover and query N individual session services.
@@ -7080,14 +7162,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7080
7162
  const { exec } = await import('child_process');
7081
7163
  const { homedir } = await import('os');
7082
7164
  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
- }
7165
+ exec(command, { cwd: cwd || homedir(), timeout: 3e4, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
7166
+ if (err) resolve(execFailureOutcome(err, stdout, stderr));
7167
+ else resolve(execSuccessOutcome(stdout, stderr));
7091
7168
  });
7092
7169
  });
7093
7170
  },
@@ -7632,6 +7709,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7632
7709
  if (params.sessionId && !serveCallerTrusted(context)) {
7633
7710
  await authorizeSessionAccess(params.sessionId, "admin", context);
7634
7711
  }
7712
+ assertMayReplaceMount(sm, params.name, context);
7635
7713
  const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7636
7714
  const access = params.access || "owner";
7637
7715
  return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
@@ -7652,14 +7730,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7652
7730
  if (params.sessionId && !serveCallerTrusted(context)) {
7653
7731
  await authorizeSessionAccess(params.sessionId, "admin", context);
7654
7732
  }
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
- }
7733
+ assertMayReplaceMount(sm, params.name, context);
7663
7734
  const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7664
7735
  const access = params.access ?? "owner";
7665
7736
  return sm.applyMount({
@@ -8030,7 +8101,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8030
8101
  }
8031
8102
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
8032
8103
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
8033
- const { toolsForRole } = await import('./sideband-L0YNMJhj.mjs');
8104
+ const { toolsForRole } = await import('./sideband-CadJ3gg1.mjs');
8034
8105
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
8035
8106
  return fmt(r2);
8036
8107
  }
@@ -8135,7 +8206,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8135
8206
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
8136
8207
  }
8137
8208
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
8138
- const { queryCore } = await import('./commands-C8im5Q2J.mjs');
8209
+ const { queryCore } = await import('./commands-D4R66JMe.mjs');
8139
8210
  const timeout = c.reply?.timeout_sec || 120;
8140
8211
  let result;
8141
8212
  try {
@@ -10242,7 +10313,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
10242
10313
  return { ok: result.status === "completed", call_id: callId, correlationId: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
10243
10314
  }
10244
10315
  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") {
10316
+ if (params.no_reply && c.action?.kind === "message" && r.sender.verified) {
10246
10317
  const envelope = renderMessage(c, { sender: r.sender, body: { message: params.message }, callId });
10247
10318
  const wire = JSON.stringify({ role: "user", content: { type: "text", text: envelope } });
10248
10319
  await deliverUserMessageCore(wire, params.localId || callId, void 0, true);
@@ -17851,6 +17922,197 @@ function shouldForceReconnect(consecutiveHeartbeatFailures) {
17851
17922
  return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
17852
17923
  }
17853
17924
 
17925
+ function acquireSupervisorLock(pidFile, pid = process.pid) {
17926
+ try {
17927
+ const fd = openSync(pidFile, "wx");
17928
+ try {
17929
+ writeSync(fd, String(pid));
17930
+ } finally {
17931
+ closeSync(fd);
17932
+ }
17933
+ return { acquired: true };
17934
+ } catch (err) {
17935
+ if (err?.code !== "EEXIST") throw err;
17936
+ }
17937
+ let existingPid = 0;
17938
+ try {
17939
+ existingPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
17940
+ } catch {
17941
+ }
17942
+ if (!existingPid || Number.isNaN(existingPid) || existingPid <= 0) {
17943
+ try {
17944
+ unlinkSync$1(pidFile);
17945
+ } catch {
17946
+ }
17947
+ return { acquired: false, staleCleaned: true };
17948
+ }
17949
+ if (isPidAlive(existingPid)) {
17950
+ return { acquired: false, heldBy: existingPid };
17951
+ }
17952
+ try {
17953
+ unlinkSync$1(pidFile);
17954
+ } catch {
17955
+ }
17956
+ return { acquired: false, staleCleaned: true };
17957
+ }
17958
+ function acquireSupervisorLockWithRetry(pidFile, pid = process.pid) {
17959
+ let result = acquireSupervisorLock(pidFile, pid);
17960
+ if (!result.acquired && result.staleCleaned) {
17961
+ result = acquireSupervisorLock(pidFile, pid);
17962
+ }
17963
+ return { acquired: result.acquired, heldBy: result.heldBy };
17964
+ }
17965
+ function releaseSupervisorLock(pidFile, pid = process.pid) {
17966
+ try {
17967
+ if (!existsSync(pidFile)) return;
17968
+ const content = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
17969
+ if (content === pid) unlinkSync$1(pidFile);
17970
+ } catch {
17971
+ }
17972
+ }
17973
+ function isPidAlive(pid) {
17974
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
17975
+ try {
17976
+ process.kill(pid, 0);
17977
+ return true;
17978
+ } catch (err) {
17979
+ return err?.code === "EPERM";
17980
+ }
17981
+ }
17982
+ function isSupervisorPid(pid) {
17983
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
17984
+ if (process.platform === "win32") return null;
17985
+ try {
17986
+ const out = execFileSync("ps", ["-o", "command=", "-p", String(pid)], {
17987
+ encoding: "utf-8",
17988
+ timeout: 5e3,
17989
+ stdio: ["ignore", "pipe", "ignore"]
17990
+ }).trim();
17991
+ if (!out) return false;
17992
+ return /daemon\s+start-supervised/.test(out);
17993
+ } catch {
17994
+ return null;
17995
+ }
17996
+ }
17997
+ function shouldSignalSupervisor(opts) {
17998
+ const { pid, alive, isSupervisor } = opts;
17999
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
18000
+ if (!alive) return false;
18001
+ if (isSupervisor === false) return false;
18002
+ return true;
18003
+ }
18004
+ function findOrphanedSyncPids(currentSupervisorPid = process.pid) {
18005
+ if (process.platform === "win32") return [];
18006
+ const pids = [];
18007
+ try {
18008
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
18009
+ const pgrepArgs = uid !== void 0 ? ["-u", String(uid), "-af", "svamp daemon start-sync"] : ["-af", "svamp daemon start-sync"];
18010
+ const output = execFileSync("pgrep", pgrepArgs, {
18011
+ encoding: "utf-8",
18012
+ timeout: 5e3
18013
+ });
18014
+ for (const line of output.split("\n")) {
18015
+ const trimmed = line.trim();
18016
+ if (!trimmed) continue;
18017
+ const match = trimmed.match(/^(\d+)\s/);
18018
+ if (!match) continue;
18019
+ const pid = parseInt(match[1], 10);
18020
+ if (Number.isNaN(pid) || pid <= 0) continue;
18021
+ if (pid === process.pid) continue;
18022
+ const ppid = getPpid(pid);
18023
+ if (ppid === currentSupervisorPid) continue;
18024
+ pids.push(pid);
18025
+ }
18026
+ } catch (err) {
18027
+ if (err?.status !== 1) ;
18028
+ }
18029
+ return pids;
18030
+ }
18031
+ function getPpid(pid) {
18032
+ if (process.platform === "win32") return 0;
18033
+ try {
18034
+ const out = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
18035
+ encoding: "utf-8",
18036
+ timeout: 2e3
18037
+ }).trim();
18038
+ const ppid = parseInt(out, 10);
18039
+ return Number.isNaN(ppid) ? 0 : ppid;
18040
+ } catch {
18041
+ return 0;
18042
+ }
18043
+ }
18044
+ async function killOrphanedSyncs(pids, opts = {}) {
18045
+ if (pids.length === 0) return;
18046
+ const log = opts.log || (() => {
18047
+ });
18048
+ const gracePeriodMs = opts.gracePeriodMs ?? 3e3;
18049
+ log(`Killing ${pids.length} orphan sync daemon(s): ${pids.join(", ")}`);
18050
+ for (const pid of pids) {
18051
+ try {
18052
+ process.kill(pid, "SIGTERM");
18053
+ } catch {
18054
+ }
18055
+ }
18056
+ const pollStep = 100;
18057
+ const steps = Math.max(1, Math.ceil(gracePeriodMs / pollStep));
18058
+ for (let i = 0; i < steps; i++) {
18059
+ await new Promise((r) => setTimeout(r, pollStep));
18060
+ if (pids.every((p) => !isPidAlive(p))) {
18061
+ log("All orphan daemons exited cleanly");
18062
+ return;
18063
+ }
18064
+ }
18065
+ const survivors = pids.filter(isPidAlive);
18066
+ if (survivors.length > 0) {
18067
+ log(`Force-killing survivors: ${survivors.join(", ")}`);
18068
+ for (const pid of survivors) {
18069
+ try {
18070
+ process.kill(pid, "SIGKILL");
18071
+ } catch {
18072
+ }
18073
+ }
18074
+ }
18075
+ }
18076
+ function watchParentLiveness(opts) {
18077
+ if (process.env.SVAMP_SUPERVISED !== "1") {
18078
+ return () => {
18079
+ };
18080
+ }
18081
+ const supervisorPid = process.ppid;
18082
+ if (!supervisorPid || supervisorPid === 1) {
18083
+ return () => {
18084
+ };
18085
+ }
18086
+ const intervalMs = opts.intervalMs ?? 5e3;
18087
+ let fired = false;
18088
+ const timer = setInterval(() => {
18089
+ if (fired) return;
18090
+ if (!isPidAlive(supervisorPid)) {
18091
+ fired = true;
18092
+ clearInterval(timer);
18093
+ opts.onParentDeath();
18094
+ }
18095
+ }, intervalMs);
18096
+ timer.unref?.();
18097
+ return () => {
18098
+ clearInterval(timer);
18099
+ };
18100
+ }
18101
+
18102
+ var supervisorLock = /*#__PURE__*/Object.freeze({
18103
+ __proto__: null,
18104
+ acquireSupervisorLock: acquireSupervisorLock,
18105
+ acquireSupervisorLockWithRetry: acquireSupervisorLockWithRetry,
18106
+ findOrphanedSyncPids: findOrphanedSyncPids,
18107
+ getPpid: getPpid,
18108
+ isPidAlive: isPidAlive,
18109
+ isSupervisorPid: isSupervisorPid,
18110
+ killOrphanedSyncs: killOrphanedSyncs,
18111
+ releaseSupervisorLock: releaseSupervisorLock,
18112
+ shouldSignalSupervisor: shouldSignalSupervisor,
18113
+ watchParentLiveness: watchParentLiveness
18114
+ });
18115
+
17854
18116
  const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
17855
18117
  function generateHookSettings(portOrOptions = {}) {
17856
18118
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
@@ -18811,7 +19073,9 @@ function spawnHeadlessEvaluator(prompt, opts) {
18811
19073
  const evalArgs = [
18812
19074
  "--print",
18813
19075
  prompt,
18814
- // Read-only reviewer (mirrors .claude/agents/loop-evaluator.md): it inspects real repo
19076
+ // Read-only reviewer (#0995: NOT the retired .claude/agents/loop-evaluator.md subagent
19077
+ // this is a separate `claude --print` process whose prompt buildEvaluatorPrompt owns,
19078
+ // which is what keeps the #0189 independence invariant): it inspects real repo
18815
19079
  // state but must not mutate the working tree it's judging. The allowedTools whitelist
18816
19080
  // already excludes mutation tools; --disallowedTools is belt-and-suspenders. Kept to
18817
19081
  // Write/Edit only — MultiEdit/NotebookEdit aren't known tool names on some claude builds
@@ -19337,6 +19601,26 @@ function clearSessionArchivedFlag(sessionId) {
19337
19601
  return null;
19338
19602
  }
19339
19603
  }
19604
+ function loadArchivedSessionInfos(isLive) {
19605
+ const out = [];
19606
+ const index = loadSessionIndex();
19607
+ for (const [sessionId, entry] of Object.entries(index)) {
19608
+ if (isLive(sessionId)) continue;
19609
+ try {
19610
+ const data = JSON.parse(readFileSync$1(getSessionFilePath(entry.directory, sessionId), "utf-8"));
19611
+ if (!data?.sessionId) continue;
19612
+ out.push({
19613
+ sessionId: data.sessionId,
19614
+ startedBy: data.startedBy || "unknown",
19615
+ directory: data.directory || entry.directory,
19616
+ active: false,
19617
+ metadata: data.metadata
19618
+ });
19619
+ } catch {
19620
+ }
19621
+ }
19622
+ return out;
19623
+ }
19340
19624
  function loadPersistedSessions() {
19341
19625
  const sessions = [];
19342
19626
  const index = loadSessionIndex();
@@ -19487,7 +19771,7 @@ async function startDaemon(options) {
19487
19771
  process.on("SIGINT", () => requestShutdown("os-signal"));
19488
19772
  process.on("SIGTERM", () => requestShutdown("os-signal"));
19489
19773
  process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
19490
- const { watchParentLiveness } = await import('./supervisorLock-DmfzJx7B.mjs');
19774
+ const { watchParentLiveness } = await Promise.resolve().then(function () { return supervisorLock; });
19491
19775
  const cancelParentWatchdog = watchParentLiveness({
19492
19776
  intervalMs: 5e3,
19493
19777
  onParentDeath: () => {
@@ -19680,7 +19964,7 @@ async function startDaemon(options) {
19680
19964
  try {
19681
19965
  const dir = loadSessionIndex()[sessionId]?.directory;
19682
19966
  if (!dir) return;
19683
- const { reconcileServiceLinks } = await import('./agentCommands-CjbndvZ2.mjs');
19967
+ const { reconcileServiceLinks } = await import('./agentCommands-BE3bFu8e.mjs');
19684
19968
  const configPath = getSvampConfigPath(dir, sessionId);
19685
19969
  const config = readSvampConfig(configPath);
19686
19970
  const entries = Array.from(urls.entries());
@@ -19702,7 +19986,7 @@ async function startDaemon(options) {
19702
19986
  try {
19703
19987
  const dir = loadSessionIndex()[sessionId]?.directory;
19704
19988
  if (!dir) return;
19705
- const { reconcileServiceLinks } = await import('./agentCommands-CjbndvZ2.mjs');
19989
+ const { reconcileServiceLinks } = await import('./agentCommands-BE3bFu8e.mjs');
19706
19990
  const configPath = getSvampConfigPath(dir, sessionId);
19707
19991
  const config = readSvampConfig(configPath);
19708
19992
  const incoming = [{
@@ -19723,7 +20007,7 @@ async function startDaemon(options) {
19723
20007
  try {
19724
20008
  const dir = loadSessionIndex()[sessionId]?.directory;
19725
20009
  if (!dir) return;
19726
- const { dropServiceLinks } = await import('./agentCommands-CjbndvZ2.mjs');
20010
+ const { dropServiceLinks } = await import('./agentCommands-BE3bFu8e.mjs');
19727
20011
  const configPath = getSvampConfigPath(dir, sessionId);
19728
20012
  const config = readSvampConfig(configPath);
19729
20013
  if (dropServiceLinks(config, "serve", mountName)) {
@@ -19766,7 +20050,7 @@ async function startDaemon(options) {
19766
20050
  ensureAutoInstalledCommands(logger);
19767
20051
  (async () => {
19768
20052
  try {
19769
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-C8CmQv3y.mjs');
20053
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-8SZDu7O-.mjs');
19770
20054
  beginClaudeVersionReconcile((msg) => logger.log(msg));
19771
20055
  } catch (e) {
19772
20056
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -22086,21 +22370,18 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22086
22370
  logger.log(`[Session ${sessionId}] Bash: ${command} (cwd: ${cwd || directory}, timeout: ${execTimeout}ms)`);
22087
22371
  const { exec: exec2 } = await import('child_process');
22088
22372
  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
- }
22373
+ exec2(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
22374
+ if (err) resolve2(execFailureOutcome(err, stdout, stderr));
22375
+ else resolve2(execSuccessOutcome(stdout, stderr));
22095
22376
  });
22096
22377
  });
22097
22378
  },
22098
22379
  onIssue: async (params) => {
22099
- const { issueRpc } = await import('./rpc-BosOiRY-.mjs');
22380
+ const { issueRpc } = await import('./rpc-D_pFjWr1.mjs');
22100
22381
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22101
22382
  },
22102
22383
  onWorkflow: async (params) => {
22103
- const { workflowRpc } = await import('./rpc-DP4CiR0g.mjs');
22384
+ const { workflowRpc } = await import('./rpc-D9LFUW93.mjs');
22104
22385
  return workflowRpc(params?.cwd || directory, params || {});
22105
22386
  },
22106
22387
  onRipgrep: async (args, cwd) => {
@@ -22797,21 +23078,18 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22797
23078
  const execTimeout = timeout || 12e4;
22798
23079
  const { exec: exec2 } = await import('child_process');
22799
23080
  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
- }
23081
+ exec2(command, { cwd: cwd || directory, timeout: execTimeout, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
23082
+ if (err) resolve2(execFailureOutcome(err, stdout, stderr));
23083
+ else resolve2(execSuccessOutcome(stdout, stderr));
22806
23084
  });
22807
23085
  });
22808
23086
  },
22809
23087
  onIssue: async (params) => {
22810
- const { issueRpc } = await import('./rpc-BosOiRY-.mjs');
23088
+ const { issueRpc } = await import('./rpc-D_pFjWr1.mjs');
22811
23089
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22812
23090
  },
22813
23091
  onWorkflow: async (params) => {
22814
- const { workflowRpc } = await import('./rpc-DP4CiR0g.mjs');
23092
+ const { workflowRpc } = await import('./rpc-D9LFUW93.mjs');
22815
23093
  return workflowRpc(params?.cwd || directory, params || {});
22816
23094
  },
22817
23095
  onRipgrep: async (args, cwd) => {
@@ -23640,6 +23918,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23640
23918
  logger.log("Shutdown requested via hypha-app (ignored \u2014 daemon never self-terminates)");
23641
23919
  },
23642
23920
  getTrackedSessions: getCurrentChildren,
23921
+ // #0970: archived (stopped-but-persisted) sessions, so CLI id resolution can find them.
23922
+ getArchivedSessions: () => {
23923
+ const live = new Set(getCurrentChildren().map((c) => c.sessionId));
23924
+ return loadArchivedSessionInfos((id) => live.has(id));
23925
+ },
23643
23926
  getSessionRPCHandlers: (sessionId) => {
23644
23927
  for (const [, session] of pidToTrackedSession) {
23645
23928
  if (session.svampSessionId === sessionId && !session.stopped && session.sessionRPCHandlers) {
@@ -23814,10 +24097,17 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23814
24097
  } catch {
23815
24098
  sharing = null;
23816
24099
  }
24100
+ let machineSharing = null;
24101
+ try {
24102
+ machineSharing = machineService.getCurrentMetadata()?.sharing ?? null;
24103
+ } catch {
24104
+ machineSharing = null;
24105
+ }
23817
24106
  return {
23818
24107
  cwd: session.directory,
23819
24108
  sharing,
23820
- ownerEmail: sharing?.owner || daemonOwnerEmail
24109
+ ownerEmail: sharing?.owner || daemonOwnerEmail,
24110
+ machineSharing
23821
24111
  };
23822
24112
  }
23823
24113
  }
@@ -23863,7 +24153,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23863
24153
  }
23864
24154
  if (persistedSessions.length > 0) {
23865
24155
  try {
23866
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-C8CmQv3y.mjs');
24156
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-8SZDu7O-.mjs');
23867
24157
  await awaitClaudeVersionReady();
23868
24158
  } catch {
23869
24159
  }
@@ -23964,8 +24254,16 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23964
24254
  const supPidFile = join(SVAMP_HOME, "supervisor.pid");
23965
24255
  const supPid = existsSync$1(supPidFile) ? parseInt(readFileSync$1(supPidFile, "utf-8").trim(), 10) : NaN;
23966
24256
  if (supPid && !isNaN(supPid)) {
23967
- process.kill(supPid, "SIGUSR2");
23968
- return;
24257
+ const ok = shouldSignalSupervisor({
24258
+ pid: supPid,
24259
+ alive: isPidAlive(supPid),
24260
+ isSupervisor: isSupervisorPid(supPid)
24261
+ });
24262
+ if (ok) {
24263
+ process.kill(supPid, "SIGUSR2");
24264
+ return;
24265
+ }
24266
+ logger.log(`[graceful-restart] supervisor.pid ${supPid} is stale or not a supervisor \u2014 not signalling`);
23969
24267
  }
23970
24268
  } catch {
23971
24269
  }
@@ -24112,7 +24410,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24112
24410
  const PING_TIMEOUT_MS = 15e3;
24113
24411
  const POST_RECONNECT_GRACE_MS = 2e4;
24114
24412
  const RECONNECT_JITTER_MS = 2500;
24115
- const { WorkflowScheduler } = await import('./scheduler-BN9GuvGN.mjs');
24413
+ const { WorkflowScheduler } = await import('./scheduler-bgEMu7PK.mjs');
24116
24414
  const workflowProjectRoots = () => {
24117
24415
  const dirs = /* @__PURE__ */ new Set();
24118
24416
  for (const s of pidToTrackedSession.values()) {
@@ -24745,4 +25043,4 @@ var run = /*#__PURE__*/Object.freeze({
24745
25043
  writeStopMarker: writeStopMarker
24746
25044
  });
24747
25045
 
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 };
25046
+ 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 };