svamp-cli 0.2.325 → 0.2.327

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 (31) hide show
  1. package/README.md +2 -3
  2. package/bin/skills/crew/SKILL.md +10 -2
  3. package/dist/{adminCommands-CMSvATDd.mjs → adminCommands-BVXEj5XN.mjs} +1 -1
  4. package/dist/{agentCommands-BE3bFu8e.mjs → agentCommands-BiSVzb5D.mjs} +5 -5
  5. package/dist/{auth-BmFYRinf.mjs → auth-D0ji0kh3.mjs} +3 -3
  6. package/dist/{cli-DiCcPggc.mjs → cli-CQlIsRxF.mjs} +79 -79
  7. package/dist/cli.mjs +4 -4
  8. package/dist/{commands-uYZfcxk0.mjs → commands-BkzYDwHO.mjs} +3 -3
  9. package/dist/{commands-C3Bkw-Km.mjs → commands-C4z-QQdO.mjs} +13 -13
  10. package/dist/{commands-Coy5CnDf.mjs → commands-CPZdcLvm.mjs} +3 -3
  11. package/dist/{commands-Z_44kVdm.mjs → commands-CXJmXwQo.mjs} +3 -3
  12. package/dist/{commands-WDZS1Clb.mjs → commands-Cegv66fj.mjs} +4 -4
  13. package/dist/{commands-B6fVaVgM.mjs → commands-D5R0AmXt.mjs} +3 -3
  14. package/dist/{commands-7ExCePUv.mjs → commands-D61SEgo0.mjs} +2 -2
  15. package/dist/{commands-D4R66JMe.mjs → commands-DexO3t-N.mjs} +1 -1
  16. package/dist/{fleet-DJj0ns7e.mjs → fleet-CCVC-NiE.mjs} +3 -3
  17. package/dist/{headlessCli-Bx41HZIx.mjs → headlessCli-CFgDFTPd.mjs} +4 -4
  18. package/dist/{httpServer-1XjB2h3K.mjs → httpServer-Be1Hcddr.mjs} +59 -38
  19. package/dist/index.mjs +3 -3
  20. package/dist/{notifyCommands-BV4ajmhc.mjs → notifyCommands-BfLeyHId.mjs} +1 -1
  21. package/dist/package-TcR2vlFS.mjs +64 -0
  22. package/dist/{pinnedClaudeCode-8SZDu7O-.mjs → pinnedClaudeCode-BO0Qs_kE.mjs} +1 -1
  23. package/dist/{rpc-D_pFjWr1.mjs → rpc-DXY3KqiA.mjs} +3 -3
  24. package/dist/{rpc-D9LFUW93.mjs → rpc-hdw0ZlCj.mjs} +3 -3
  25. package/dist/{run-D7G5CDp7.mjs → run-CewXMfIf.mjs} +1 -1
  26. package/dist/{run-CO4F7ZaJ.mjs → run-DdTcDN3e.mjs} +372 -86
  27. package/dist/{scheduler-bgEMu7PK.mjs → scheduler-3LUL1jFD.mjs} +3 -3
  28. package/dist/{serveCommands-MbZuPWD4.mjs → serveCommands-DUJ4eojk.mjs} +12 -12
  29. package/dist/{sideband-CadJ3gg1.mjs → sideband-BRHu4M46.mjs} +3 -3
  30. package/package.json +2 -2
  31. package/dist/package-C3Usc86E.mjs +0 -64
@@ -8,15 +8,15 @@ import { fileURLToPath } from 'url';
8
8
  import { execFile, execSync, spawn, exec as exec$1 } from 'child_process';
9
9
  import * as crypto from 'crypto';
10
10
  import { randomUUID, createHash } from 'crypto';
11
- import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
12
11
  import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync as unlinkSync$1, writeSync } from 'node:fs';
12
+ import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
13
+ import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
13
14
  import { exec, spawn as spawn$1, execSync as execSync$1, execFile as execFile$1, execFileSync } from 'node:child_process';
14
15
  import { promisify } from 'util';
15
16
  import * as http from 'http';
16
17
  import * as net from 'net';
17
18
  import { createServer } from 'net';
18
19
  import os, { homedir as homedir$1, platform as platform$1 } from 'node:os';
19
- import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
20
20
  import { EventEmitter } from 'node:events';
21
21
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
22
22
  import { promisify as promisify$1 } from 'node:util';
@@ -3001,6 +3001,13 @@ function assertNonAdminMountDirSafe(directory, homeDir, resolveReal = defaultRes
3001
3001
  }
3002
3002
  }
3003
3003
  }
3004
+ function hiddenSegmentOf(relPath) {
3005
+ for (const seg of relPath.split(/[\\/]+/)) {
3006
+ if (!seg || seg === ".") continue;
3007
+ if (seg.startsWith(".")) return seg;
3008
+ }
3009
+ return null;
3010
+ }
3004
3011
  function defaultResolveReal(p) {
3005
3012
  try {
3006
3013
  return fs.realpathSync(p);
@@ -3160,6 +3167,9 @@ class ServeManager {
3160
3167
  hyphaServerUrl;
3161
3168
  /** #126: daemon-provided lookup of sessionId → { cwd, sharing, ownerEmail } for the files gateway. */
3162
3169
  sessionResolver = null;
3170
+ /** Channel-capability authorizer: a `fileAccess`-enabled channel's caller key grants
3171
+ * whole-project file management for its session. Checked BEFORE email auth. */
3172
+ capabilityAuthorizer = null;
3163
3173
  /**
3164
3174
  * #0944/#0945: daemon-provided "this mount no longer backs that session's Launch Pad entry"
3165
3175
  * hook. Fired with the OWNING session id, which is the only correct target — the CLI-side
@@ -3189,6 +3199,43 @@ class ServeManager {
3189
3199
  setSessionResolver(fn) {
3190
3200
  this.sessionResolver = fn;
3191
3201
  }
3202
+ /**
3203
+ * Register the channel-capability authorizer used by the session-files gateway to admit
3204
+ * a `fileAccess`-enabled channel's caller key (ck_… bearer). Call once at daemon start.
3205
+ */
3206
+ setCapabilityAuthorizer(fn) {
3207
+ this.capabilityAuthorizer = fn;
3208
+ }
3209
+ /**
3210
+ * Extract a channel capability token from the request, if one is present. A channel caller
3211
+ * key is `ck_`-prefixed; we ONLY return a token that carries that prefix so a Hypha JWT
3212
+ * bearer (used by the email-identity path) never collides with the capability authorizer.
3213
+ * Accepted carriers: `Authorization: Bearer ck_…` header (any verb), or `?key=ck_…` query
3214
+ * param (READ verbs only). Returns null when no capability token is presented.
3215
+ * NEVER log the returned value.
3216
+ *
3217
+ * #1047: the query carrier is restricted to GET/HEAD. A query string is a poor place for a
3218
+ * credential — it lands in browser history, in the `Referer` of any HTML the gateway serves,
3219
+ * and in every tunnel/intermediary access log on the path (and the gateway is reached over an
3220
+ * frps tunnel whose logs the user does not control). This is the same exposure class #1006
3221
+ * closed for the Hypha JWT in download URLs. It is kept for reads because a browser-embeddable
3222
+ * URL genuinely needs it (an `<img src>` or a download link cannot set a header), but a leaked
3223
+ * URL must not also confer PUT/DELETE over the whole project. The generated skill already
3224
+ * documents only the Authorization form (see `fileSection` in channel/store.ts), so this
3225
+ * narrows an unused carrier rather than breaking the documented path.
3226
+ */
3227
+ extractCapabilityToken(req, url) {
3228
+ const auth = req.headers["authorization"];
3229
+ if (typeof auth === "string") {
3230
+ const m = /^Bearer\s+(\S+)$/i.exec(auth.trim());
3231
+ if (m && m[1].startsWith("ck_")) return m[1];
3232
+ }
3233
+ const method = (req.method || "GET").toUpperCase();
3234
+ if (method !== "GET" && method !== "HEAD") return null;
3235
+ const q = url.searchParams.get("key");
3236
+ if (q && q.startsWith("ck_")) return q;
3237
+ return null;
3238
+ }
3192
3239
  /**
3193
3240
  * #0944/#0945: register the daemon's "drop this session's Launch Pad link for this mount"
3194
3241
  * callback. Fired when a mount stops backing a session's Pad entry — on a real removal, and
@@ -3785,40 +3832,59 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3785
3832
  deny(404, "Unknown session");
3786
3833
  return;
3787
3834
  }
3788
- const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
3789
- if (!userEmail) {
3790
- deny(401, "Authentication required");
3791
- return;
3835
+ let capabilityGranted = false;
3836
+ const capToken = this.extractCapabilityToken(req, url);
3837
+ if (capToken && this.capabilityAuthorizer) {
3838
+ try {
3839
+ capabilityGranted = this.capabilityAuthorizer(sessionId, capToken);
3840
+ } catch {
3841
+ capabilityGranted = false;
3842
+ }
3792
3843
  }
3793
- const ownerEmail = (target.ownerEmail || "").toLowerCase();
3794
- const isOwner = !!ownerEmail && userEmail.toLowerCase() === ownerEmail;
3795
- if (!isOwner) {
3796
- const principal = { user: { email: userEmail } };
3797
- let granted = false;
3798
- const sharing = target.sharing;
3799
- if (sharing && sharing.enabled) {
3800
- try {
3801
- authorizeRequest(principal, sharing, "admin");
3802
- granted = true;
3803
- } catch {
3804
- }
3844
+ if (!capabilityGranted) {
3845
+ const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
3846
+ if (!userEmail) {
3847
+ deny(401, "Authentication required");
3848
+ return;
3805
3849
  }
3806
- const machineSharing = target.machineSharing;
3807
- if (!granted && machineSharing && machineSharing.enabled) {
3808
- try {
3809
- authorizeRequest(principal, machineSharing, "admin");
3810
- granted = true;
3811
- } catch {
3850
+ const ownerEmail = (target.ownerEmail || "").toLowerCase();
3851
+ const isOwner = !!ownerEmail && userEmail.toLowerCase() === ownerEmail;
3852
+ if (!isOwner) {
3853
+ const principal = { user: { email: userEmail } };
3854
+ let granted = false;
3855
+ const sharing = target.sharing;
3856
+ if (sharing && sharing.enabled) {
3857
+ try {
3858
+ authorizeRequest(principal, sharing, "admin");
3859
+ granted = true;
3860
+ } catch {
3861
+ }
3862
+ }
3863
+ const machineSharing = target.machineSharing;
3864
+ if (!granted && machineSharing && machineSharing.enabled) {
3865
+ try {
3866
+ authorizeRequest(principal, machineSharing, "admin");
3867
+ granted = true;
3868
+ } catch {
3869
+ }
3870
+ }
3871
+ if (!granted) {
3872
+ this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and not a session/machine admin`);
3873
+ deny(403, "Access denied");
3874
+ return;
3812
3875
  }
3813
- }
3814
- if (!granted) {
3815
- this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and not a session/machine admin`);
3816
- deny(403, "Access denied");
3817
- return;
3818
3876
  }
3819
3877
  }
3820
3878
  const rootDir = target.cwd;
3821
3879
  const normRel = this.toSessionRelative(rootDir, m[2] || "");
3880
+ if (capabilityGranted) {
3881
+ const badSeg = hiddenSegmentOf(normRel);
3882
+ if (badSeg) {
3883
+ this.log(`Files-gateway DENY '${sessionId}': channel capability may not reach a hidden path (${badSeg})`);
3884
+ deny(403, `Forbidden: channel file access may not reach hidden paths (${badSeg})`);
3885
+ return;
3886
+ }
3887
+ }
3822
3888
  if (method === "GET" || method === "HEAD") {
3823
3889
  if (url.searchParams.get("list") === "1") {
3824
3890
  const lex = this.resolveContainedPath(rootDir, normRel);
@@ -3840,6 +3906,7 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3840
3906
  }
3841
3907
  return { name: d.name, isDir, size, mtime };
3842
3908
  });
3909
+ if (capabilityGranted) entries = entries.filter((e) => !e.name.startsWith("."));
3843
3910
  } catch (err) {
3844
3911
  deny(err?.code === "ENOENT" ? 404 : 500, `Listing failed: ${err?.message || err}`);
3845
3912
  return;
@@ -4420,6 +4487,7 @@ var serveManager = /*#__PURE__*/Object.freeze({
4420
4487
  ServeManager: ServeManager,
4421
4488
  assertNonAdminMountDirSafe: assertNonAdminMountDirSafe,
4422
4489
  buildLinkSubdomain: buildLinkSubdomain,
4490
+ hiddenSegmentOf: hiddenSegmentOf,
4423
4491
  resolveMountOwnerEmail: resolveMountOwnerEmail,
4424
4492
  sanitizeMountForRole: sanitizeMountForRole
4425
4493
  });
@@ -4598,8 +4666,13 @@ function applyPromptCacheTtl(spawnEnv) {
4598
4666
  const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
4599
4667
  if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
4600
4668
  }
4669
+ function applyTodoTools(spawnEnv) {
4670
+ const override = spawnEnv.CLAUDE_CODE_ENABLE_TODO_TOOLS ?? process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS;
4671
+ if (override === void 0) spawnEnv.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1";
4672
+ }
4601
4673
  function applyClaudeProxyEnv(spawnEnv) {
4602
4674
  const mode = currentMode();
4675
+ applyTodoTools(spawnEnv);
4603
4676
  if (mode === "hypha") {
4604
4677
  const proxyUrl = resolveHyphaProxyUrl();
4605
4678
  if (!proxyUrl) {
@@ -5613,6 +5686,12 @@ function validateChannel(c) {
5613
5686
  if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
5614
5687
  for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
5615
5688
  errs.push(...validateUploadConfig(c));
5689
+ if (c.fileAccess !== void 0) {
5690
+ if (typeof c.fileAccess !== "object" || c.fileAccess === null || Array.isArray(c.fileAccess))
5691
+ errs.push("fileAccess must be an object");
5692
+ else if (c.fileAccess.enabled !== void 0 && typeof c.fileAccess.enabled !== "boolean")
5693
+ errs.push("fileAccess.enabled must be a boolean");
5694
+ }
5616
5695
  return errs;
5617
5696
  }
5618
5697
  function normalizeBind(c) {
@@ -5636,14 +5715,34 @@ function channelAsyncRepliable(c) {
5636
5715
  if ((c.action?.kind ?? "message") !== "message") return false;
5637
5716
  return bindMode(c) !== "stateless";
5638
5717
  }
5718
+ function channelKeyGrantsFileAccess(channels, token) {
5719
+ if (!token || !token.startsWith("ck_")) return false;
5720
+ const presented = createHash$1("sha256").update(token).digest();
5721
+ const eq = (candidate) => {
5722
+ if (!candidate) return false;
5723
+ try {
5724
+ return timingSafeEqual(presented, createHash$1("sha256").update(candidate).digest());
5725
+ } catch {
5726
+ return false;
5727
+ }
5728
+ };
5729
+ for (const ch of channels) {
5730
+ if (ch.enabled === false) continue;
5731
+ if (!ch.fileAccess?.enabled) continue;
5732
+ if (eq(ch.identity?.shared_key)) return true;
5733
+ for (const caller of ch.identity?.callers || []) if (eq(caller.key)) return true;
5734
+ }
5735
+ return false;
5736
+ }
5737
+ const _channelListCache = /* @__PURE__ */ new Map();
5738
+ const _CHANNEL_CACHE_TTL_MS = 1e3;
5739
+ function invalidateChannelCache(dir) {
5740
+ _channelListCache.delete(dir);
5741
+ }
5639
5742
  class ChannelStore {
5640
5743
  dir;
5641
5744
  constructor(projectDir) {
5642
5745
  this.dir = join$1(projectDir, ".svamp", "channels");
5643
- try {
5644
- mkdirSync$1(this.dir, { recursive: true });
5645
- } catch {
5646
- }
5647
5746
  }
5648
5747
  // #0982: reject a traversing/oversized id at the path boundary itself, so no
5649
5748
  // caller (RPC, HTTP, CLI) can reach outside this.dir regardless of its own checks.
@@ -5664,19 +5763,43 @@ class ChannelStore {
5664
5763
  if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
5665
5764
  mkdirSync$1(this.dir, { recursive: true });
5666
5765
  const tmp = `${this._path(c.id)}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
5667
- writeFileSync$1(tmp, JSON.stringify(c, null, 2));
5766
+ writeFileSync$1(tmp, JSON.stringify(c, null, 2), { mode: 384 });
5668
5767
  renameSync(tmp, this._path(c.id));
5768
+ invalidateChannelCache(this.dir);
5669
5769
  return c;
5670
5770
  }
5771
+ /**
5772
+ * All channels in this project, parsed. Cached per directory and revalidated with ONE statSync
5773
+ * — see the #1049/#1068 note above for why the uncached version was a daemon-wide stall.
5774
+ */
5671
5775
  list() {
5672
- if (!existsSync(this.dir)) return [];
5673
- return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
5674
- try {
5675
- return normalizeBind(JSON.parse(readFileSync(join$1(this.dir, f), "utf8")));
5676
- } catch {
5677
- return null;
5678
- }
5679
- }).filter((c) => !!c);
5776
+ let st;
5777
+ try {
5778
+ const s = statSync(this.dir);
5779
+ st = { mtimeMs: s.mtimeMs, size: s.size };
5780
+ } catch {
5781
+ _channelListCache.delete(this.dir);
5782
+ return [];
5783
+ }
5784
+ const hit = _channelListCache.get(this.dir);
5785
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size && Date.now() - hit.at < _CHANNEL_CACHE_TTL_MS) {
5786
+ return hit.channels;
5787
+ }
5788
+ let channels;
5789
+ try {
5790
+ channels = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
5791
+ try {
5792
+ return normalizeBind(JSON.parse(readFileSync(join$1(this.dir, f), "utf8")));
5793
+ } catch {
5794
+ return null;
5795
+ }
5796
+ }).filter((c) => !!c);
5797
+ } catch {
5798
+ _channelListCache.delete(this.dir);
5799
+ return [];
5800
+ }
5801
+ _channelListCache.set(this.dir, { mtimeMs: st.mtimeMs, size: st.size, at: Date.now(), channels });
5802
+ return channels;
5680
5803
  }
5681
5804
  get(id) {
5682
5805
  try {
@@ -5694,6 +5817,7 @@ class ChannelStore {
5694
5817
  const p = this._path(id);
5695
5818
  if (existsSync(p)) {
5696
5819
  rmSync$1(p);
5820
+ invalidateChannelCache(this.dir);
5697
5821
  return true;
5698
5822
  }
5699
5823
  return false;
@@ -5734,6 +5858,28 @@ class ChannelStore {
5734
5858
  return caller;
5735
5859
  });
5736
5860
  }
5861
+ /**
5862
+ * #1036: revoke ONE caller's key without destroying the channel.
5863
+ *
5864
+ * Before this, the only removal primitive was remove(id) — so withdrawing a single leaked key
5865
+ * meant deleting the whole channel and every other caller with it. That is a bad enough trade
5866
+ * that in practice a leaked key just stayed live, which matters much more now that a caller key
5867
+ * can carry whole-project file access (see channelKeyGrantsFileAccess).
5868
+ *
5869
+ * Same per-channel lock as the sibling mutators (#0679), so a concurrent addCaller cannot
5870
+ * resurrect the revoked entry by writing a stale base. Returns true when a caller was removed.
5871
+ */
5872
+ removeCaller(id, name) {
5873
+ return withFileLock(this._lock(id), () => {
5874
+ const c = this.get(id);
5875
+ if (!c?.identity?.callers) return false;
5876
+ const before = c.identity.callers.length;
5877
+ c.identity.callers = c.identity.callers.filter((x) => x.name !== name);
5878
+ if (c.identity.callers.length === before) return false;
5879
+ this._writeChannel(c);
5880
+ return true;
5881
+ });
5882
+ }
5737
5883
  }
5738
5884
  function routingSession(channel, ctx) {
5739
5885
  const mode = bindMode(channel);
@@ -5846,6 +5992,32 @@ curl -X POST "${uploadUrl}" \\
5846
5992
  -d '{"kwargs": {"channel": "${channel.id}", "filename": "report.pdf", "content_base64": "<base64>", "content_type": "application/pdf", "from": "your-name", "key": "${key}"}}'
5847
5993
  \`\`\``;
5848
5994
  })() : "";
5995
+ const filesBase = ctx?.filesEndpoint;
5996
+ const fileSection = channel.fileAccess?.enabled && filesBase ? `
5997
+
5998
+ ## Managing project files
5999
+ This channel also grants **full file management of the whole project folder** \u2014 list,
6000
+ download, upload, and delete any file in the working directory (this is NOT the sandboxed
6001
+ \`upload\` above). Authenticate every request with your channel key as a Bearer token:
6002
+
6003
+ Base: \`${filesBase}\` \u2014 append a project-relative path (URL-encode as needed).
6004
+
6005
+ - **List a directory:** \`GET ${filesBase}/<dir>/?list=1\` \u2192 JSON of entries
6006
+ - **Download a file:** \`GET ${filesBase}/<path>\`
6007
+ - **Upload / overwrite:** \`PUT ${filesBase}/<path>\` with the raw file bytes as the body
6008
+ - **Delete a file:** \`DELETE ${filesBase}/<path>\`
6009
+
6010
+ \`\`\`bash
6011
+ # list the project root
6012
+ curl -H "Authorization: Bearer ${key}" "${filesBase}/?list=1"
6013
+ # download a file
6014
+ curl -H "Authorization: Bearer ${key}" "${filesBase}/README.md" -o README.md
6015
+ # upload / overwrite a file (raw bytes)
6016
+ curl -X PUT -H "Authorization: Bearer ${key}" --data-binary @local.txt "${filesBase}/notes/local.txt"
6017
+ # delete a file
6018
+ curl -X DELETE -H "Authorization: Bearer ${key}" "${filesBase}/notes/local.txt"
6019
+ \`\`\`
6020
+ > \u26A0\uFE0F Anyone holding this key can read, write, and **delete** any file in this project. Revoke it when you're done.` : "";
5849
6021
  return `---
5850
6022
  name: ${name}
5851
6023
  description: ${desc}
@@ -5878,7 +6050,7 @@ curl -X POST "${sendUrl}" \\
5878
6050
  -d '{"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"${sessionKv}}}'
5879
6051
  \`\`\`
5880
6052
 
5881
- Always-current copy of this skill (all ids filled in): ${skillUrl}${uploadSection}${queueSection}`;
6053
+ Always-current copy of this skill (all ids filled in): ${skillUrl}${uploadSection}${fileSection}${queueSection}`;
5882
6054
  }
5883
6055
 
5884
6056
  function keysMatch(a, b) {
@@ -6347,6 +6519,9 @@ function getParamNames(fn) {
6347
6519
  const terminalSessions = /* @__PURE__ */ new Map();
6348
6520
  let ptyModule = null;
6349
6521
  const TERMINAL_IDLE_MS = 30 * 60 * 1e3;
6522
+ const TERMINAL_CLIENT_ABSENT_MS = 4 * 60 * 60 * 1e3;
6523
+ const TERMINAL_BUFFER_MAX_BYTES = 1024 * 1024;
6524
+ const TERMINAL_MAX_SESSIONS = 64;
6350
6525
  let terminalReaper = null;
6351
6526
  function ensureTerminalReaper() {
6352
6527
  if (terminalReaper) return;
@@ -6354,7 +6529,8 @@ function ensureTerminalReaper() {
6354
6529
  const now = Date.now();
6355
6530
  for (const [id, s] of terminalSessions) {
6356
6531
  if (s.exited) continue;
6357
- if (now - s.lastActivity > TERMINAL_IDLE_MS) {
6532
+ const abandoned = now - (s.lastClientActivity ?? s.lastActivity) > TERMINAL_CLIENT_ABSENT_MS;
6533
+ if (now - s.lastActivity > TERMINAL_IDLE_MS || abandoned) {
6358
6534
  try {
6359
6535
  s.pty.kill();
6360
6536
  } catch {
@@ -7208,8 +7384,29 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7208
7384
  rows,
7209
7385
  createdAt: Date.now(),
7210
7386
  lastActivity: Date.now(),
7387
+ lastClientActivity: Date.now(),
7388
+ outputBytes: 0,
7211
7389
  cwd
7212
7390
  };
7391
+ if (terminalSessions.size >= TERMINAL_MAX_SESSIONS) {
7392
+ let victim = null, oldest = Infinity;
7393
+ for (const [id, s] of terminalSessions) {
7394
+ const seen = s.lastClientActivity ?? s.lastActivity;
7395
+ if (seen < oldest) {
7396
+ oldest = seen;
7397
+ victim = id;
7398
+ }
7399
+ }
7400
+ if (victim) {
7401
+ const v = terminalSessions.get(victim);
7402
+ try {
7403
+ v?.pty.kill();
7404
+ } catch {
7405
+ }
7406
+ terminalSessions.delete(victim);
7407
+ console.log(`[terminal] evicted idlest session ${victim} \u2014 at the ${TERMINAL_MAX_SESSIONS}-PTY ceiling`);
7408
+ }
7409
+ }
7213
7410
  terminalSessions.set(sessionId, session);
7214
7411
  ensureTerminalReaper();
7215
7412
  let outputBatch = "";
@@ -7220,8 +7417,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7220
7417
  const batch = outputBatch;
7221
7418
  outputBatch = "";
7222
7419
  session.outputBuffer.push(batch);
7223
- if (session.outputBuffer.length > 1e3) {
7224
- session.outputBuffer.splice(0, session.outputBuffer.length - 500);
7420
+ session.outputBytes += batch.length;
7421
+ while (session.outputBytes > TERMINAL_BUFFER_MAX_BYTES && session.outputBuffer.length > 1) {
7422
+ session.outputBytes -= (session.outputBuffer.shift() || "").length;
7225
7423
  }
7226
7424
  if (terminalClient) {
7227
7425
  try {
@@ -7270,6 +7468,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7270
7468
  const filtered = filterTerminalResponses(params.data);
7271
7469
  if (filtered) session.pty.write(filtered);
7272
7470
  session.lastActivity = Date.now();
7471
+ session.lastClientActivity = Date.now();
7273
7472
  return { success: true };
7274
7473
  },
7275
7474
  /** Resize a terminal session. */
@@ -7291,7 +7490,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
7291
7490
  const session = terminalSessions.get(params.sessionId);
7292
7491
  if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
7293
7492
  const output = session.outputBuffer.splice(0).join("");
7493
+ session.outputBytes = 0;
7294
7494
  session.lastActivity = Date.now();
7495
+ session.lastClientActivity = Date.now();
7295
7496
  const result = {
7296
7497
  output,
7297
7498
  exited: session.exited
@@ -8101,7 +8302,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8101
8302
  }
8102
8303
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
8103
8304
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
8104
- const { toolsForRole } = await import('./sideband-CadJ3gg1.mjs');
8305
+ const { toolsForRole } = await import('./sideband-BRHu4M46.mjs');
8105
8306
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
8106
8307
  return fmt(r2);
8107
8308
  }
@@ -8206,7 +8407,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
8206
8407
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
8207
8408
  }
8208
8409
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
8209
- const { queryCore } = await import('./commands-D4R66JMe.mjs');
8410
+ const { queryCore } = await import('./commands-DexO3t-N.mjs');
8210
8411
  const timeout = c.reply?.timeout_sec || 120;
8211
8412
  let result;
8212
8413
  try {
@@ -9354,6 +9555,7 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
9354
9555
  }
9355
9556
  }
9356
9557
  const messageCountCache = /* @__PURE__ */ new Map();
9558
+ const MESSAGE_COUNT_CACHE_MAX = 2e3;
9357
9559
  function countMessagesOnDisk(messagesDir, fallbackInMemory) {
9358
9560
  const filePath = join$1(messagesDir, "messages.jsonl");
9359
9561
  if (!existsSync(filePath)) return fallbackInMemory;
@@ -9381,7 +9583,13 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
9381
9583
  for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
9382
9584
  if (data.length > 0 && data[data.length - 1] !== "\n") count++;
9383
9585
  }
9586
+ messageCountCache.delete(filePath);
9384
9587
  messageCountCache.set(filePath, { size: st.size, mtimeMs: st.mtimeMs, count });
9588
+ while (messageCountCache.size > MESSAGE_COUNT_CACHE_MAX) {
9589
+ const oldest = messageCountCache.keys().next();
9590
+ if (oldest.done) break;
9591
+ messageCountCache.delete(oldest.value);
9592
+ }
9385
9593
  return count;
9386
9594
  } catch {
9387
9595
  return fallbackInMemory;
@@ -9674,7 +9882,11 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9674
9882
  key: c.identity?.shared_key,
9675
9883
  // The session this skill is being copied FROM — baked into `dynamic` channel
9676
9884
  // instructions as the routing target so the copied link lands in THIS session.
9677
- session: sessionId
9885
+ session: sessionId,
9886
+ // For a fileAccess-enabled channel, the session-files REST gateway prefix
9887
+ // (`<base>/s/<sessionId>/`) so generateSkillBody can render the file curl block.
9888
+ // Null until the always-on gateway tunnel is up ⇒ the file section is omitted.
9889
+ filesEndpoint: callbacks.onFilesEndpoint?.() ?? void 0
9678
9890
  });
9679
9891
  const skillUrlsFor = (c) => {
9680
9892
  if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
@@ -10250,10 +10462,17 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
10250
10462
  return { error: e?.message || String(e) };
10251
10463
  }
10252
10464
  },
10253
- recordMeetingActivity: async (params) => {
10465
+ recordMeetingActivity: async (params, context) => {
10254
10466
  const c = channelStore.get(params.channel);
10255
10467
  if (!c || c.enabled === false) return { error: "channel not found" };
10256
- const r = resolveSender(c, { key: params.key, from: params.from });
10468
+ const u = context?.user;
10469
+ const r = resolveSender(c, {
10470
+ key: params.key,
10471
+ from: params.from,
10472
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
10473
+ hyphaAnonymous: u?.is_anonymous === true,
10474
+ hyphaWorkspace: u?.scope?.current_workspace
10475
+ });
10257
10476
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
10258
10477
  const text = String(params.text || "").replace(/\s+/g, " ").trim().slice(0, 1e3);
10259
10478
  if (!text) return { ok: true };
@@ -10721,7 +10940,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
10721
10940
  // ── Listener Registration ──
10722
10941
  registerListener: async (callback, context) => {
10723
10942
  authorizeRequest(context, metadata.sharing, "view");
10943
+ const MAX_LISTENERS = 64;
10724
10944
  listeners.push(callback);
10945
+ while (listeners.length > MAX_LISTENERS) listeners.shift();
10725
10946
  const replayMessages = messages.slice(-50);
10726
10947
  const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
10727
10948
  for (const msg of replayMessages) {
@@ -11463,6 +11684,25 @@ function handleClaudeMessage(ctx, msg) {
11463
11684
  }
11464
11685
  }
11465
11686
 
11687
+ function sessionQueueMax(env = process.env) {
11688
+ return Math.max(10, Number(env.SVAMP_SESSION_QUEUE_MAX) || 200);
11689
+ }
11690
+ function appendToMessageQueue(existing, entry, max = sessionQueueMax()) {
11691
+ const next = [...existing, entry];
11692
+ if (next.length <= max) return { queue: next, dropped: 0 };
11693
+ const overflow = next.length - max;
11694
+ let budget = overflow;
11695
+ const kept = [];
11696
+ for (const item of next) {
11697
+ if (budget > 0 && item !== entry && item.inboxHop !== void 0) {
11698
+ budget--;
11699
+ continue;
11700
+ }
11701
+ kept.push(item);
11702
+ }
11703
+ return { queue: kept, dropped: overflow - budget };
11704
+ }
11705
+
11466
11706
  const KNOWN_AGENT_LAUNCHERS = ["claude", "codex", "gemini", "kimi"];
11467
11707
  function agentLauncherExists(cmd, env = process.env) {
11468
11708
  if (!cmd) return false;
@@ -13580,6 +13820,25 @@ var acpAgentConfig = /*#__PURE__*/Object.freeze({
13580
13820
  resolveAcpAgentConfig: resolveAcpAgentConfig
13581
13821
  });
13582
13822
 
13823
+ const COMPLETED_REQUESTS_MAX = 500;
13824
+ function recordCompletedRequest(existing, id, entry, max = COMPLETED_REQUESTS_MAX) {
13825
+ const out = { ...existing || {} };
13826
+ delete out[id];
13827
+ const slim = {
13828
+ completedAt: typeof entry.completedAt === "number" ? entry.completedAt : Date.now(),
13829
+ status: String(entry.status ?? "")
13830
+ };
13831
+ if (typeof entry.tool === "string") slim.tool = entry.tool;
13832
+ if (entry.reason !== void 0) slim.reason = entry.reason;
13833
+ if (entry.mode !== void 0) slim.mode = entry.mode;
13834
+ if (entry.decision !== void 0) slim.decision = entry.decision;
13835
+ if (Array.isArray(entry.allowedTools)) slim.allowedTools = entry.allowedTools;
13836
+ out[id] = slim;
13837
+ const keys = Object.keys(out);
13838
+ for (let i = 0; i < keys.length - max; i++) delete out[keys[i]];
13839
+ return out;
13840
+ }
13841
+
13583
13842
  function applyPermissionResolution(sessionService, requestId, approved) {
13584
13843
  const reqs = { ...sessionService._agentState?.requests };
13585
13844
  if (!(requestId in reqs)) return;
@@ -13589,14 +13848,12 @@ function applyPermissionResolution(sessionService, requestId, approved) {
13589
13848
  sessionService.updateAgentState({
13590
13849
  controlledByUser: false,
13591
13850
  requests: reqs,
13592
- completedRequests: {
13593
- ...completedReqs,
13594
- [requestId]: {
13595
- ...existingReq || {},
13596
- completedAt: Date.now(),
13597
- status: approved ? "approved" : "denied"
13598
- }
13599
- }
13851
+ // #1041: bounded + slimmed, same as the Claude permission path in daemon/run.ts.
13852
+ completedRequests: recordCompletedRequest(completedReqs, requestId, {
13853
+ ...existingReq || {},
13854
+ completedAt: Date.now(),
13855
+ status: approved ? "approved" : "denied"
13856
+ })
13600
13857
  });
13601
13858
  }
13602
13859
  function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, log, onTurnEnd, getModelLabel) {
@@ -17917,6 +18174,9 @@ function shouldRunZombieProbe(args) {
17917
18174
  function nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures) {
17918
18175
  return consecutiveHeartbeatFailures + 1;
17919
18176
  }
18177
+ function shouldRunServiceProbe(args) {
18178
+ return !args.inGrace && !args.zombieProbeRan;
18179
+ }
17920
18180
  function shouldForceReconnect(consecutiveHeartbeatFailures) {
17921
18181
  if (consecutiveHeartbeatFailures < 2) return false;
17922
18182
  return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
@@ -19964,7 +20224,7 @@ async function startDaemon(options) {
19964
20224
  try {
19965
20225
  const dir = loadSessionIndex()[sessionId]?.directory;
19966
20226
  if (!dir) return;
19967
- const { reconcileServiceLinks } = await import('./agentCommands-BE3bFu8e.mjs');
20227
+ const { reconcileServiceLinks } = await import('./agentCommands-BiSVzb5D.mjs');
19968
20228
  const configPath = getSvampConfigPath(dir, sessionId);
19969
20229
  const config = readSvampConfig(configPath);
19970
20230
  const entries = Array.from(urls.entries());
@@ -19986,7 +20246,7 @@ async function startDaemon(options) {
19986
20246
  try {
19987
20247
  const dir = loadSessionIndex()[sessionId]?.directory;
19988
20248
  if (!dir) return;
19989
- const { reconcileServiceLinks } = await import('./agentCommands-BE3bFu8e.mjs');
20249
+ const { reconcileServiceLinks } = await import('./agentCommands-BiSVzb5D.mjs');
19990
20250
  const configPath = getSvampConfigPath(dir, sessionId);
19991
20251
  const config = readSvampConfig(configPath);
19992
20252
  const incoming = [{
@@ -20007,7 +20267,7 @@ async function startDaemon(options) {
20007
20267
  try {
20008
20268
  const dir = loadSessionIndex()[sessionId]?.directory;
20009
20269
  if (!dir) return;
20010
- const { dropServiceLinks } = await import('./agentCommands-BE3bFu8e.mjs');
20270
+ const { dropServiceLinks } = await import('./agentCommands-BiSVzb5D.mjs');
20011
20271
  const configPath = getSvampConfigPath(dir, sessionId);
20012
20272
  const config = readSvampConfig(configPath);
20013
20273
  if (dropServiceLinks(config, "serve", mountName)) {
@@ -20050,7 +20310,7 @@ async function startDaemon(options) {
20050
20310
  ensureAutoInstalledCommands(logger);
20051
20311
  (async () => {
20052
20312
  try {
20053
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-8SZDu7O-.mjs');
20313
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-BO0Qs_kE.mjs');
20054
20314
  beginClaudeVersionReconcile((msg) => logger.log(msg));
20055
20315
  } catch (e) {
20056
20316
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -21270,15 +21530,19 @@ ${parts.join("\n")}`);
21270
21530
  sessionService.updateAgentState({
21271
21531
  controlledByUser: false,
21272
21532
  requests: reqs,
21273
- completedRequests: {
21274
- ...sessionService._agentState?.completedRequests,
21275
- [correlationId]: {
21533
+ // #1041: bounded + slimmed. `arguments: toolInput` used to
21534
+ // be stored here — the whole tool input, e.g. a full Write
21535
+ // body — in a map that grew forever and is re-walked by the
21536
+ // reducer on every streamed batch.
21537
+ completedRequests: recordCompletedRequest(
21538
+ sessionService._agentState?.completedRequests,
21539
+ correlationId,
21540
+ {
21276
21541
  tool: toolName,
21277
- arguments: toolInput,
21278
21542
  completedAt: Date.now(),
21279
21543
  status: result.behavior === "allow" ? "approved" : "denied"
21280
21544
  }
21281
- }
21545
+ )
21282
21546
  });
21283
21547
  }).catch((err) => {
21284
21548
  logger.log(`[Session ${sessionId}] Permission handler error (request ${requestId}): ${err}`);
@@ -22314,11 +22578,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22314
22578
  } else {
22315
22579
  logger.log(`[Session ${sessionId}] Queuing inbox message for idle processing`);
22316
22580
  const existingQueue = sessionMetadata.messageQueue || [];
22317
- sessionMetadata = {
22318
- ...sessionMetadata,
22319
- messageQueue: [...existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId }]
22320
- };
22581
+ const { queue: cappedQueue, dropped } = appendToMessageQueue(existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId });
22582
+ sessionMetadata = { ...sessionMetadata, messageQueue: cappedQueue };
22321
22583
  sessionService.updateMetadata(sessionMetadata);
22584
+ if (dropped > 0) {
22585
+ logger.log(`[Session ${sessionId}] Inbox queue over ${sessionQueueMax()}; dropped ${dropped} oldest queued inbox message(s)`);
22586
+ sessionService.pushMessage({ type: "message", message: `Inbox queue full (${sessionQueueMax()}) \u2014 dropped ${dropped} older queued message(s).`, level: "warning" }, "event");
22587
+ }
22322
22588
  if (!sessionWasProcessing) {
22323
22589
  setTimeout(() => processMessageQueueRef?.(), 200);
22324
22590
  }
@@ -22377,11 +22643,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22377
22643
  });
22378
22644
  },
22379
22645
  onIssue: async (params) => {
22380
- const { issueRpc } = await import('./rpc-D_pFjWr1.mjs');
22646
+ const { issueRpc } = await import('./rpc-DXY3KqiA.mjs');
22381
22647
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
22382
22648
  },
22383
22649
  onWorkflow: async (params) => {
22384
- const { workflowRpc } = await import('./rpc-D9LFUW93.mjs');
22650
+ const { workflowRpc } = await import('./rpc-hdw0ZlCj.mjs');
22385
22651
  return workflowRpc(params?.cwd || directory, params || {});
22386
22652
  },
22387
22653
  onRipgrep: async (args, cwd) => {
@@ -22992,11 +23258,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
22992
23258
  } else {
22993
23259
  logger.log(`[${agentName} Session ${sessionId}] Queuing inbox message for idle processing`);
22994
23260
  const existingQueue = sessionMetadata.messageQueue || [];
22995
- sessionMetadata = {
22996
- ...sessionMetadata,
22997
- messageQueue: [...existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId }]
22998
- };
23261
+ const { queue: cappedQueue, dropped } = appendToMessageQueue(existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId });
23262
+ sessionMetadata = { ...sessionMetadata, messageQueue: cappedQueue };
22999
23263
  sessionService.updateMetadata(sessionMetadata);
23264
+ if (dropped > 0) {
23265
+ logger.log(`[${agentName} Session ${sessionId}] Inbox queue over ${sessionQueueMax()}; dropped ${dropped} oldest queued inbox message(s)`);
23266
+ sessionService.pushMessage({ type: "message", message: `Inbox queue full (${sessionQueueMax()}) \u2014 dropped ${dropped} older queued message(s).`, level: "warning" }, "event");
23267
+ }
23000
23268
  if (sessionMetadata.lifecycleState === "idle" && acpBackendReady) {
23001
23269
  const queue = sessionMetadata.messageQueue;
23002
23270
  if (queue && queue.length > 0) {
@@ -23085,11 +23353,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
23085
23353
  });
23086
23354
  },
23087
23355
  onIssue: async (params) => {
23088
- const { issueRpc } = await import('./rpc-D_pFjWr1.mjs');
23356
+ const { issueRpc } = await import('./rpc-DXY3KqiA.mjs');
23089
23357
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
23090
23358
  },
23091
23359
  onWorkflow: async (params) => {
23092
- const { workflowRpc } = await import('./rpc-D9LFUW93.mjs');
23360
+ const { workflowRpc } = await import('./rpc-hdw0ZlCj.mjs');
23093
23361
  return workflowRpc(params?.cwd || directory, params || {});
23094
23362
  },
23095
23363
  onRipgrep: async (args, cwd) => {
@@ -23997,7 +24265,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
23997
24265
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
23998
24266
  if (channelHttpPort > 0) {
23999
24267
  try {
24000
- const { createChannelHttpServer } = await import('./httpServer-1XjB2h3K.mjs');
24268
+ const { createChannelHttpServer } = await import('./httpServer-Be1Hcddr.mjs');
24001
24269
  const channelHttpServer = createChannelHttpServer({
24002
24270
  getSessionIds: () => {
24003
24271
  const ids = [];
@@ -24113,11 +24381,27 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24113
24381
  }
24114
24382
  return null;
24115
24383
  });
24384
+ serveManager$1.setCapabilityAuthorizer((sessionId, token) => {
24385
+ if (!token || !token.startsWith("ck_")) return false;
24386
+ let cwd = null;
24387
+ for (const [, session] of pidToTrackedSession) {
24388
+ if (session.svampSessionId === sessionId && !session.stopped && session.directory) {
24389
+ cwd = session.directory;
24390
+ break;
24391
+ }
24392
+ }
24393
+ if (!cwd) return false;
24394
+ try {
24395
+ return channelKeyGrantsFileAccess(new ChannelStore(cwd).list(), token);
24396
+ } catch {
24397
+ return false;
24398
+ }
24399
+ });
24116
24400
  const sessionFilesToken = createHash("sha256").update(`session-files:${machineId}`).digest("hex").slice(0, 20);
24117
24401
  serveManager$1.ensureSessionFilesGateway(sessionFilesToken).then((base) => logger.log(`[serve] session-files gateway: ${base || "(tunnel pending)"}`)).catch((err) => logger.error(`[serve] session-files gateway failed: ${err?.message || err}`));
24118
24402
  const persistedSessions = loadPersistedSessions();
24403
+ const knownIds = new Set(persistedSessions.map((s) => s.sessionId).concat(Object.keys(loadSessionIndex())));
24119
24404
  try {
24120
- const knownIds = new Set(persistedSessions.map((s) => s.sessionId).concat(Object.keys(loadSessionIndex())));
24121
24405
  const { removed } = await sweepOrphanedStagedHomes(knownIds);
24122
24406
  if (removed.length > 0) {
24123
24407
  logger.log(`Cleaned up ${removed.length} orphaned staged home(s) from prior runs.`);
@@ -24128,7 +24412,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24128
24412
  const sessionsToAutoContinue = [];
24129
24413
  const sessionsToLoopResume = [];
24130
24414
  {
24131
- const knownSessionIds = new Set(persistedSessions.map((p) => p.sessionId));
24415
+ const knownSessionIds = knownIds;
24132
24416
  const sweptDirs = /* @__PURE__ */ new Set();
24133
24417
  for (const p of persistedSessions) {
24134
24418
  if (sweptDirs.has(p.directory)) continue;
@@ -24153,7 +24437,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24153
24437
  }
24154
24438
  if (persistedSessions.length > 0) {
24155
24439
  try {
24156
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-8SZDu7O-.mjs');
24440
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-BO0Qs_kE.mjs');
24157
24441
  await awaitClaudeVersionReady();
24158
24442
  } catch {
24159
24443
  }
@@ -24410,7 +24694,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24410
24694
  const PING_TIMEOUT_MS = 15e3;
24411
24695
  const POST_RECONNECT_GRACE_MS = 2e4;
24412
24696
  const RECONNECT_JITTER_MS = 2500;
24413
- const { WorkflowScheduler } = await import('./scheduler-bgEMu7PK.mjs');
24697
+ const { WorkflowScheduler } = await import('./scheduler-3LUL1jFD.mjs');
24414
24698
  const workflowProjectRoots = () => {
24415
24699
  const dirs = /* @__PURE__ */ new Set();
24416
24700
  for (const s of pidToTrackedSession.values()) {
@@ -24489,7 +24773,9 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24489
24773
  const INBOUND_SILENCE_THRESHOLD_MS = 12e4;
24490
24774
  const inboundSilenceMs = Date.now() - machineService.getLastInboundRpcAt();
24491
24775
  const hasActiveSessions = pidToTrackedSession.size > 0;
24776
+ let zombieProbeRan = false;
24492
24777
  if (shouldRunZombieProbe({ hasActiveSessions, inboundSilenceMs, consecutiveHeartbeatFailures, inGrace, thresholdMs: INBOUND_SILENCE_THRESHOLD_MS })) {
24778
+ zombieProbeRan = true;
24493
24779
  logger.log(`No inbound RPC for ${Math.round(inboundSilenceMs / 1e3)}s with ${pidToTrackedSession.size} active session(s) \u2014 zombie probe`);
24494
24780
  try {
24495
24781
  const machineServiceId = `${server.config.client_id}:default`;
@@ -24502,7 +24788,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
24502
24788
  consecutiveHeartbeatFailures = nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures);
24503
24789
  }
24504
24790
  }
24505
- if (!inGrace) {
24791
+ if (shouldRunServiceProbe({ inGrace, zombieProbeRan })) {
24506
24792
  try {
24507
24793
  const pingStart = Date.now();
24508
24794
  const machineServiceId = `${server.config.client_id}:default`;