svamp-cli 0.2.319 → 0.2.321

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-DBADX_Uo.mjs → adminCommands-BRiz5afS.mjs} +1 -1
  2. package/dist/{agentCommands-BJeGwkeX.mjs → agentCommands-Cve_0_8K.mjs} +32 -6
  3. package/dist/{auth-0NUGwOc0.mjs → auth-DrLE8Yp-.mjs} +1 -1
  4. package/dist/{cli-CWElpiIq.mjs → cli-xMKB7O8k.mjs} +89 -85
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-BsaTwKnd.mjs → commands-BxHc_Exm.mjs} +3 -3
  7. package/dist/{commands-C0lwV0U3.mjs → commands-C-QNVo3l.mjs} +5 -3
  8. package/dist/{commands-DubEbPAY.mjs → commands-CXgDZgqh.mjs} +4 -2
  9. package/dist/{commands-AdVHZXK6.mjs → commands-D1d9snmv.mjs} +1 -1
  10. package/dist/{commands-CJWgDDl5.mjs → commands-D9-YqAI3.mjs} +5 -3
  11. package/dist/{commands-CLgdrTSU.mjs → commands-DfRahEsO.mjs} +5 -3
  12. package/dist/{commands-DMSkWb0h.mjs → commands-Fvgfzyo3.mjs} +34 -9
  13. package/dist/{commands-BY1lmXaM.mjs → commands-slA1eX13.mjs} +1 -1
  14. package/dist/{fleet-OKnU0cYK.mjs → fleet-umFHXPoE.mjs} +2 -2
  15. package/dist/{headlessCli-CCO0IFYl.mjs → headlessCli-DgJ15daM.mjs} +2 -2
  16. package/dist/index.mjs +1 -1
  17. package/dist/{notifyCommands-4GUy4Mcl.mjs → notifyCommands-Cip4lIYS.mjs} +1 -1
  18. package/dist/package-DJRwvqcx.mjs +64 -0
  19. package/dist/{pinnedClaudeCode-dbXgsGNK.mjs → pinnedClaudeCode-BkZfX5pW.mjs} +1 -1
  20. package/dist/{rpc-FGp88Yxp.mjs → rpc-Bz9w7JV9.mjs} +1 -1
  21. package/dist/{rpc-TXF-di9u.mjs → rpc-CLoO6Z5Z.mjs} +1 -1
  22. package/dist/{run-09AkgeGM.mjs → run-CKLatoxa.mjs} +268 -94
  23. package/dist/{run-7TVWXnOM.mjs → run-CxuKTe0N.mjs} +1 -1
  24. package/dist/{scheduler-b-ysyKIj.mjs → scheduler-Bs0nAlhs.mjs} +1 -1
  25. package/dist/{serveCommands-D3W7w0G6.mjs → serveCommands-Bi7DJs-3.mjs} +33 -8
  26. package/dist/{sideband-C1db5sMF.mjs → sideband-CFbMRvzr.mjs} +1 -1
  27. package/package.json +3 -3
  28. package/dist/package-BEEqsQhN.mjs +0 -64
@@ -1261,11 +1261,11 @@ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
1261
1261
  }
1262
1262
  }
1263
1263
 
1264
- function svampHome() {
1264
+ function svampHome$1() {
1265
1265
  return process.env.SVAMP_HOME || join(homedir(), ".svamp");
1266
1266
  }
1267
1267
  function cacheFile() {
1268
- return join(svampHome(), "instance-config.json");
1268
+ return join(svampHome$1(), "instance-config.json");
1269
1269
  }
1270
1270
  const CONFIG_FILENAME = "svamp.json";
1271
1271
  let _config = null;
@@ -1308,7 +1308,7 @@ function readCache() {
1308
1308
  }
1309
1309
  function writeCache(cfg) {
1310
1310
  try {
1311
- mkdirSync(svampHome(), { recursive: true });
1311
+ mkdirSync(svampHome$1(), { recursive: true });
1312
1312
  writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
1313
1313
  } catch {
1314
1314
  }
@@ -1444,7 +1444,7 @@ function getSkillsCollection() {
1444
1444
  }
1445
1445
 
1446
1446
  function getMachineFingerprint() {
1447
- const idFile = join(homedir(), ".svamp", "machine-id");
1447
+ const idFile = join(svampHome(), "machine-id");
1448
1448
  try {
1449
1449
  const existing = readFileSync$1(idFile, "utf-8").trim();
1450
1450
  if (existing) return existing;
@@ -1452,15 +1452,18 @@ function getMachineFingerprint() {
1452
1452
  }
1453
1453
  const id = randomUUID();
1454
1454
  try {
1455
- mkdirSync(join(homedir(), ".svamp"), { recursive: true });
1455
+ mkdirSync(svampHome(), { recursive: true });
1456
1456
  writeFileSync(idFile, id + "\n");
1457
1457
  } catch {
1458
1458
  }
1459
1459
  return id;
1460
1460
  }
1461
+ function svampHome() {
1462
+ return process.env.SVAMP_HOME || join(homedir(), ".svamp");
1463
+ }
1461
1464
  const FRP_VERSION = "0.68.0";
1462
- const BIN_DIR = join(homedir(), ".svamp", "bin");
1463
- const FRPC_BIN = join(BIN_DIR, platform() === "win32" ? "frpc.exe" : "frpc");
1465
+ const binDir = () => join(svampHome(), "bin");
1466
+ const frpcBin = () => join(binDir(), platform() === "win32" ? "frpc.exe" : "frpc");
1464
1467
  const _livingFrpcPids = /* @__PURE__ */ new Set();
1465
1468
  function isFrpcSubdomainConflict(errorLine) {
1466
1469
  return /router config conflict|already exists/i.test(errorLine);
@@ -1469,15 +1472,17 @@ function shouldGiveUpOnConflict(args) {
1469
1472
  if (args.everStartedProxy) return false;
1470
1473
  return args.conflictCount >= args.minAttempts && args.elapsedMs >= args.minElapsedMs;
1471
1474
  }
1472
- function selectOrphanedFrpcPids(psOutput, selfPid, livingPids) {
1475
+ function selectOrphanedFrpcPids(psOutput, selfPid, livingPids, managedBinPath = frpcBin()) {
1473
1476
  const pids = [];
1474
1477
  for (const line of psOutput.split("\n")) {
1475
- const m = /^\s*(\d+)\s+(.*)$/.exec(line);
1478
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
1476
1479
  if (!m) continue;
1477
1480
  const pid = Number(m[1]);
1478
- const cmd = m[2];
1479
- if (!cmd.includes(".svamp/bin/frpc")) continue;
1481
+ const ppid = Number(m[2]);
1482
+ const cmd = m[3];
1483
+ if (!cmd.includes(".svamp/bin/frpc") && !cmd.includes(managedBinPath)) continue;
1480
1484
  if (pid === selfPid || livingPids.has(pid)) continue;
1485
+ if (ppid !== 1) continue;
1481
1486
  pids.push(pid);
1482
1487
  }
1483
1488
  return pids;
@@ -1485,7 +1490,7 @@ function selectOrphanedFrpcPids(psOutput, selfPid, livingPids) {
1485
1490
  function killOrphanedFrpc(log) {
1486
1491
  if (process.platform === "win32") return;
1487
1492
  try {
1488
- const out = execSync("ps -axo pid=,command=", { encoding: "utf-8" });
1493
+ const out = execSync("ps -axo pid=,ppid=,command=", { encoding: "utf-8" });
1489
1494
  const pids = selectOrphanedFrpcPids(out, process.pid, _livingFrpcPids);
1490
1495
  for (const pid of pids) {
1491
1496
  try {
@@ -1494,7 +1499,7 @@ function killOrphanedFrpc(log) {
1494
1499
  }
1495
1500
  }
1496
1501
  if (pids.length) {
1497
- log(`Reaped ${pids.length} orphaned frpc process(es) from a prior daemon generation \u2014 they were retrying stale subdomains and causing frps router-config-conflict churn.`);
1502
+ log(`Reaped ${pids.length} orphaned frpc process(es) (reparented to init) from a prior daemon generation \u2014 they were retrying stale subdomains and causing frps router-config-conflict churn.`);
1498
1503
  }
1499
1504
  } catch {
1500
1505
  }
@@ -1542,10 +1547,10 @@ function getFrpcDownloadUrl() {
1542
1547
  }
1543
1548
  let _frpcDownloadInFlight = null;
1544
1549
  async function ensureFrpc(log) {
1545
- if (existsSync$1(FRPC_BIN)) {
1550
+ if (existsSync$1(frpcBin())) {
1546
1551
  try {
1547
- const out = execSync(`"${FRPC_BIN}" --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
1548
- if (out === FRP_VERSION) return FRPC_BIN;
1552
+ const out = execSync(`"${frpcBin()}" --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
1553
+ if (out === FRP_VERSION) return frpcBin();
1549
1554
  (log || console.log)(`frpc version mismatch: ${out} \u2260 ${FRP_VERSION}, re-downloading...`);
1550
1555
  } catch {
1551
1556
  (log || console.log)("frpc binary broken or invalid signature, re-downloading...");
@@ -1559,11 +1564,11 @@ async function ensureFrpc(log) {
1559
1564
  }
1560
1565
  async function downloadFrpc(log) {
1561
1566
  try {
1562
- if (existsSync$1(FRPC_BIN)) unlinkSync(FRPC_BIN);
1567
+ if (existsSync$1(frpcBin())) unlinkSync(frpcBin());
1563
1568
  } catch {
1564
1569
  }
1565
1570
  const logger = log || console.log;
1566
- mkdirSync(BIN_DIR, { recursive: true });
1571
+ mkdirSync(binDir(), { recursive: true });
1567
1572
  const url = getFrpcDownloadUrl();
1568
1573
  logger(`Downloading frpc ${FRP_VERSION} from ${url}...`);
1569
1574
  const ctrl = new AbortController();
@@ -1584,23 +1589,23 @@ async function downloadFrpc(log) {
1584
1589
  if (platform() === "win32") {
1585
1590
  throw new Error("Windows ZIP extraction not implemented \u2014 please install frpc manually");
1586
1591
  }
1587
- const tmpTar = join(BIN_DIR, `frpc-${FRP_VERSION}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.tar.gz`);
1592
+ const tmpTar = join(binDir(), `frpc-${FRP_VERSION}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.tar.gz`);
1588
1593
  writeFileSync(tmpTar, buffer);
1589
1594
  try {
1590
1595
  const dirName = `frp_${FRP_VERSION}_${platform() === "darwin" ? "darwin" : "linux"}_${arch() === "x64" ? "amd64" : arch()}`;
1591
1596
  execSync(
1592
- `tar -xzf "${tmpTar}" -C "${BIN_DIR}" --strip-components=1 "${dirName}/frpc"`,
1597
+ `tar -xzf "${tmpTar}" -C "${binDir()}" --strip-components=1 "${dirName}/frpc"`,
1593
1598
  { stdio: "pipe" }
1594
1599
  );
1595
- chmodSync(FRPC_BIN, 493);
1596
- logger(`frpc installed at ${FRPC_BIN}`);
1600
+ chmodSync(frpcBin(), 493);
1601
+ logger(`frpc installed at ${frpcBin()}`);
1597
1602
  } finally {
1598
1603
  try {
1599
1604
  unlinkSync(tmpTar);
1600
1605
  } catch {
1601
1606
  }
1602
1607
  }
1603
- return FRPC_BIN;
1608
+ return frpcBin();
1604
1609
  }
1605
1610
  function tomlStr(v) {
1606
1611
  return String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
@@ -1799,7 +1804,7 @@ class FrpcTunnel {
1799
1804
  healthCheckInterval: options.healthCheckInterval
1800
1805
  };
1801
1806
  });
1802
- const configDir = join(homedir(), ".svamp", "frpc");
1807
+ const configDir = join(svampHome(), "frpc");
1803
1808
  mkdirSync(configDir, { recursive: true, mode: 448 });
1804
1809
  try {
1805
1810
  chmodSync(configDir, 448);
@@ -2967,9 +2972,9 @@ const NON_ADMIN_SECRET_DIR_SEGMENTS = /* @__PURE__ */ new Set([
2967
2972
  ".azure",
2968
2973
  ".npmrc"
2969
2974
  ]);
2970
- function assertNonAdminMountDirSafe(directory, homeDir) {
2971
- const resolved = path.resolve(directory);
2972
- const home = path.resolve(homeDir);
2975
+ function assertNonAdminMountDirSafe(directory, homeDir, resolveReal = defaultResolveReal) {
2976
+ const resolved = resolveReal(path.resolve(directory));
2977
+ const home = resolveReal(path.resolve(homeDir));
2973
2978
  if (resolved === home) {
2974
2979
  throw new Error("Access denied: shared users may not serve the home directory root (exposes all dotfiles)");
2975
2980
  }
@@ -2978,12 +2983,24 @@ function assertNonAdminMountDirSafe(directory, homeDir) {
2978
2983
  if (!insideHome) {
2979
2984
  throw new Error("Access denied: shared users may only serve directories within the home directory");
2980
2985
  }
2981
- for (const seg of resolved.split(path.sep)) {
2982
- if (NON_ADMIN_SECRET_DIR_SEGMENTS.has(seg)) {
2983
- throw new Error(`Access denied: refusing to serve a sensitive directory (${seg})`);
2986
+ for (const seg of rel.split(path.sep)) {
2987
+ if (seg.startsWith(".")) {
2988
+ const known = NON_ADMIN_SECRET_DIR_SEGMENTS.has(seg);
2989
+ throw new Error(known ? `Access denied: refusing to serve a sensitive directory (${seg})` : `Access denied: shared users may not serve a hidden directory (${seg}) \u2014 it may hold credentials`);
2984
2990
  }
2985
2991
  }
2986
2992
  }
2993
+ function defaultResolveReal(p) {
2994
+ try {
2995
+ return fs.realpathSync(p);
2996
+ } catch {
2997
+ return p;
2998
+ }
2999
+ }
3000
+ function resolveMountOwnerEmail(suppliedOwnerEmail, callerEmail, machineOwnerEmail, callerIsTrusted, envOwnerEmail) {
3001
+ const supplied = callerIsTrusted ? suppliedOwnerEmail : void 0;
3002
+ return supplied || callerEmail || machineOwnerEmail || envOwnerEmail || void 0;
3003
+ }
2987
3004
  function sanitizeMountForRole(mount, isAdmin) {
2988
3005
  if (isAdmin) return mount;
2989
3006
  const { linkToken: _lt, directory: _dir, process: proc, url, ...rest } = mount;
@@ -3132,6 +3149,16 @@ class ServeManager {
3132
3149
  hyphaServerUrl;
3133
3150
  /** #126: daemon-provided lookup of sessionId → { cwd, sharing, ownerEmail } for the files gateway. */
3134
3151
  sessionResolver = null;
3152
+ /**
3153
+ * #0944/#0945: daemon-provided "this mount no longer backs that session's Launch Pad entry"
3154
+ * hook. Fired with the OWNING session id, which is the only correct target — the CLI-side
3155
+ * removeSessionLinkByService() defaults to the AMBIENT $SVAMP_SESSION_ID and resolves
3156
+ * `.svamp/<id>/config.json` relative to its own CWD, so it silently no-ops when the teardown
3157
+ * is run from a plain shell, from another session, or against a mount in another project dir.
3158
+ * Living here means EVERY path — the serveRemove RPC, the CLI, the app's Launch Pad "Remove &
3159
+ * stop", and a cross-session replace — drops the right link.
3160
+ */
3161
+ onMountUnbound = null;
3135
3162
  constructor(svampHome, logger, hyphaServerUrl) {
3136
3163
  this.persistFile = path.join(svampHome, "serve-mounts.json");
3137
3164
  this.managedPidDir = path.join(svampHome, "serve-managed");
@@ -3151,6 +3178,23 @@ class ServeManager {
3151
3178
  setSessionResolver(fn) {
3152
3179
  this.sessionResolver = fn;
3153
3180
  }
3181
+ /**
3182
+ * #0944/#0945: register the daemon's "drop this session's Launch Pad link for this mount"
3183
+ * callback. Fired when a mount stops backing a session's Pad entry — on a real removal, and
3184
+ * on a replace whose new owner is a DIFFERENT session. Best-effort by contract: the callback
3185
+ * must never throw, because Launch Pad bookkeeping may not fail a teardown the user asked for.
3186
+ */
3187
+ setMountUnboundHook(fn) {
3188
+ this.onMountUnbound = fn;
3189
+ }
3190
+ /** Fire the unbind hook, swallowing anything it throws (bookkeeping is never load-bearing). */
3191
+ fireMountUnbound(sessionId, mountName) {
3192
+ if (!sessionId || !this.onMountUnbound) return;
3193
+ try {
3194
+ this.onMountUnbound(sessionId, mountName);
3195
+ } catch {
3196
+ }
3197
+ }
3154
3198
  /**
3155
3199
  * #126: bring up the always-on, per-machine session-files gateway. Idempotent. `stableToken`
3156
3200
  * must be a stable per-machine hex/alnum string (DNS-label-safe) so the gateway's subdomain is
@@ -3235,9 +3279,14 @@ class ServeManager {
3235
3279
  if (resolvedDir && !fs.existsSync(resolvedDir)) {
3236
3280
  throw new Error(`Path does not exist: ${resolvedDir}`);
3237
3281
  }
3238
- if (this.mounts.has(spec.name)) {
3282
+ const replaced = this.mounts.get(spec.name);
3283
+ if (replaced) {
3239
3284
  await this.removeMount(spec.name, { replacing: true });
3240
3285
  }
3286
+ if (replaced?.sessionId && replaced.sessionId !== spec.sessionId) {
3287
+ this.log(`Mount '${spec.name}': taken over by session ${spec.sessionId || "(none)"} from ${replaced.sessionId} \u2014 the previous owner's Launch Pad link is being dropped (its URL no longer resolves).`);
3288
+ this.fireMountUnbound(replaced.sessionId, spec.name);
3289
+ }
3241
3290
  const access = spec.access ?? "link";
3242
3291
  if (access === "owner" && !spec.ownerEmail) {
3243
3292
  this.log(`\u26A0 Mount '${spec.name}': access='owner' but no owner email could be resolved \u2014 NO ONE will be able to sign in. Use an explicit email allowlist instead, e.g. access: ["you@example.com"].`);
@@ -3276,10 +3325,12 @@ class ServeManager {
3276
3325
  * Remove a mount. If no mounts remain, stop Caddy + auth proxy.
3277
3326
  */
3278
3327
  async removeMount(name, opts) {
3279
- if (!this.mounts.has(name)) {
3328
+ const removed = this.mounts.get(name);
3329
+ if (!removed) {
3280
3330
  throw new Error(`Mount '${name}' not found`);
3281
3331
  }
3282
3332
  this.mounts.delete(name);
3333
+ if (!opts?.replacing) this.fireMountUnbound(removed.sessionId, name);
3283
3334
  await this.stopManagedProcess(name).catch(() => {
3284
3335
  });
3285
3336
  const tunnel = this.mountTunnels.get(name);
@@ -3744,9 +3795,10 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3744
3795
  }
3745
3796
  }
3746
3797
  const rootDir = target.cwd;
3798
+ const normRel = this.toSessionRelative(rootDir, m[2] || "");
3747
3799
  if (method === "GET" || method === "HEAD") {
3748
3800
  if (url.searchParams.get("list") === "1") {
3749
- const lex = containedPath(rootDir, relPath.replace(/^\/+/, ""));
3801
+ const lex = containedPath(rootDir, normRel);
3750
3802
  if (!lex) {
3751
3803
  deny(403, "Forbidden");
3752
3804
  return;
@@ -3773,11 +3825,11 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3773
3825
  res.end(method === "HEAD" ? void 0 : JSON.stringify({ path: relPath, entries }));
3774
3826
  return;
3775
3827
  }
3776
- serveStaticMount(req, res, { rootDir, relPath, mountUrlPrefix: "", browse: false});
3828
+ serveStaticMount(req, res, { rootDir, relPath: normRel ? `/${normRel}` : "/", mountUrlPrefix: "", browse: false});
3777
3829
  return;
3778
3830
  }
3779
3831
  if (method === "PUT" || method === "DELETE") {
3780
- const filePath = this.resolveContainedPath(rootDir, relPath);
3832
+ const filePath = this.resolveContainedPath(rootDir, normRel);
3781
3833
  if (!filePath) {
3782
3834
  deny(403, "Forbidden: path escapes session root");
3783
3835
  return;
@@ -3839,6 +3891,27 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3839
3891
  }
3840
3892
  deny(405, "Method not allowed");
3841
3893
  }
3894
+ /**
3895
+ * #126 root-cause fix: the frontend builds gateway URLs from the file's ABSOLUTE path (the
3896
+ * session cwd + name), e.g. `/s/<id>/Users/me/proj/big.bin` for a file at
3897
+ * `/Users/me/proj/big.bin` in a session whose cwd is `/Users/me/proj`. A URL path can't carry
3898
+ * the leading slash, so a naive join against cwd DOUBLE-NESTS it
3899
+ * (`/Users/me/proj/Users/me/proj/big.bin`) — the file uploads with a 201 but lands in the wrong
3900
+ * place (and downloads 404). The RPC path never hit this because Node's `resolve(cwd, absPath)`
3901
+ * returns the absolute path as-is. Mirror that semantics here: reconstruct the remainder as an
3902
+ * absolute path; if it IS the cwd or lives under it, the caller meant an absolute path — return
3903
+ * it relative to cwd. Otherwise it's already cwd-relative. Returns a cwd-relative path with no
3904
+ * leading slash (containment is still enforced downstream by `containedPath`/`resolveContainedPath`).
3905
+ */
3906
+ toSessionRelative(rootDir, remainder) {
3907
+ const rel = (remainder || "").replace(/^\/+/, "");
3908
+ if (!rel) return "";
3909
+ const base = path.resolve(rootDir);
3910
+ const asAbsolute = path.resolve("/", rel);
3911
+ if (asAbsolute === base) return "";
3912
+ if (asAbsolute.startsWith(base + path.sep)) return asAbsolute.slice(base.length + 1);
3913
+ return rel;
3914
+ }
3842
3915
  /**
3843
3916
  * #126: resolve a session-relative path against `rootDir` with lexical + symlink containment
3844
3917
  * (mirrors the static PUT handler's resolveContained). Allows a not-yet-existing target (for
@@ -4264,6 +4337,10 @@ Connection: close\r
4264
4337
  async startMountTunnel(mountName) {
4265
4338
  if (this.mountTunnels.has(mountName)) return;
4266
4339
  if (!this.port) throw new Error("Auth proxy not running \u2014 call ensureRunning() first");
4340
+ if (process.env.SVAMP_SERVE_NO_TUNNEL === "1") {
4341
+ this.log(`Mount '${mountName}': tunnel skipped (SVAMP_SERVE_NO_TUNNEL=1) \u2014 local only at http://127.0.0.1:${this.port}/${mountName}/`);
4342
+ return;
4343
+ }
4267
4344
  const subdomainSafe = mountName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
4268
4345
  const tunnelName = `static-${subdomainSafe}`;
4269
4346
  const mount = this.mounts.get(mountName);
@@ -4319,6 +4396,7 @@ var serveManager = /*#__PURE__*/Object.freeze({
4319
4396
  ServeManager: ServeManager,
4320
4397
  assertNonAdminMountDirSafe: assertNonAdminMountDirSafe,
4321
4398
  buildLinkSubdomain: buildLinkSubdomain,
4399
+ resolveMountOwnerEmail: resolveMountOwnerEmail,
4322
4400
  sanitizeMountForRole: sanitizeMountForRole
4323
4401
  });
4324
4402
 
@@ -4469,6 +4547,10 @@ function setClaudeAuthCustom(baseUrl, apiKey) {
4469
4547
  ANTHROPIC_API_KEY: apiKey
4470
4548
  });
4471
4549
  }
4550
+ function applyPromptCacheTtl(spawnEnv) {
4551
+ const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
4552
+ if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
4553
+ }
4472
4554
  function applyClaudeProxyEnv(spawnEnv) {
4473
4555
  const mode = currentMode();
4474
4556
  if (mode === "hypha") {
@@ -4486,8 +4568,7 @@ function applyClaudeProxyEnv(spawnEnv) {
4486
4568
  }
4487
4569
  spawnEnv.ANTHROPIC_BASE_URL = proxyUrl;
4488
4570
  spawnEnv.ANTHROPIC_API_KEY = token;
4489
- const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
4490
- if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
4571
+ applyPromptCacheTtl(spawnEnv);
4491
4572
  return `hypha proxy (${proxyUrl}, 1h cache TTL)`;
4492
4573
  }
4493
4574
  if (mode === "custom") {
@@ -4500,7 +4581,8 @@ function applyClaudeProxyEnv(spawnEnv) {
4500
4581
  }
4501
4582
  spawnEnv.ANTHROPIC_BASE_URL = url;
4502
4583
  spawnEnv.ANTHROPIC_API_KEY = key;
4503
- return `custom proxy (${url})`;
4584
+ applyPromptCacheTtl(spawnEnv);
4585
+ return `custom proxy (${url}, 1h cache TTL)`;
4504
4586
  }
4505
4587
  delete spawnEnv.ANTHROPIC_BASE_URL;
4506
4588
  delete spawnEnv.ANTHROPIC_API_KEY;
@@ -6360,6 +6442,13 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6360
6442
  } catch {
6361
6443
  }
6362
6444
  const serveCallerTrusted = (context) => !context?.user || isSameOwnerWorkspace(context) || roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "admin");
6445
+ const resolveMountOwnerEmailFor = (supplied, context) => resolveMountOwnerEmail(
6446
+ supplied,
6447
+ context?.user?.email,
6448
+ currentMetadata.sharing?.owner,
6449
+ serveCallerTrusted(context),
6450
+ process.env.SVAMP_OWNER_EMAIL
6451
+ );
6363
6452
  let lastInboundRpcAt = Date.now();
6364
6453
  const trackInbound = () => {
6365
6454
  lastInboundRpcAt = Date.now();
@@ -7526,7 +7615,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7526
7615
  const { homedir } = await import('os');
7527
7616
  assertNonAdminMountDirSafe(params.directory, homedir());
7528
7617
  }
7529
- const ownerEmail2 = params.ownerEmail || context?.user?.email || currentMetadata.sharing?.owner || process.env.SVAMP_OWNER_EMAIL || void 0;
7618
+ if (params.sessionId && !serveCallerTrusted(context)) {
7619
+ await authorizeSessionAccess(params.sessionId, "admin", context);
7620
+ }
7621
+ const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7530
7622
  const access = params.access || "owner";
7531
7623
  return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
7532
7624
  },
@@ -7543,6 +7635,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7543
7635
  const { homedir } = await import('os');
7544
7636
  assertNonAdminMountDirSafe(params.directory, homedir());
7545
7637
  }
7638
+ if (params.sessionId && !serveCallerTrusted(context)) {
7639
+ await authorizeSessionAccess(params.sessionId, "admin", context);
7640
+ }
7546
7641
  const existingMount = sm.getMount(params.name);
7547
7642
  if (existingMount && !serveCallerTrusted(context)) {
7548
7643
  const callerEmail = (context?.user?.email || "").toLowerCase();
@@ -7551,7 +7646,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7551
7646
  throw new Error(`Not authorized to replace mount '${params.name}' (owned by another user)`);
7552
7647
  }
7553
7648
  }
7554
- const ownerEmail2 = params.ownerEmail || context?.user?.email || currentMetadata.sharing?.owner || process.env.SVAMP_OWNER_EMAIL || void 0;
7649
+ const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7555
7650
  const access = params.access ?? "owner";
7556
7651
  return sm.applyMount({
7557
7652
  name: params.name,
@@ -7921,7 +8016,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
7921
8016
  }
7922
8017
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
7923
8018
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
7924
- const { toolsForRole } = await import('./sideband-C1db5sMF.mjs');
8019
+ const { toolsForRole } = await import('./sideband-CFbMRvzr.mjs');
7925
8020
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
7926
8021
  return fmt(r2);
7927
8022
  }
@@ -8026,7 +8121,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8026
8121
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
8027
8122
  }
8028
8123
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
8029
- const { queryCore } = await import('./commands-BY1lmXaM.mjs');
8124
+ const { queryCore } = await import('./commands-slA1eX13.mjs');
8030
8125
  const timeout = c.reply?.timeout_sec || 120;
8031
8126
  let result;
8032
8127
  try {
@@ -8238,6 +8333,9 @@ ${d?.error || "not found"}`;
8238
8333
  });
8239
8334
  },
8240
8335
  getLastInboundRpcAt: () => lastInboundRpcAt,
8336
+ resetInboundClock: () => {
8337
+ lastInboundRpcAt = Date.now();
8338
+ },
8241
8339
  getCurrentMetadata: () => currentMetadata,
8242
8340
  setHandlers: (newHandlers) => {
8243
8341
  handlers = newHandlers;
@@ -16370,8 +16468,12 @@ function isVisibleTo(issue, session) {
16370
16468
  if (!issue.owner && !issue.session) return true;
16371
16469
  return false;
16372
16470
  }
16471
+ function normalizeIssueRef(id) {
16472
+ const s = String(id ?? "").trim().replace(/^#+/, "");
16473
+ return /^\d+$/.test(s) ? s.padStart(4, "0") : s;
16474
+ }
16373
16475
  function getIssue(projectRoot, id) {
16374
- const padded = /^\d+$/.test(id) ? id.padStart(4, "0") : id;
16476
+ const padded = normalizeIssueRef(id);
16375
16477
  let best = null;
16376
16478
  for (const p of [issuePath(projectRoot, padded), issuePath(projectRoot, padded, true)]) {
16377
16479
  if (!existsSync(p)) continue;
@@ -16383,7 +16485,14 @@ function getIssue(projectRoot, id) {
16383
16485
  } catch {
16384
16486
  }
16385
16487
  }
16386
- return best ? best.issue : null;
16488
+ if (best) return best.issue;
16489
+ try {
16490
+ for (const issue of listIssues(projectRoot, { includeArchived: true })) {
16491
+ if (normalizeIssueRef(issue.id) === padded) return issue;
16492
+ }
16493
+ } catch {
16494
+ }
16495
+ return null;
16387
16496
  }
16388
16497
  function atomicWrite(path, content) {
16389
16498
  mkdirSync$1(dirname(path), { recursive: true });
@@ -16432,8 +16541,7 @@ function addIssue(projectRoot, fields) {
16432
16541
  throw new Error("addIssue: could not allocate a free issue id");
16433
16542
  }
16434
16543
  function issueLockPath(projectRoot, id) {
16435
- const padded = /^\d+$/.test(id) ? id.padStart(4, "0") : id;
16436
- return join$1(issuesDir(projectRoot), `${padded}.lock`);
16544
+ return join$1(issuesDir(projectRoot), `${normalizeIssueRef(id)}.lock`);
16437
16545
  }
16438
16546
  function withIssueLock(projectRoot, id, fn) {
16439
16547
  try {
@@ -16446,16 +16554,20 @@ function _updateIssueUnlocked(projectRoot, id, patch) {
16446
16554
  const cur = getIssue(projectRoot, id);
16447
16555
  if (!cur) return null;
16448
16556
  const wasArchived = cur.status === "archived";
16449
- const next = { ...cur, ...patch, id: cur.id };
16557
+ const canonicalId = normalizeIssueRef(cur.id);
16558
+ const next = { ...cur, ...patch, id: canonicalId };
16450
16559
  if (patch.status) next.status = normalizeStatus(patch.status);
16451
16560
  const nowArchived = next.status === "archived";
16452
16561
  if (nowArchived && !next.closed) next.closed = (/* @__PURE__ */ new Date()).toISOString();
16453
16562
  if (!nowArchived) next.closed = null;
16454
16563
  atomicWrite(issuePath(projectRoot, next.id, nowArchived), serializeIssue(next));
16455
- if (wasArchived !== nowArchived) {
16456
- const oldPath = issuePath(projectRoot, cur.id, wasArchived);
16564
+ for (const stale of [
16565
+ issuePath(projectRoot, cur.id, wasArchived),
16566
+ issuePath(projectRoot, cur.id, nowArchived)
16567
+ ]) {
16568
+ if (stale === issuePath(projectRoot, next.id, nowArchived)) continue;
16457
16569
  try {
16458
- if (existsSync(oldPath)) unlinkSync$1(oldPath);
16570
+ if (existsSync(stale)) unlinkSync$1(stale);
16459
16571
  } catch {
16460
16572
  }
16461
16573
  }
@@ -17439,6 +17551,19 @@ function parseEvaluatorVerdict(text) {
17439
17551
  }
17440
17552
  return null;
17441
17553
  }
17554
+ function buildStaleSafeOracleGuidance(oracleOutput, now = /* @__PURE__ */ new Date()) {
17555
+ const reading = (oracleOutput || "").trim().slice(0, 500);
17556
+ return [
17557
+ `The loop oracle reported unfinished work as of ${now.toISOString()}:`,
17558
+ reading || "(no detail)",
17559
+ "",
17560
+ "That is a POINT-IN-TIME reading, not a fact about right now \u2014 the backlog may have changed",
17561
+ "since (an item can be closed or paused out-of-band while this message was queued). RE-RUN",
17562
+ "the oracle yourself and treat ITS CURRENT OUTPUT as the truth. If it is now empty, the goal",
17563
+ "is complete: say so rather than chasing the items listed above. Otherwise keep working until",
17564
+ "it is genuinely empty."
17565
+ ].join("\n");
17566
+ }
17442
17567
  async function runLoopVerification(cfg, deps) {
17443
17568
  const log = deps.log || (() => {
17444
17569
  });
@@ -17453,9 +17578,11 @@ async function runLoopVerification(cfg, deps) {
17453
17578
  return { action: "gave_up", reason: `oracle still reports pending work after ${holds} verification hold(s): ${oracleOutput.trim().slice(0, 200)}` };
17454
17579
  }
17455
17580
  log(`[loopVerify] oracle reports pending work \u2014 re-kicking instead of marking done`);
17456
- return { action: "rekick", reason: "oracle reports pending work", guidance: `The loop oracle still reports unfinished work, so the goal is NOT complete:
17457
- ${oracleOutput.trim().slice(0, 500)}
17458
- Keep working until it is genuinely empty.` };
17581
+ return {
17582
+ action: "rekick",
17583
+ reason: "oracle reports pending work",
17584
+ guidance: buildStaleSafeOracleGuidance(oracleOutput)
17585
+ };
17459
17586
  }
17460
17587
  } catch (e) {
17461
17588
  log(`[loopVerify] oracle runner error (continuing to evaluator): ${e?.message || e}`);
@@ -17684,6 +17811,17 @@ function compactUnsupportedWarning(version) {
17684
17811
  return `\u26A0\uFE0F /compact did not run${v} \u2014 Claude Code forwarded it as a normal message, so nothing was compacted. In-session /compact needs a newer Claude Code (>= 2.1.205). Ask the machine owner to upgrade (e.g. \`svamp fleet upgrade-claude\`), then try again.`;
17685
17812
  }
17686
17813
 
17814
+ function shouldRunZombieProbe(args) {
17815
+ return args.hasActiveSessions && !args.inGrace && args.consecutiveHeartbeatFailures === 0 && args.inboundSilenceMs > args.thresholdMs;
17816
+ }
17817
+ function nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures) {
17818
+ return consecutiveHeartbeatFailures + 1;
17819
+ }
17820
+ function shouldForceReconnect(consecutiveHeartbeatFailures) {
17821
+ if (consecutiveHeartbeatFailures < 2) return false;
17822
+ return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
17823
+ }
17824
+
17687
17825
  const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
17688
17826
  function generateHookSettings(portOrOptions = {}) {
17689
17827
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
@@ -18555,9 +18693,11 @@ function atomicWriteLoopState(path, obj) {
18555
18693
  writeFileSync(tmp, JSON.stringify(obj, null, 2));
18556
18694
  renameSync$1(tmp, path);
18557
18695
  }
18558
- function safeBacklogPendingCount(projectRoot, sessionId) {
18696
+ function safeBacklogPendingCount(directory, selfSessionId, oracle) {
18559
18697
  try {
18560
- return backlogOraclePending(projectRoot, sessionId).length;
18698
+ const root = resolveProjectRoot(directory);
18699
+ const scope = oracleScopeSessionId(typeof oracle === "string" ? oracle : void 0, selfSessionId);
18700
+ return backlogOraclePending(root, scope).length;
18561
18701
  } catch {
18562
18702
  return 0;
18563
18703
  }
@@ -19314,6 +19454,7 @@ async function startDaemon(options) {
19314
19454
  };
19315
19455
  });
19316
19456
  let consecutiveHeartbeatFailures = 0;
19457
+ let lastReconnectAt = 0;
19317
19458
  process.on("SIGINT", () => requestShutdown("os-signal"));
19318
19459
  process.on("SIGTERM", () => requestShutdown("os-signal"));
19319
19460
  process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
@@ -19510,7 +19651,7 @@ async function startDaemon(options) {
19510
19651
  try {
19511
19652
  const dir = loadSessionIndex()[sessionId]?.directory;
19512
19653
  if (!dir) return;
19513
- const { reconcileServiceLinks } = await import('./agentCommands-BJeGwkeX.mjs');
19654
+ const { reconcileServiceLinks } = await import('./agentCommands-Cve_0_8K.mjs');
19514
19655
  const configPath = getSvampConfigPath(dir, sessionId);
19515
19656
  const config = readSvampConfig(configPath);
19516
19657
  const entries = Array.from(urls.entries());
@@ -19532,7 +19673,7 @@ async function startDaemon(options) {
19532
19673
  try {
19533
19674
  const dir = loadSessionIndex()[sessionId]?.directory;
19534
19675
  if (!dir) return;
19535
- const { reconcileServiceLinks } = await import('./agentCommands-BJeGwkeX.mjs');
19676
+ const { reconcileServiceLinks } = await import('./agentCommands-Cve_0_8K.mjs');
19536
19677
  const configPath = getSvampConfigPath(dir, sessionId);
19537
19678
  const config = readSvampConfig(configPath);
19538
19679
  const incoming = [{
@@ -19548,6 +19689,22 @@ async function startDaemon(options) {
19548
19689
  logger.log(`[serve] Link reconcile failed for mount ${mountName}: ${err?.message || err}`);
19549
19690
  }
19550
19691
  }
19692
+ async function dropMountSessionLink(sessionId, mountName) {
19693
+ if (!sessionId) return;
19694
+ try {
19695
+ const dir = loadSessionIndex()[sessionId]?.directory;
19696
+ if (!dir) return;
19697
+ const { dropServiceLinks } = await import('./agentCommands-Cve_0_8K.mjs');
19698
+ const configPath = getSvampConfigPath(dir, sessionId);
19699
+ const config = readSvampConfig(configPath);
19700
+ if (dropServiceLinks(config, "serve", mountName)) {
19701
+ writeSvampConfig(configPath, config);
19702
+ logger.log(`[serve] Dropped Launch Pad link for mount ${mountName} \u2190 session ${sessionId}`);
19703
+ }
19704
+ } catch (err) {
19705
+ logger.log(`[serve] Link drop failed for mount ${mountName}: ${err?.message || err}`);
19706
+ }
19707
+ }
19551
19708
  async function createExposedTunnel(spec) {
19552
19709
  const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
19553
19710
  const tunnel = new FrpcTunnel({
@@ -19572,12 +19729,15 @@ async function startDaemon(options) {
19572
19729
  const tunnelRecreateInFlight = /* @__PURE__ */ new Set();
19573
19730
  const { ServeManager } = await Promise.resolve().then(function () { return serveManager; });
19574
19731
  const serveManager$1 = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
19732
+ serveManager$1.setMountUnboundHook((sessionId, mountName) => {
19733
+ void dropMountSessionLink(sessionId, mountName);
19734
+ });
19575
19735
  ensureAutoInstalledSkills(logger).catch(() => {
19576
19736
  });
19577
19737
  ensureAutoInstalledCommands(logger);
19578
19738
  (async () => {
19579
19739
  try {
19580
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-dbXgsGNK.mjs');
19740
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-BkZfX5pW.mjs');
19581
19741
  beginClaudeVersionReconcile((msg) => logger.log(msg));
19582
19742
  } catch (e) {
19583
19743
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -19690,10 +19850,14 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
19690
19850
  }
19691
19851
  };
19692
19852
  server.on("services_registered", () => {
19853
+ lastReconnectAt = Date.now();
19854
+ try {
19855
+ machineService.resetInboundClock?.();
19856
+ } catch {
19857
+ }
19693
19858
  if (consecutiveHeartbeatFailures > 0) {
19694
19859
  logger.log(`Hypha reconnection successful \u2014 services re-registered (resetting ${consecutiveHeartbeatFailures} failures)`);
19695
19860
  consecutiveHeartbeatFailures = 0;
19696
- lastReconnectAt = Date.now();
19697
19861
  }
19698
19862
  const reEmitLiveness = (phase) => {
19699
19863
  try {
@@ -20259,26 +20423,33 @@ ${parts.join("\n")}`);
20259
20423
  const oracleCmd = typeof ls.oracle === "string" && ls.oracle.trim() ? ls.oracle.trim() : void 0;
20260
20424
  try {
20261
20425
  sessionService.pushMessage({ type: "message", message: "\u{1F50D} Verifying loop completion (independent review)\u2026" }, "event");
20262
- const result = await runLoopVerification(
20263
- { task: ls.goal_task || ls.task, until: ls.until, oracle: oracleCmd, startedAt, sessionId, holds, maxHolds: 3 },
20264
- {
20265
- projectRoot,
20266
- runOracle: async (cmd) => {
20267
- if (cmd) {
20268
- return await new Promise((resolve2) => {
20269
- exec$1(cmd, { cwd: directory, timeout: 12e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
20270
- if (err) resolve2({ ok: false, output: (String(stdout || "") + String(stderr || "")).slice(-500) });
20271
- else resolve2({ ok: true, output: "" });
20272
- });
20273
- });
20274
- }
20275
- const pending = backlogOraclePending(projectRoot, sessionId);
20276
- return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
20277
- },
20278
- runEvaluator: (prompt) => runHeadlessEvaluator(prompt),
20279
- log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
20426
+ const runOracle = async (cmd) => {
20427
+ if (cmd) {
20428
+ return await new Promise((resolve2) => {
20429
+ exec$1(cmd, { cwd: directory, timeout: 12e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
20430
+ if (err) resolve2({ ok: false, output: (String(stdout || "") + String(stderr || "")).slice(-500) });
20431
+ else resolve2({ ok: true, output: "" });
20432
+ });
20433
+ });
20280
20434
  }
20281
- );
20435
+ const pending = backlogOraclePending(projectRoot, sessionId);
20436
+ return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
20437
+ };
20438
+ const verifyDeps = {
20439
+ projectRoot,
20440
+ runOracle,
20441
+ runEvaluator: (prompt) => runHeadlessEvaluator(prompt),
20442
+ log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
20443
+ };
20444
+ const verifyCfg = { task: ls.goal_task || ls.task, until: ls.until, oracle: oracleCmd, startedAt, sessionId, holds, maxHolds: 3 };
20445
+ let result = await runLoopVerification(verifyCfg, verifyDeps);
20446
+ if (result.action === "rekick" && result.reason === "oracle reports pending work") {
20447
+ const fresh = await runOracle(oracleCmd);
20448
+ if (fresh.ok) {
20449
+ logger.log(`[Session ${sessionId}] [#0194] oracle went CLEAN during verification \u2014 re-running the gate instead of re-kicking on the stale reading`);
20450
+ result = await runLoopVerification(verifyCfg, { ...verifyDeps, runOracle: async () => fresh });
20451
+ }
20452
+ }
20282
20453
  if (loopCancelledDuringVerify(readLoopState(directory, sessionId))) {
20283
20454
  logger.log(`[Session ${sessionId}] loop was cancelled during verification \u2014 not re-kicking (#0308)`);
20284
20455
  return;
@@ -20958,7 +21129,7 @@ ${parts.join("\n")}`);
20958
21129
  const maxExt = resolveMaxExtensions(process.env.SVAMP_LOOP_MAX_EXTENSIONS);
20959
21130
  const effMaxIters = effectiveSoftCap(ls.max_iterations, ls.extensions, maxExt);
20960
21131
  const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false, kind: void 0 } : checkLoopBudget(ledger, { max_iterations: effMaxIters, ...ls.budget || {} }, now, startedAt);
20961
- const pending = safeBacklogPendingCount(resolveProjectRoot(directory), sessionId);
21132
+ const pending = safeBacklogPendingCount(directory, sessionId, ls.oracle);
20962
21133
  const prog = updateLoopProgress(ls, pending, now);
20963
21134
  ls.progress_history = prog.progress_history;
20964
21135
  ls.auto_resumes = prog.auto_resumes;
@@ -21894,11 +22065,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21894
22065
  });
21895
22066
  },
21896
22067
  onIssue: async (params) => {
21897
- const { issueRpc } = await import('./rpc-TXF-di9u.mjs');
22068
+ const { issueRpc } = await import('./rpc-CLoO6Z5Z.mjs');
21898
22069
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
21899
22070
  },
21900
22071
  onWorkflow: async (params) => {
21901
- const { workflowRpc } = await import('./rpc-FGp88Yxp.mjs');
22072
+ const { workflowRpc } = await import('./rpc-Bz9w7JV9.mjs');
21902
22073
  return workflowRpc(params?.cwd || directory, params || {});
21903
22074
  },
21904
22075
  onRipgrep: async (args, cwd) => {
@@ -22605,11 +22776,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22605
22776
  });
22606
22777
  },
22607
22778
  onIssue: async (params) => {
22608
- const { issueRpc } = await import('./rpc-TXF-di9u.mjs');
22779
+ const { issueRpc } = await import('./rpc-CLoO6Z5Z.mjs');
22609
22780
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22610
22781
  },
22611
22782
  onWorkflow: async (params) => {
22612
- const { workflowRpc } = await import('./rpc-FGp88Yxp.mjs');
22783
+ const { workflowRpc } = await import('./rpc-Bz9w7JV9.mjs');
22613
22784
  return workflowRpc(params?.cwd || directory, params || {});
22614
22785
  },
22615
22786
  onRipgrep: async (args, cwd) => {
@@ -22958,7 +23129,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22958
23129
  const acpMaxExt = resolveMaxExtensions(process.env.SVAMP_LOOP_MAX_EXTENSIONS);
22959
23130
  const acpEffMax = effectiveSoftCap(ls.max_iterations, ls.extensions, acpMaxExt);
22960
23131
  const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false, kind: void 0 } : checkLoopBudget(ledger, { max_iterations: acpEffMax, ...ls.budget || {} }, now, startedAt);
22961
- const acpPending = safeBacklogPendingCount(projectRoot, sessionId);
23132
+ const acpPending = safeBacklogPendingCount(projectRoot, sessionId, ls.oracle);
22962
23133
  const acpProg = updateLoopProgress(ls, acpPending, now);
22963
23134
  ls.progress_history = acpProg.progress_history;
22964
23135
  ls.auto_resumes = acpProg.auto_resumes;
@@ -23660,7 +23831,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23660
23831
  }
23661
23832
  if (persistedSessions.length > 0) {
23662
23833
  try {
23663
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-dbXgsGNK.mjs');
23834
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-BkZfX5pW.mjs');
23664
23835
  await awaitClaudeVersionReady();
23665
23836
  } catch {
23666
23837
  }
@@ -23700,7 +23871,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23700
23871
  isOrphaned: !!isOrphaned,
23701
23872
  loopActive: isLoopActiveForSession(persisted.directory, persisted.sessionId),
23702
23873
  loopArmed: _loopArmed,
23703
- armedHasPendingWork: _loopArmed ? safeBacklogPendingCount(persisted.directory, persisted.sessionId) > 0 : false
23874
+ armedHasPendingWork: _loopArmed ? safeBacklogPendingCount(
23875
+ persisted.directory,
23876
+ persisted.sessionId,
23877
+ readLoopState(persisted.directory, persisted.sessionId)?.oracle
23878
+ ) > 0 : false
23704
23879
  });
23705
23880
  if (_loopArmed) {
23706
23881
  logger.log(`[loop] Restored DORMANT loop for ${persisted.sessionId} \u2192 ${_restoreAction === "loop-resume" ? "re-arming (pending work found)" : "staying dormant/watching (backlog empty)"}`);
@@ -23905,7 +24080,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23905
24080
  const PING_TIMEOUT_MS = 15e3;
23906
24081
  const POST_RECONNECT_GRACE_MS = 2e4;
23907
24082
  const RECONNECT_JITTER_MS = 2500;
23908
- const { WorkflowScheduler } = await import('./scheduler-b-ysyKIj.mjs');
24083
+ const { WorkflowScheduler } = await import('./scheduler-Bs0nAlhs.mjs');
23909
24084
  const workflowProjectRoots = () => {
23910
24085
  const dirs = /* @__PURE__ */ new Set();
23911
24086
  for (const s of pidToTrackedSession.values()) {
@@ -23945,7 +24120,6 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23945
24120
  runOAuthAccountRefresh();
23946
24121
  const backendAccountRefreshInterval = setInterval(runOAuthAccountRefresh, 5 * 6e4);
23947
24122
  let heartbeatRunning = false;
23948
- let lastReconnectAt = 0;
23949
24123
  let heartbeatCycle = 0;
23950
24124
  const heartbeatInterval = setInterval(async () => {
23951
24125
  if (heartbeatRunning) return;
@@ -23981,10 +24155,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23981
24155
  }
23982
24156
  }
23983
24157
  }
24158
+ const inGrace = lastReconnectAt > 0 && Date.now() - lastReconnectAt < POST_RECONNECT_GRACE_MS;
23984
24159
  const INBOUND_SILENCE_THRESHOLD_MS = 12e4;
23985
24160
  const inboundSilenceMs = Date.now() - machineService.getLastInboundRpcAt();
23986
24161
  const hasActiveSessions = pidToTrackedSession.size > 0;
23987
- if (hasActiveSessions && inboundSilenceMs > INBOUND_SILENCE_THRESHOLD_MS && consecutiveHeartbeatFailures === 0) {
24162
+ if (shouldRunZombieProbe({ hasActiveSessions, inboundSilenceMs, consecutiveHeartbeatFailures, inGrace, thresholdMs: INBOUND_SILENCE_THRESHOLD_MS })) {
23988
24163
  logger.log(`No inbound RPC for ${Math.round(inboundSilenceMs / 1e3)}s with ${pidToTrackedSession.size} active session(s) \u2014 zombie probe`);
23989
24164
  try {
23990
24165
  const machineServiceId = `${server.config.client_id}:default`;
@@ -23993,11 +24168,10 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23993
24168
  new Promise((_, reject) => setTimeout(() => reject(new Error("Zombie probe timed out")), PING_TIMEOUT_MS))
23994
24169
  ]);
23995
24170
  } catch (err) {
23996
- logger.log(`Zombie detection probe failed: ${err.message} \u2014 forcing reconnection`);
23997
- consecutiveHeartbeatFailures = 2;
24171
+ logger.log(`Zombie detection probe failed: ${err.message} \u2014 raising suspicion (two-strike)`);
24172
+ consecutiveHeartbeatFailures = nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures);
23998
24173
  }
23999
24174
  }
24000
- const inGrace = lastReconnectAt > 0 && Date.now() - lastReconnectAt < POST_RECONNECT_GRACE_MS;
24001
24175
  if (!inGrace) {
24002
24176
  try {
24003
24177
  const pingStart = Date.now();
@@ -24022,7 +24196,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24022
24196
  } else if (consecutiveHeartbeatFailures % 6 === 0) {
24023
24197
  logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures} failures, retrying indefinitely)`);
24024
24198
  }
24025
- if (consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0) {
24199
+ if (shouldForceReconnect(consecutiveHeartbeatFailures)) {
24026
24200
  const jitterMs = Math.floor(Math.random() * RECONNECT_JITTER_MS);
24027
24201
  if (jitterMs > 0) await new Promise((r) => setTimeout(r, jitterMs));
24028
24202
  const conn = server.rpc?._connection;
@@ -24539,4 +24713,4 @@ var run = /*#__PURE__*/Object.freeze({
24539
24713
  writeStopMarker: writeStopMarker
24540
24714
  });
24541
24715
 
24542
- 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, clearStopMarker as a1, stopMarkerExists as a2, formatHandle as a3, normalizeAllowedUser as a4, loadSecurityContextConfig as a5, resolveSecurityContext as a6, buildSecurityContextFromFlags as a7, mergeSecurityContexts as a8, buildSessionShareUrl as a9, kimiSetup as aA, api as aB, run as aC, computeOutboundHop as aa, registerAwaitingReply as ab, buildMachineShareUrl as ac, parseHandle as ad, handleMatchesMetadata as ae, PINNED_CODEX_VERSION 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 };
24716
+ 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 };