svamp-cli 0.2.319 → 0.2.320

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-JGAVkD1F.mjs} +1 -1
  2. package/dist/{agentCommands-BJeGwkeX.mjs → agentCommands-CoCcX8K1.mjs} +32 -6
  3. package/dist/{auth-0NUGwOc0.mjs → auth-CFGN1ArQ.mjs} +1 -1
  4. package/dist/{cli-CWElpiIq.mjs → cli-C0tO-Dnx.mjs} +89 -85
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-CLgdrTSU.mjs → commands-6KY9uV6s.mjs} +5 -3
  7. package/dist/{commands-CJWgDDl5.mjs → commands-BHcPDQCC.mjs} +5 -3
  8. package/dist/{commands-DubEbPAY.mjs → commands-BUZUle6g.mjs} +4 -2
  9. package/dist/{commands-C0lwV0U3.mjs → commands-BwkoaGfZ.mjs} +5 -3
  10. package/dist/{commands-DMSkWb0h.mjs → commands-CeFjRNKk.mjs} +34 -9
  11. package/dist/{commands-BY1lmXaM.mjs → commands-CubsoWG-.mjs} +1 -1
  12. package/dist/{commands-BsaTwKnd.mjs → commands-DPi7PGxP.mjs} +3 -3
  13. package/dist/{commands-AdVHZXK6.mjs → commands-XN2UtO-W.mjs} +1 -1
  14. package/dist/{fleet-OKnU0cYK.mjs → fleet-BKrbG7G-.mjs} +2 -2
  15. package/dist/{headlessCli-CCO0IFYl.mjs → headlessCli-nywi2aop.mjs} +2 -2
  16. package/dist/index.mjs +1 -1
  17. package/dist/{notifyCommands-4GUy4Mcl.mjs → notifyCommands-BFr80nfh.mjs} +1 -1
  18. package/dist/package-D4zdkORw.mjs +64 -0
  19. package/dist/{pinnedClaudeCode-dbXgsGNK.mjs → pinnedClaudeCode-BkZfX5pW.mjs} +1 -1
  20. package/dist/{rpc-TXF-di9u.mjs → rpc-DVtlZahE.mjs} +1 -1
  21. package/dist/{rpc-FGp88Yxp.mjs → rpc-rpH-yXR7.mjs} +1 -1
  22. package/dist/{run-7TVWXnOM.mjs → run-C22c9FOe.mjs} +1 -1
  23. package/dist/{run-09AkgeGM.mjs → run-CvwPIrfo.mjs} +241 -90
  24. package/dist/{scheduler-b-ysyKIj.mjs → scheduler-CYyGe6kn.mjs} +1 -1
  25. package/dist/{serveCommands-D3W7w0G6.mjs → serveCommands-DRMwdDXS.mjs} +33 -8
  26. package/dist/{sideband-C1db5sMF.mjs → sideband-D1dsa-t0.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);
@@ -4264,6 +4315,10 @@ Connection: close\r
4264
4315
  async startMountTunnel(mountName) {
4265
4316
  if (this.mountTunnels.has(mountName)) return;
4266
4317
  if (!this.port) throw new Error("Auth proxy not running \u2014 call ensureRunning() first");
4318
+ if (process.env.SVAMP_SERVE_NO_TUNNEL === "1") {
4319
+ this.log(`Mount '${mountName}': tunnel skipped (SVAMP_SERVE_NO_TUNNEL=1) \u2014 local only at http://127.0.0.1:${this.port}/${mountName}/`);
4320
+ return;
4321
+ }
4267
4322
  const subdomainSafe = mountName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
4268
4323
  const tunnelName = `static-${subdomainSafe}`;
4269
4324
  const mount = this.mounts.get(mountName);
@@ -4319,6 +4374,7 @@ var serveManager = /*#__PURE__*/Object.freeze({
4319
4374
  ServeManager: ServeManager,
4320
4375
  assertNonAdminMountDirSafe: assertNonAdminMountDirSafe,
4321
4376
  buildLinkSubdomain: buildLinkSubdomain,
4377
+ resolveMountOwnerEmail: resolveMountOwnerEmail,
4322
4378
  sanitizeMountForRole: sanitizeMountForRole
4323
4379
  });
4324
4380
 
@@ -4469,6 +4525,10 @@ function setClaudeAuthCustom(baseUrl, apiKey) {
4469
4525
  ANTHROPIC_API_KEY: apiKey
4470
4526
  });
4471
4527
  }
4528
+ function applyPromptCacheTtl(spawnEnv) {
4529
+ const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
4530
+ if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
4531
+ }
4472
4532
  function applyClaudeProxyEnv(spawnEnv) {
4473
4533
  const mode = currentMode();
4474
4534
  if (mode === "hypha") {
@@ -4486,8 +4546,7 @@ function applyClaudeProxyEnv(spawnEnv) {
4486
4546
  }
4487
4547
  spawnEnv.ANTHROPIC_BASE_URL = proxyUrl;
4488
4548
  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";
4549
+ applyPromptCacheTtl(spawnEnv);
4491
4550
  return `hypha proxy (${proxyUrl}, 1h cache TTL)`;
4492
4551
  }
4493
4552
  if (mode === "custom") {
@@ -4500,7 +4559,8 @@ function applyClaudeProxyEnv(spawnEnv) {
4500
4559
  }
4501
4560
  spawnEnv.ANTHROPIC_BASE_URL = url;
4502
4561
  spawnEnv.ANTHROPIC_API_KEY = key;
4503
- return `custom proxy (${url})`;
4562
+ applyPromptCacheTtl(spawnEnv);
4563
+ return `custom proxy (${url}, 1h cache TTL)`;
4504
4564
  }
4505
4565
  delete spawnEnv.ANTHROPIC_BASE_URL;
4506
4566
  delete spawnEnv.ANTHROPIC_API_KEY;
@@ -6360,6 +6420,13 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6360
6420
  } catch {
6361
6421
  }
6362
6422
  const serveCallerTrusted = (context) => !context?.user || isSameOwnerWorkspace(context) || roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "admin");
6423
+ const resolveMountOwnerEmailFor = (supplied, context) => resolveMountOwnerEmail(
6424
+ supplied,
6425
+ context?.user?.email,
6426
+ currentMetadata.sharing?.owner,
6427
+ serveCallerTrusted(context),
6428
+ process.env.SVAMP_OWNER_EMAIL
6429
+ );
6363
6430
  let lastInboundRpcAt = Date.now();
6364
6431
  const trackInbound = () => {
6365
6432
  lastInboundRpcAt = Date.now();
@@ -7526,7 +7593,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7526
7593
  const { homedir } = await import('os');
7527
7594
  assertNonAdminMountDirSafe(params.directory, homedir());
7528
7595
  }
7529
- const ownerEmail2 = params.ownerEmail || context?.user?.email || currentMetadata.sharing?.owner || process.env.SVAMP_OWNER_EMAIL || void 0;
7596
+ if (params.sessionId && !serveCallerTrusted(context)) {
7597
+ await authorizeSessionAccess(params.sessionId, "admin", context);
7598
+ }
7599
+ const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7530
7600
  const access = params.access || "owner";
7531
7601
  return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
7532
7602
  },
@@ -7543,6 +7613,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7543
7613
  const { homedir } = await import('os');
7544
7614
  assertNonAdminMountDirSafe(params.directory, homedir());
7545
7615
  }
7616
+ if (params.sessionId && !serveCallerTrusted(context)) {
7617
+ await authorizeSessionAccess(params.sessionId, "admin", context);
7618
+ }
7546
7619
  const existingMount = sm.getMount(params.name);
7547
7620
  if (existingMount && !serveCallerTrusted(context)) {
7548
7621
  const callerEmail = (context?.user?.email || "").toLowerCase();
@@ -7551,7 +7624,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7551
7624
  throw new Error(`Not authorized to replace mount '${params.name}' (owned by another user)`);
7552
7625
  }
7553
7626
  }
7554
- const ownerEmail2 = params.ownerEmail || context?.user?.email || currentMetadata.sharing?.owner || process.env.SVAMP_OWNER_EMAIL || void 0;
7627
+ const ownerEmail2 = resolveMountOwnerEmailFor(params.ownerEmail, context);
7555
7628
  const access = params.access ?? "owner";
7556
7629
  return sm.applyMount({
7557
7630
  name: params.name,
@@ -7921,7 +7994,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
7921
7994
  }
7922
7995
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
7923
7996
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
7924
- const { toolsForRole } = await import('./sideband-C1db5sMF.mjs');
7997
+ const { toolsForRole } = await import('./sideband-D1dsa-t0.mjs');
7925
7998
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
7926
7999
  return fmt(r2);
7927
8000
  }
@@ -8026,7 +8099,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8026
8099
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
8027
8100
  }
8028
8101
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
8029
- const { queryCore } = await import('./commands-BY1lmXaM.mjs');
8102
+ const { queryCore } = await import('./commands-CubsoWG-.mjs');
8030
8103
  const timeout = c.reply?.timeout_sec || 120;
8031
8104
  let result;
8032
8105
  try {
@@ -8238,6 +8311,9 @@ ${d?.error || "not found"}`;
8238
8311
  });
8239
8312
  },
8240
8313
  getLastInboundRpcAt: () => lastInboundRpcAt,
8314
+ resetInboundClock: () => {
8315
+ lastInboundRpcAt = Date.now();
8316
+ },
8241
8317
  getCurrentMetadata: () => currentMetadata,
8242
8318
  setHandlers: (newHandlers) => {
8243
8319
  handlers = newHandlers;
@@ -16370,8 +16446,12 @@ function isVisibleTo(issue, session) {
16370
16446
  if (!issue.owner && !issue.session) return true;
16371
16447
  return false;
16372
16448
  }
16449
+ function normalizeIssueRef(id) {
16450
+ const s = String(id ?? "").trim().replace(/^#+/, "");
16451
+ return /^\d+$/.test(s) ? s.padStart(4, "0") : s;
16452
+ }
16373
16453
  function getIssue(projectRoot, id) {
16374
- const padded = /^\d+$/.test(id) ? id.padStart(4, "0") : id;
16454
+ const padded = normalizeIssueRef(id);
16375
16455
  let best = null;
16376
16456
  for (const p of [issuePath(projectRoot, padded), issuePath(projectRoot, padded, true)]) {
16377
16457
  if (!existsSync(p)) continue;
@@ -16383,7 +16463,14 @@ function getIssue(projectRoot, id) {
16383
16463
  } catch {
16384
16464
  }
16385
16465
  }
16386
- return best ? best.issue : null;
16466
+ if (best) return best.issue;
16467
+ try {
16468
+ for (const issue of listIssues(projectRoot, { includeArchived: true })) {
16469
+ if (normalizeIssueRef(issue.id) === padded) return issue;
16470
+ }
16471
+ } catch {
16472
+ }
16473
+ return null;
16387
16474
  }
16388
16475
  function atomicWrite(path, content) {
16389
16476
  mkdirSync$1(dirname(path), { recursive: true });
@@ -16432,8 +16519,7 @@ function addIssue(projectRoot, fields) {
16432
16519
  throw new Error("addIssue: could not allocate a free issue id");
16433
16520
  }
16434
16521
  function issueLockPath(projectRoot, id) {
16435
- const padded = /^\d+$/.test(id) ? id.padStart(4, "0") : id;
16436
- return join$1(issuesDir(projectRoot), `${padded}.lock`);
16522
+ return join$1(issuesDir(projectRoot), `${normalizeIssueRef(id)}.lock`);
16437
16523
  }
16438
16524
  function withIssueLock(projectRoot, id, fn) {
16439
16525
  try {
@@ -16446,16 +16532,20 @@ function _updateIssueUnlocked(projectRoot, id, patch) {
16446
16532
  const cur = getIssue(projectRoot, id);
16447
16533
  if (!cur) return null;
16448
16534
  const wasArchived = cur.status === "archived";
16449
- const next = { ...cur, ...patch, id: cur.id };
16535
+ const canonicalId = normalizeIssueRef(cur.id);
16536
+ const next = { ...cur, ...patch, id: canonicalId };
16450
16537
  if (patch.status) next.status = normalizeStatus(patch.status);
16451
16538
  const nowArchived = next.status === "archived";
16452
16539
  if (nowArchived && !next.closed) next.closed = (/* @__PURE__ */ new Date()).toISOString();
16453
16540
  if (!nowArchived) next.closed = null;
16454
16541
  atomicWrite(issuePath(projectRoot, next.id, nowArchived), serializeIssue(next));
16455
- if (wasArchived !== nowArchived) {
16456
- const oldPath = issuePath(projectRoot, cur.id, wasArchived);
16542
+ for (const stale of [
16543
+ issuePath(projectRoot, cur.id, wasArchived),
16544
+ issuePath(projectRoot, cur.id, nowArchived)
16545
+ ]) {
16546
+ if (stale === issuePath(projectRoot, next.id, nowArchived)) continue;
16457
16547
  try {
16458
- if (existsSync(oldPath)) unlinkSync$1(oldPath);
16548
+ if (existsSync(stale)) unlinkSync$1(stale);
16459
16549
  } catch {
16460
16550
  }
16461
16551
  }
@@ -17439,6 +17529,19 @@ function parseEvaluatorVerdict(text) {
17439
17529
  }
17440
17530
  return null;
17441
17531
  }
17532
+ function buildStaleSafeOracleGuidance(oracleOutput, now = /* @__PURE__ */ new Date()) {
17533
+ const reading = (oracleOutput || "").trim().slice(0, 500);
17534
+ return [
17535
+ `The loop oracle reported unfinished work as of ${now.toISOString()}:`,
17536
+ reading || "(no detail)",
17537
+ "",
17538
+ "That is a POINT-IN-TIME reading, not a fact about right now \u2014 the backlog may have changed",
17539
+ "since (an item can be closed or paused out-of-band while this message was queued). RE-RUN",
17540
+ "the oracle yourself and treat ITS CURRENT OUTPUT as the truth. If it is now empty, the goal",
17541
+ "is complete: say so rather than chasing the items listed above. Otherwise keep working until",
17542
+ "it is genuinely empty."
17543
+ ].join("\n");
17544
+ }
17442
17545
  async function runLoopVerification(cfg, deps) {
17443
17546
  const log = deps.log || (() => {
17444
17547
  });
@@ -17453,9 +17556,11 @@ async function runLoopVerification(cfg, deps) {
17453
17556
  return { action: "gave_up", reason: `oracle still reports pending work after ${holds} verification hold(s): ${oracleOutput.trim().slice(0, 200)}` };
17454
17557
  }
17455
17558
  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.` };
17559
+ return {
17560
+ action: "rekick",
17561
+ reason: "oracle reports pending work",
17562
+ guidance: buildStaleSafeOracleGuidance(oracleOutput)
17563
+ };
17459
17564
  }
17460
17565
  } catch (e) {
17461
17566
  log(`[loopVerify] oracle runner error (continuing to evaluator): ${e?.message || e}`);
@@ -17684,6 +17789,16 @@ function compactUnsupportedWarning(version) {
17684
17789
  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
17790
  }
17686
17791
 
17792
+ function shouldRunZombieProbe(args) {
17793
+ return args.hasActiveSessions && !args.inGrace && args.consecutiveHeartbeatFailures === 0 && args.inboundSilenceMs > args.thresholdMs;
17794
+ }
17795
+ function nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures) {
17796
+ return consecutiveHeartbeatFailures + 1;
17797
+ }
17798
+ function shouldForceReconnect(consecutiveHeartbeatFailures) {
17799
+ return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
17800
+ }
17801
+
17687
17802
  const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
17688
17803
  function generateHookSettings(portOrOptions = {}) {
17689
17804
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
@@ -18555,9 +18670,11 @@ function atomicWriteLoopState(path, obj) {
18555
18670
  writeFileSync(tmp, JSON.stringify(obj, null, 2));
18556
18671
  renameSync$1(tmp, path);
18557
18672
  }
18558
- function safeBacklogPendingCount(projectRoot, sessionId) {
18673
+ function safeBacklogPendingCount(directory, selfSessionId, oracle) {
18559
18674
  try {
18560
- return backlogOraclePending(projectRoot, sessionId).length;
18675
+ const root = resolveProjectRoot(directory);
18676
+ const scope = oracleScopeSessionId(typeof oracle === "string" ? oracle : void 0, selfSessionId);
18677
+ return backlogOraclePending(root, scope).length;
18561
18678
  } catch {
18562
18679
  return 0;
18563
18680
  }
@@ -19510,7 +19627,7 @@ async function startDaemon(options) {
19510
19627
  try {
19511
19628
  const dir = loadSessionIndex()[sessionId]?.directory;
19512
19629
  if (!dir) return;
19513
- const { reconcileServiceLinks } = await import('./agentCommands-BJeGwkeX.mjs');
19630
+ const { reconcileServiceLinks } = await import('./agentCommands-CoCcX8K1.mjs');
19514
19631
  const configPath = getSvampConfigPath(dir, sessionId);
19515
19632
  const config = readSvampConfig(configPath);
19516
19633
  const entries = Array.from(urls.entries());
@@ -19532,7 +19649,7 @@ async function startDaemon(options) {
19532
19649
  try {
19533
19650
  const dir = loadSessionIndex()[sessionId]?.directory;
19534
19651
  if (!dir) return;
19535
- const { reconcileServiceLinks } = await import('./agentCommands-BJeGwkeX.mjs');
19652
+ const { reconcileServiceLinks } = await import('./agentCommands-CoCcX8K1.mjs');
19536
19653
  const configPath = getSvampConfigPath(dir, sessionId);
19537
19654
  const config = readSvampConfig(configPath);
19538
19655
  const incoming = [{
@@ -19548,6 +19665,22 @@ async function startDaemon(options) {
19548
19665
  logger.log(`[serve] Link reconcile failed for mount ${mountName}: ${err?.message || err}`);
19549
19666
  }
19550
19667
  }
19668
+ async function dropMountSessionLink(sessionId, mountName) {
19669
+ if (!sessionId) return;
19670
+ try {
19671
+ const dir = loadSessionIndex()[sessionId]?.directory;
19672
+ if (!dir) return;
19673
+ const { dropServiceLinks } = await import('./agentCommands-CoCcX8K1.mjs');
19674
+ const configPath = getSvampConfigPath(dir, sessionId);
19675
+ const config = readSvampConfig(configPath);
19676
+ if (dropServiceLinks(config, "serve", mountName)) {
19677
+ writeSvampConfig(configPath, config);
19678
+ logger.log(`[serve] Dropped Launch Pad link for mount ${mountName} \u2190 session ${sessionId}`);
19679
+ }
19680
+ } catch (err) {
19681
+ logger.log(`[serve] Link drop failed for mount ${mountName}: ${err?.message || err}`);
19682
+ }
19683
+ }
19551
19684
  async function createExposedTunnel(spec) {
19552
19685
  const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
19553
19686
  const tunnel = new FrpcTunnel({
@@ -19572,12 +19705,15 @@ async function startDaemon(options) {
19572
19705
  const tunnelRecreateInFlight = /* @__PURE__ */ new Set();
19573
19706
  const { ServeManager } = await Promise.resolve().then(function () { return serveManager; });
19574
19707
  const serveManager$1 = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
19708
+ serveManager$1.setMountUnboundHook((sessionId, mountName) => {
19709
+ void dropMountSessionLink(sessionId, mountName);
19710
+ });
19575
19711
  ensureAutoInstalledSkills(logger).catch(() => {
19576
19712
  });
19577
19713
  ensureAutoInstalledCommands(logger);
19578
19714
  (async () => {
19579
19715
  try {
19580
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-dbXgsGNK.mjs');
19716
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-BkZfX5pW.mjs');
19581
19717
  beginClaudeVersionReconcile((msg) => logger.log(msg));
19582
19718
  } catch (e) {
19583
19719
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -19690,10 +19826,14 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
19690
19826
  }
19691
19827
  };
19692
19828
  server.on("services_registered", () => {
19829
+ lastReconnectAt = Date.now();
19830
+ try {
19831
+ machineService.resetInboundClock?.();
19832
+ } catch {
19833
+ }
19693
19834
  if (consecutiveHeartbeatFailures > 0) {
19694
19835
  logger.log(`Hypha reconnection successful \u2014 services re-registered (resetting ${consecutiveHeartbeatFailures} failures)`);
19695
19836
  consecutiveHeartbeatFailures = 0;
19696
- lastReconnectAt = Date.now();
19697
19837
  }
19698
19838
  const reEmitLiveness = (phase) => {
19699
19839
  try {
@@ -20259,26 +20399,33 @@ ${parts.join("\n")}`);
20259
20399
  const oracleCmd = typeof ls.oracle === "string" && ls.oracle.trim() ? ls.oracle.trim() : void 0;
20260
20400
  try {
20261
20401
  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}`)
20402
+ const runOracle = async (cmd) => {
20403
+ if (cmd) {
20404
+ return await new Promise((resolve2) => {
20405
+ exec$1(cmd, { cwd: directory, timeout: 12e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
20406
+ if (err) resolve2({ ok: false, output: (String(stdout || "") + String(stderr || "")).slice(-500) });
20407
+ else resolve2({ ok: true, output: "" });
20408
+ });
20409
+ });
20280
20410
  }
20281
- );
20411
+ const pending = backlogOraclePending(projectRoot, sessionId);
20412
+ return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
20413
+ };
20414
+ const verifyDeps = {
20415
+ projectRoot,
20416
+ runOracle,
20417
+ runEvaluator: (prompt) => runHeadlessEvaluator(prompt),
20418
+ log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
20419
+ };
20420
+ const verifyCfg = { task: ls.goal_task || ls.task, until: ls.until, oracle: oracleCmd, startedAt, sessionId, holds, maxHolds: 3 };
20421
+ let result = await runLoopVerification(verifyCfg, verifyDeps);
20422
+ if (result.action === "rekick" && result.reason === "oracle reports pending work") {
20423
+ const fresh = await runOracle(oracleCmd);
20424
+ if (fresh.ok) {
20425
+ logger.log(`[Session ${sessionId}] [#0194] oracle went CLEAN during verification \u2014 re-running the gate instead of re-kicking on the stale reading`);
20426
+ result = await runLoopVerification(verifyCfg, { ...verifyDeps, runOracle: async () => fresh });
20427
+ }
20428
+ }
20282
20429
  if (loopCancelledDuringVerify(readLoopState(directory, sessionId))) {
20283
20430
  logger.log(`[Session ${sessionId}] loop was cancelled during verification \u2014 not re-kicking (#0308)`);
20284
20431
  return;
@@ -20958,7 +21105,7 @@ ${parts.join("\n")}`);
20958
21105
  const maxExt = resolveMaxExtensions(process.env.SVAMP_LOOP_MAX_EXTENSIONS);
20959
21106
  const effMaxIters = effectiveSoftCap(ls.max_iterations, ls.extensions, maxExt);
20960
21107
  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);
21108
+ const pending = safeBacklogPendingCount(directory, sessionId, ls.oracle);
20962
21109
  const prog = updateLoopProgress(ls, pending, now);
20963
21110
  ls.progress_history = prog.progress_history;
20964
21111
  ls.auto_resumes = prog.auto_resumes;
@@ -21894,11 +22041,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
21894
22041
  });
21895
22042
  },
21896
22043
  onIssue: async (params) => {
21897
- const { issueRpc } = await import('./rpc-TXF-di9u.mjs');
22044
+ const { issueRpc } = await import('./rpc-DVtlZahE.mjs');
21898
22045
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
21899
22046
  },
21900
22047
  onWorkflow: async (params) => {
21901
- const { workflowRpc } = await import('./rpc-FGp88Yxp.mjs');
22048
+ const { workflowRpc } = await import('./rpc-rpH-yXR7.mjs');
21902
22049
  return workflowRpc(params?.cwd || directory, params || {});
21903
22050
  },
21904
22051
  onRipgrep: async (args, cwd) => {
@@ -22605,11 +22752,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22605
22752
  });
22606
22753
  },
22607
22754
  onIssue: async (params) => {
22608
- const { issueRpc } = await import('./rpc-TXF-di9u.mjs');
22755
+ const { issueRpc } = await import('./rpc-DVtlZahE.mjs');
22609
22756
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22610
22757
  },
22611
22758
  onWorkflow: async (params) => {
22612
- const { workflowRpc } = await import('./rpc-FGp88Yxp.mjs');
22759
+ const { workflowRpc } = await import('./rpc-rpH-yXR7.mjs');
22613
22760
  return workflowRpc(params?.cwd || directory, params || {});
22614
22761
  },
22615
22762
  onRipgrep: async (args, cwd) => {
@@ -22958,7 +23105,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22958
23105
  const acpMaxExt = resolveMaxExtensions(process.env.SVAMP_LOOP_MAX_EXTENSIONS);
22959
23106
  const acpEffMax = effectiveSoftCap(ls.max_iterations, ls.extensions, acpMaxExt);
22960
23107
  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);
23108
+ const acpPending = safeBacklogPendingCount(projectRoot, sessionId, ls.oracle);
22962
23109
  const acpProg = updateLoopProgress(ls, acpPending, now);
22963
23110
  ls.progress_history = acpProg.progress_history;
22964
23111
  ls.auto_resumes = acpProg.auto_resumes;
@@ -23660,7 +23807,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23660
23807
  }
23661
23808
  if (persistedSessions.length > 0) {
23662
23809
  try {
23663
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-dbXgsGNK.mjs');
23810
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-BkZfX5pW.mjs');
23664
23811
  await awaitClaudeVersionReady();
23665
23812
  } catch {
23666
23813
  }
@@ -23700,7 +23847,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23700
23847
  isOrphaned: !!isOrphaned,
23701
23848
  loopActive: isLoopActiveForSession(persisted.directory, persisted.sessionId),
23702
23849
  loopArmed: _loopArmed,
23703
- armedHasPendingWork: _loopArmed ? safeBacklogPendingCount(persisted.directory, persisted.sessionId) > 0 : false
23850
+ armedHasPendingWork: _loopArmed ? safeBacklogPendingCount(
23851
+ persisted.directory,
23852
+ persisted.sessionId,
23853
+ readLoopState(persisted.directory, persisted.sessionId)?.oracle
23854
+ ) > 0 : false
23704
23855
  });
23705
23856
  if (_loopArmed) {
23706
23857
  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 +24056,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23905
24056
  const PING_TIMEOUT_MS = 15e3;
23906
24057
  const POST_RECONNECT_GRACE_MS = 2e4;
23907
24058
  const RECONNECT_JITTER_MS = 2500;
23908
- const { WorkflowScheduler } = await import('./scheduler-b-ysyKIj.mjs');
24059
+ const { WorkflowScheduler } = await import('./scheduler-CYyGe6kn.mjs');
23909
24060
  const workflowProjectRoots = () => {
23910
24061
  const dirs = /* @__PURE__ */ new Set();
23911
24062
  for (const s of pidToTrackedSession.values()) {
@@ -23981,10 +24132,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23981
24132
  }
23982
24133
  }
23983
24134
  }
24135
+ const inGrace = lastReconnectAt > 0 && Date.now() - lastReconnectAt < POST_RECONNECT_GRACE_MS;
23984
24136
  const INBOUND_SILENCE_THRESHOLD_MS = 12e4;
23985
24137
  const inboundSilenceMs = Date.now() - machineService.getLastInboundRpcAt();
23986
24138
  const hasActiveSessions = pidToTrackedSession.size > 0;
23987
- if (hasActiveSessions && inboundSilenceMs > INBOUND_SILENCE_THRESHOLD_MS && consecutiveHeartbeatFailures === 0) {
24139
+ if (shouldRunZombieProbe({ hasActiveSessions, inboundSilenceMs, consecutiveHeartbeatFailures, inGrace, thresholdMs: INBOUND_SILENCE_THRESHOLD_MS })) {
23988
24140
  logger.log(`No inbound RPC for ${Math.round(inboundSilenceMs / 1e3)}s with ${pidToTrackedSession.size} active session(s) \u2014 zombie probe`);
23989
24141
  try {
23990
24142
  const machineServiceId = `${server.config.client_id}:default`;
@@ -23993,11 +24145,10 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23993
24145
  new Promise((_, reject) => setTimeout(() => reject(new Error("Zombie probe timed out")), PING_TIMEOUT_MS))
23994
24146
  ]);
23995
24147
  } catch (err) {
23996
- logger.log(`Zombie detection probe failed: ${err.message} \u2014 forcing reconnection`);
23997
- consecutiveHeartbeatFailures = 2;
24148
+ logger.log(`Zombie detection probe failed: ${err.message} \u2014 raising suspicion (two-strike)`);
24149
+ consecutiveHeartbeatFailures = nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures);
23998
24150
  }
23999
24151
  }
24000
- const inGrace = lastReconnectAt > 0 && Date.now() - lastReconnectAt < POST_RECONNECT_GRACE_MS;
24001
24152
  if (!inGrace) {
24002
24153
  try {
24003
24154
  const pingStart = Date.now();
@@ -24022,7 +24173,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24022
24173
  } else if (consecutiveHeartbeatFailures % 6 === 0) {
24023
24174
  logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures} failures, retrying indefinitely)`);
24024
24175
  }
24025
- if (consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0) {
24176
+ if (shouldForceReconnect(consecutiveHeartbeatFailures)) {
24026
24177
  const jitterMs = Math.floor(Math.random() * RECONNECT_JITTER_MS);
24027
24178
  if (jitterMs > 0) await new Promise((r) => setTimeout(r, jitterMs));
24028
24179
  const conn = server.rpc?._connection;
@@ -24539,4 +24690,4 @@ var run = /*#__PURE__*/Object.freeze({
24539
24690
  writeStopMarker: writeStopMarker
24540
24691
  });
24541
24692
 
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 };
24693
+ 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 };