svamp-cli 0.2.326 → 0.2.328
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.
- package/dist/{adminCommands-D1NzA4Hb.mjs → adminCommands-DotiO7jx.mjs} +1 -1
- package/dist/{agentCommands-DUHUBzrj.mjs → agentCommands-DefL1pIo.mjs} +5 -5
- package/dist/{auth-DogM3PPm.mjs → auth-DLK3tc_f.mjs} +1 -1
- package/dist/{cli-PWoHWBkF.mjs → cli-BCN1uAze.mjs} +77 -77
- package/dist/cli.mjs +2 -2
- package/dist/{commands-D-4ry2I6.mjs → commands-BD6Qfv3u.mjs} +11 -11
- package/dist/{commands-CqqQYRzg.mjs → commands-BEnXz1U_.mjs} +2 -2
- package/dist/{commands-Dt_MecCB.mjs → commands-BFlfbiey.mjs} +1 -1
- package/dist/{commands-CxfUr2Qr.mjs → commands-DEZLlXJ3.mjs} +1 -1
- package/dist/{commands-B5bC9eXz.mjs → commands-DH2WCdPH.mjs} +3 -3
- package/dist/{commands-Dm9KNB-p.mjs → commands-gO42Ijz3.mjs} +3 -3
- package/dist/{commands-CBk4dLUP.mjs → commands-uN_gulw0.mjs} +3 -3
- package/dist/{commands-Ck4pP86Q.mjs → commands-xhA5FWM8.mjs} +2 -2
- package/dist/{fleet-Bzg3YxM4.mjs → fleet-CbKnESdR.mjs} +2 -2
- package/dist/{headlessCli-DvJI9DRh.mjs → headlessCli-2wTVI-Kj.mjs} +2 -2
- package/dist/{httpServer-1XjB2h3K.mjs → httpServer-Be1Hcddr.mjs} +59 -38
- package/dist/index.mjs +1 -1
- package/dist/{notifyCommands-BpMErHcv.mjs → notifyCommands--tpDRdrL.mjs} +1 -1
- package/dist/package-BORyBEey.mjs +64 -0
- package/dist/{rpc-27xBajCe.mjs → rpc-BCLZpI5H.mjs} +1 -1
- package/dist/{rpc-Qcj4M_LQ.mjs → rpc-CZqCc9Pr.mjs} +1 -1
- package/dist/{run-CocRShOR.mjs → run-BAYXQ4-N.mjs} +1 -1
- package/dist/{run-BBBr4jkI.mjs → run-CSV6OgQ8.mjs} +274 -56
- package/dist/{scheduler-DjAVEyn3.mjs → scheduler-s_j089nA.mjs} +1 -1
- package/dist/{serveCommands-BK-wWc_i.mjs → serveCommands-C4779W5n.mjs} +19 -11
- package/dist/{sideband-5ck3CDWM.mjs → sideband-B7sDP61Z.mjs} +1 -1
- package/package.json +3 -3
- package/dist/package-D_BgajkI.mjs +0 -64
|
@@ -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);
|
|
@@ -3203,8 +3210,19 @@ class ServeManager {
|
|
|
3203
3210
|
* Extract a channel capability token from the request, if one is present. A channel caller
|
|
3204
3211
|
* key is `ck_`-prefixed; we ONLY return a token that carries that prefix so a Hypha JWT
|
|
3205
3212
|
* bearer (used by the email-identity path) never collides with the capability authorizer.
|
|
3206
|
-
* Accepted carriers: `Authorization: Bearer ck_…` header, or `?key=ck_…` query
|
|
3207
|
-
* Returns null when no capability token is presented.
|
|
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.
|
|
3208
3226
|
*/
|
|
3209
3227
|
extractCapabilityToken(req, url) {
|
|
3210
3228
|
const auth = req.headers["authorization"];
|
|
@@ -3212,6 +3230,8 @@ class ServeManager {
|
|
|
3212
3230
|
const m = /^Bearer\s+(\S+)$/i.exec(auth.trim());
|
|
3213
3231
|
if (m && m[1].startsWith("ck_")) return m[1];
|
|
3214
3232
|
}
|
|
3233
|
+
const method = (req.method || "GET").toUpperCase();
|
|
3234
|
+
if (method !== "GET" && method !== "HEAD") return null;
|
|
3215
3235
|
const q = url.searchParams.get("key");
|
|
3216
3236
|
if (q && q.startsWith("ck_")) return q;
|
|
3217
3237
|
return null;
|
|
@@ -3321,7 +3341,8 @@ class ServeManager {
|
|
|
3321
3341
|
if (replaced) {
|
|
3322
3342
|
await this.removeMount(spec.name, { replacing: true });
|
|
3323
3343
|
}
|
|
3324
|
-
|
|
3344
|
+
const crossSessionTakeover = !!(replaced?.sessionId && replaced.sessionId !== spec.sessionId);
|
|
3345
|
+
if (crossSessionTakeover) {
|
|
3325
3346
|
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).`);
|
|
3326
3347
|
this.fireMountUnbound(replaced.sessionId, spec.name);
|
|
3327
3348
|
}
|
|
@@ -3329,6 +3350,7 @@ class ServeManager {
|
|
|
3329
3350
|
if (access === "owner" && !spec.ownerEmail) {
|
|
3330
3351
|
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"].`);
|
|
3331
3352
|
}
|
|
3353
|
+
const reusableToken = replaced && access === "link" && replaced.linkToken && !spec.regenerateUrl && !crossSessionTakeover ? replaced.linkToken : void 0;
|
|
3332
3354
|
const mount = {
|
|
3333
3355
|
name: spec.name,
|
|
3334
3356
|
directory: resolvedDir,
|
|
@@ -3336,10 +3358,10 @@ class ServeManager {
|
|
|
3336
3358
|
sessionId: spec.sessionId,
|
|
3337
3359
|
ownerEmail: spec.ownerEmail,
|
|
3338
3360
|
access,
|
|
3339
|
-
//
|
|
3340
|
-
//
|
|
3341
|
-
//
|
|
3342
|
-
linkToken: access === "link" ? spec.linkToken || generateLinkToken() : void 0,
|
|
3361
|
+
// Capability token for 'link' access. Precedence: an explicit spec.linkToken
|
|
3362
|
+
// (used by restore to preserve URLs across daemon restarts) → the existing
|
|
3363
|
+
// mount's token on a stable re-apply (#1072) → a freshly generated token.
|
|
3364
|
+
linkToken: access === "link" ? spec.linkToken || reusableToken || generateLinkToken() : void 0,
|
|
3343
3365
|
addedAt: Date.now()
|
|
3344
3366
|
};
|
|
3345
3367
|
this.mounts.set(spec.name, mount);
|
|
@@ -3857,6 +3879,14 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3857
3879
|
}
|
|
3858
3880
|
const rootDir = target.cwd;
|
|
3859
3881
|
const normRel = this.toSessionRelative(rootDir, m[2] || "");
|
|
3882
|
+
if (capabilityGranted) {
|
|
3883
|
+
const badSeg = hiddenSegmentOf(normRel);
|
|
3884
|
+
if (badSeg) {
|
|
3885
|
+
this.log(`Files-gateway DENY '${sessionId}': channel capability may not reach a hidden path (${badSeg})`);
|
|
3886
|
+
deny(403, `Forbidden: channel file access may not reach hidden paths (${badSeg})`);
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3860
3890
|
if (method === "GET" || method === "HEAD") {
|
|
3861
3891
|
if (url.searchParams.get("list") === "1") {
|
|
3862
3892
|
const lex = this.resolveContainedPath(rootDir, normRel);
|
|
@@ -3878,6 +3908,7 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3878
3908
|
}
|
|
3879
3909
|
return { name: d.name, isDir, size, mtime };
|
|
3880
3910
|
});
|
|
3911
|
+
if (capabilityGranted) entries = entries.filter((e) => !e.name.startsWith("."));
|
|
3881
3912
|
} catch (err) {
|
|
3882
3913
|
deny(err?.code === "ENOENT" ? 404 : 500, `Listing failed: ${err?.message || err}`);
|
|
3883
3914
|
return;
|
|
@@ -4458,6 +4489,7 @@ var serveManager = /*#__PURE__*/Object.freeze({
|
|
|
4458
4489
|
ServeManager: ServeManager,
|
|
4459
4490
|
assertNonAdminMountDirSafe: assertNonAdminMountDirSafe,
|
|
4460
4491
|
buildLinkSubdomain: buildLinkSubdomain,
|
|
4492
|
+
hiddenSegmentOf: hiddenSegmentOf,
|
|
4461
4493
|
resolveMountOwnerEmail: resolveMountOwnerEmail,
|
|
4462
4494
|
sanitizeMountForRole: sanitizeMountForRole
|
|
4463
4495
|
});
|
|
@@ -4636,8 +4668,13 @@ function applyPromptCacheTtl(spawnEnv) {
|
|
|
4636
4668
|
const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
|
|
4637
4669
|
if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
|
|
4638
4670
|
}
|
|
4671
|
+
function applyTodoTools(spawnEnv) {
|
|
4672
|
+
const override = spawnEnv.CLAUDE_CODE_ENABLE_TODO_TOOLS ?? process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS;
|
|
4673
|
+
if (override === void 0) spawnEnv.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1";
|
|
4674
|
+
}
|
|
4639
4675
|
function applyClaudeProxyEnv(spawnEnv) {
|
|
4640
4676
|
const mode = currentMode();
|
|
4677
|
+
applyTodoTools(spawnEnv);
|
|
4641
4678
|
if (mode === "hypha") {
|
|
4642
4679
|
const proxyUrl = resolveHyphaProxyUrl();
|
|
4643
4680
|
if (!proxyUrl) {
|
|
@@ -5692,20 +5729,22 @@ function channelKeyGrantsFileAccess(channels, token) {
|
|
|
5692
5729
|
}
|
|
5693
5730
|
};
|
|
5694
5731
|
for (const ch of channels) {
|
|
5732
|
+
if (ch.enabled === false) continue;
|
|
5695
5733
|
if (!ch.fileAccess?.enabled) continue;
|
|
5696
5734
|
if (eq(ch.identity?.shared_key)) return true;
|
|
5697
5735
|
for (const caller of ch.identity?.callers || []) if (eq(caller.key)) return true;
|
|
5698
5736
|
}
|
|
5699
5737
|
return false;
|
|
5700
5738
|
}
|
|
5739
|
+
const _channelListCache = /* @__PURE__ */ new Map();
|
|
5740
|
+
const _CHANNEL_CACHE_TTL_MS = 1e3;
|
|
5741
|
+
function invalidateChannelCache(dir) {
|
|
5742
|
+
_channelListCache.delete(dir);
|
|
5743
|
+
}
|
|
5701
5744
|
class ChannelStore {
|
|
5702
5745
|
dir;
|
|
5703
5746
|
constructor(projectDir) {
|
|
5704
5747
|
this.dir = join$1(projectDir, ".svamp", "channels");
|
|
5705
|
-
try {
|
|
5706
|
-
mkdirSync$1(this.dir, { recursive: true });
|
|
5707
|
-
} catch {
|
|
5708
|
-
}
|
|
5709
5748
|
}
|
|
5710
5749
|
// #0982: reject a traversing/oversized id at the path boundary itself, so no
|
|
5711
5750
|
// caller (RPC, HTTP, CLI) can reach outside this.dir regardless of its own checks.
|
|
@@ -5728,17 +5767,41 @@ class ChannelStore {
|
|
|
5728
5767
|
const tmp = `${this._path(c.id)}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
5729
5768
|
writeFileSync$1(tmp, JSON.stringify(c, null, 2), { mode: 384 });
|
|
5730
5769
|
renameSync(tmp, this._path(c.id));
|
|
5770
|
+
invalidateChannelCache(this.dir);
|
|
5731
5771
|
return c;
|
|
5732
5772
|
}
|
|
5773
|
+
/**
|
|
5774
|
+
* All channels in this project, parsed. Cached per directory and revalidated with ONE statSync
|
|
5775
|
+
* — see the #1049/#1068 note above for why the uncached version was a daemon-wide stall.
|
|
5776
|
+
*/
|
|
5733
5777
|
list() {
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
}
|
|
5778
|
+
let st;
|
|
5779
|
+
try {
|
|
5780
|
+
const s = statSync(this.dir);
|
|
5781
|
+
st = { mtimeMs: s.mtimeMs, size: s.size };
|
|
5782
|
+
} catch {
|
|
5783
|
+
_channelListCache.delete(this.dir);
|
|
5784
|
+
return [];
|
|
5785
|
+
}
|
|
5786
|
+
const hit = _channelListCache.get(this.dir);
|
|
5787
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size && Date.now() - hit.at < _CHANNEL_CACHE_TTL_MS) {
|
|
5788
|
+
return hit.channels;
|
|
5789
|
+
}
|
|
5790
|
+
let channels;
|
|
5791
|
+
try {
|
|
5792
|
+
channels = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
5793
|
+
try {
|
|
5794
|
+
return normalizeBind(JSON.parse(readFileSync(join$1(this.dir, f), "utf8")));
|
|
5795
|
+
} catch {
|
|
5796
|
+
return null;
|
|
5797
|
+
}
|
|
5798
|
+
}).filter((c) => !!c);
|
|
5799
|
+
} catch {
|
|
5800
|
+
_channelListCache.delete(this.dir);
|
|
5801
|
+
return [];
|
|
5802
|
+
}
|
|
5803
|
+
_channelListCache.set(this.dir, { mtimeMs: st.mtimeMs, size: st.size, at: Date.now(), channels });
|
|
5804
|
+
return channels;
|
|
5742
5805
|
}
|
|
5743
5806
|
get(id) {
|
|
5744
5807
|
try {
|
|
@@ -5756,6 +5819,7 @@ class ChannelStore {
|
|
|
5756
5819
|
const p = this._path(id);
|
|
5757
5820
|
if (existsSync(p)) {
|
|
5758
5821
|
rmSync$1(p);
|
|
5822
|
+
invalidateChannelCache(this.dir);
|
|
5759
5823
|
return true;
|
|
5760
5824
|
}
|
|
5761
5825
|
return false;
|
|
@@ -5796,6 +5860,28 @@ class ChannelStore {
|
|
|
5796
5860
|
return caller;
|
|
5797
5861
|
});
|
|
5798
5862
|
}
|
|
5863
|
+
/**
|
|
5864
|
+
* #1036: revoke ONE caller's key without destroying the channel.
|
|
5865
|
+
*
|
|
5866
|
+
* Before this, the only removal primitive was remove(id) — so withdrawing a single leaked key
|
|
5867
|
+
* meant deleting the whole channel and every other caller with it. That is a bad enough trade
|
|
5868
|
+
* that in practice a leaked key just stayed live, which matters much more now that a caller key
|
|
5869
|
+
* can carry whole-project file access (see channelKeyGrantsFileAccess).
|
|
5870
|
+
*
|
|
5871
|
+
* Same per-channel lock as the sibling mutators (#0679), so a concurrent addCaller cannot
|
|
5872
|
+
* resurrect the revoked entry by writing a stale base. Returns true when a caller was removed.
|
|
5873
|
+
*/
|
|
5874
|
+
removeCaller(id, name) {
|
|
5875
|
+
return withFileLock(this._lock(id), () => {
|
|
5876
|
+
const c = this.get(id);
|
|
5877
|
+
if (!c?.identity?.callers) return false;
|
|
5878
|
+
const before = c.identity.callers.length;
|
|
5879
|
+
c.identity.callers = c.identity.callers.filter((x) => x.name !== name);
|
|
5880
|
+
if (c.identity.callers.length === before) return false;
|
|
5881
|
+
this._writeChannel(c);
|
|
5882
|
+
return true;
|
|
5883
|
+
});
|
|
5884
|
+
}
|
|
5799
5885
|
}
|
|
5800
5886
|
function routingSession(channel, ctx) {
|
|
5801
5887
|
const mode = bindMode(channel);
|
|
@@ -6435,6 +6521,9 @@ function getParamNames(fn) {
|
|
|
6435
6521
|
const terminalSessions = /* @__PURE__ */ new Map();
|
|
6436
6522
|
let ptyModule = null;
|
|
6437
6523
|
const TERMINAL_IDLE_MS = 30 * 60 * 1e3;
|
|
6524
|
+
const TERMINAL_CLIENT_ABSENT_MS = 4 * 60 * 60 * 1e3;
|
|
6525
|
+
const TERMINAL_BUFFER_MAX_BYTES = 1024 * 1024;
|
|
6526
|
+
const TERMINAL_MAX_SESSIONS = 64;
|
|
6438
6527
|
let terminalReaper = null;
|
|
6439
6528
|
function ensureTerminalReaper() {
|
|
6440
6529
|
if (terminalReaper) return;
|
|
@@ -6442,7 +6531,8 @@ function ensureTerminalReaper() {
|
|
|
6442
6531
|
const now = Date.now();
|
|
6443
6532
|
for (const [id, s] of terminalSessions) {
|
|
6444
6533
|
if (s.exited) continue;
|
|
6445
|
-
|
|
6534
|
+
const abandoned = now - (s.lastClientActivity ?? s.lastActivity) > TERMINAL_CLIENT_ABSENT_MS;
|
|
6535
|
+
if (now - s.lastActivity > TERMINAL_IDLE_MS || abandoned) {
|
|
6446
6536
|
try {
|
|
6447
6537
|
s.pty.kill();
|
|
6448
6538
|
} catch {
|
|
@@ -7296,8 +7386,29 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7296
7386
|
rows,
|
|
7297
7387
|
createdAt: Date.now(),
|
|
7298
7388
|
lastActivity: Date.now(),
|
|
7389
|
+
lastClientActivity: Date.now(),
|
|
7390
|
+
outputBytes: 0,
|
|
7299
7391
|
cwd
|
|
7300
7392
|
};
|
|
7393
|
+
if (terminalSessions.size >= TERMINAL_MAX_SESSIONS) {
|
|
7394
|
+
let victim = null, oldest = Infinity;
|
|
7395
|
+
for (const [id, s] of terminalSessions) {
|
|
7396
|
+
const seen = s.lastClientActivity ?? s.lastActivity;
|
|
7397
|
+
if (seen < oldest) {
|
|
7398
|
+
oldest = seen;
|
|
7399
|
+
victim = id;
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
if (victim) {
|
|
7403
|
+
const v = terminalSessions.get(victim);
|
|
7404
|
+
try {
|
|
7405
|
+
v?.pty.kill();
|
|
7406
|
+
} catch {
|
|
7407
|
+
}
|
|
7408
|
+
terminalSessions.delete(victim);
|
|
7409
|
+
console.log(`[terminal] evicted idlest session ${victim} \u2014 at the ${TERMINAL_MAX_SESSIONS}-PTY ceiling`);
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7301
7412
|
terminalSessions.set(sessionId, session);
|
|
7302
7413
|
ensureTerminalReaper();
|
|
7303
7414
|
let outputBatch = "";
|
|
@@ -7308,8 +7419,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7308
7419
|
const batch = outputBatch;
|
|
7309
7420
|
outputBatch = "";
|
|
7310
7421
|
session.outputBuffer.push(batch);
|
|
7311
|
-
|
|
7312
|
-
|
|
7422
|
+
session.outputBytes += batch.length;
|
|
7423
|
+
while (session.outputBytes > TERMINAL_BUFFER_MAX_BYTES && session.outputBuffer.length > 1) {
|
|
7424
|
+
session.outputBytes -= (session.outputBuffer.shift() || "").length;
|
|
7313
7425
|
}
|
|
7314
7426
|
if (terminalClient) {
|
|
7315
7427
|
try {
|
|
@@ -7358,6 +7470,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7358
7470
|
const filtered = filterTerminalResponses(params.data);
|
|
7359
7471
|
if (filtered) session.pty.write(filtered);
|
|
7360
7472
|
session.lastActivity = Date.now();
|
|
7473
|
+
session.lastClientActivity = Date.now();
|
|
7361
7474
|
return { success: true };
|
|
7362
7475
|
},
|
|
7363
7476
|
/** Resize a terminal session. */
|
|
@@ -7379,7 +7492,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7379
7492
|
const session = terminalSessions.get(params.sessionId);
|
|
7380
7493
|
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
7381
7494
|
const output = session.outputBuffer.splice(0).join("");
|
|
7495
|
+
session.outputBytes = 0;
|
|
7382
7496
|
session.lastActivity = Date.now();
|
|
7497
|
+
session.lastClientActivity = Date.now();
|
|
7383
7498
|
const result = {
|
|
7384
7499
|
output,
|
|
7385
7500
|
exited: session.exited
|
|
@@ -7827,7 +7942,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7827
7942
|
process: params.process,
|
|
7828
7943
|
sessionId: params.sessionId,
|
|
7829
7944
|
access,
|
|
7830
|
-
ownerEmail: ownerEmail2
|
|
7945
|
+
ownerEmail: ownerEmail2,
|
|
7946
|
+
regenerateUrl: params.regenerateUrl
|
|
7831
7947
|
});
|
|
7832
7948
|
},
|
|
7833
7949
|
/** Remove a mount from the shared static file server. */
|
|
@@ -8189,7 +8305,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
8189
8305
|
}
|
|
8190
8306
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
8191
8307
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
8192
|
-
const { toolsForRole } = await import('./sideband-
|
|
8308
|
+
const { toolsForRole } = await import('./sideband-B7sDP61Z.mjs');
|
|
8193
8309
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
8194
8310
|
return fmt(r2);
|
|
8195
8311
|
}
|
|
@@ -8294,7 +8410,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
8294
8410
|
return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
|
|
8295
8411
|
}
|
|
8296
8412
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
8297
|
-
const { queryCore } = await import('./commands-
|
|
8413
|
+
const { queryCore } = await import('./commands-DEZLlXJ3.mjs');
|
|
8298
8414
|
const timeout = c.reply?.timeout_sec || 120;
|
|
8299
8415
|
let result;
|
|
8300
8416
|
try {
|
|
@@ -8858,6 +8974,11 @@ function nextBackoffMs(prevMs) {
|
|
|
8858
8974
|
if (!prevMs || prevMs <= 0) return RL_BACKOFF_MIN_MS;
|
|
8859
8975
|
return Math.min(prevMs * 2, RL_BACKOFF_MAX_MS);
|
|
8860
8976
|
}
|
|
8977
|
+
const LISTENER_ERROR_STRIKE_LIMIT = 3;
|
|
8978
|
+
function decideListenerErrorAction(prevStrikes, limit = LISTENER_ERROR_STRIKE_LIMIT) {
|
|
8979
|
+
const strikes = (prevStrikes > 0 ? prevStrikes : 0) + 1;
|
|
8980
|
+
return { strikes, drop: strikes >= limit };
|
|
8981
|
+
}
|
|
8861
8982
|
function retryDecision(listenerPresent, now, backoffUntil) {
|
|
8862
8983
|
if (!listenerPresent) return "drop";
|
|
8863
8984
|
if (backoffUntil !== void 0 && now < backoffUntil) return "reschedule";
|
|
@@ -9442,6 +9563,7 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
|
|
|
9442
9563
|
}
|
|
9443
9564
|
}
|
|
9444
9565
|
const messageCountCache = /* @__PURE__ */ new Map();
|
|
9566
|
+
const MESSAGE_COUNT_CACHE_MAX = 2e3;
|
|
9445
9567
|
function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
9446
9568
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
9447
9569
|
if (!existsSync(filePath)) return fallbackInMemory;
|
|
@@ -9469,7 +9591,13 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
|
9469
9591
|
for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
|
|
9470
9592
|
if (data.length > 0 && data[data.length - 1] !== "\n") count++;
|
|
9471
9593
|
}
|
|
9594
|
+
messageCountCache.delete(filePath);
|
|
9472
9595
|
messageCountCache.set(filePath, { size: st.size, mtimeMs: st.mtimeMs, count });
|
|
9596
|
+
while (messageCountCache.size > MESSAGE_COUNT_CACHE_MAX) {
|
|
9597
|
+
const oldest = messageCountCache.keys().next();
|
|
9598
|
+
if (oldest.done) break;
|
|
9599
|
+
messageCountCache.delete(oldest.value);
|
|
9600
|
+
}
|
|
9473
9601
|
return count;
|
|
9474
9602
|
} catch {
|
|
9475
9603
|
return fallbackInMemory;
|
|
@@ -9592,11 +9720,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9592
9720
|
const listeners = [];
|
|
9593
9721
|
const rateLimitBackoff = /* @__PURE__ */ new Map();
|
|
9594
9722
|
const rateLimitRetry = /* @__PURE__ */ new Map();
|
|
9723
|
+
const listenerErrorStrikes = /* @__PURE__ */ new Map();
|
|
9595
9724
|
const removeListener = (listener, reason) => {
|
|
9596
9725
|
const idx = listeners.indexOf(listener);
|
|
9597
9726
|
if (idx >= 0) {
|
|
9598
9727
|
listeners.splice(idx, 1);
|
|
9599
9728
|
rateLimitBackoff.delete(listener);
|
|
9729
|
+
listenerErrorStrikes.delete(listener);
|
|
9600
9730
|
const t = rateLimitRetry.get(listener);
|
|
9601
9731
|
if (t) {
|
|
9602
9732
|
clearTimeout(t);
|
|
@@ -9621,8 +9751,15 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9621
9751
|
} else if (kind === "stale") {
|
|
9622
9752
|
removeListener(listener, "stale connection");
|
|
9623
9753
|
} else {
|
|
9624
|
-
|
|
9625
|
-
|
|
9754
|
+
const { strikes, drop } = decideListenerErrorAction(listenerErrorStrikes.get(listener) || 0);
|
|
9755
|
+
if (drop) {
|
|
9756
|
+
listenerErrorStrikes.delete(listener);
|
|
9757
|
+
console.error(`[HYPHA SESSION ${sessionId}] Listener error (strike ${strikes}/${LISTENER_ERROR_STRIKE_LIMIT}, dropping):`, err);
|
|
9758
|
+
removeListener(listener, "async error");
|
|
9759
|
+
} else {
|
|
9760
|
+
listenerErrorStrikes.set(listener, strikes);
|
|
9761
|
+
console.warn(`[HYPHA SESSION ${sessionId}] Listener error (strike ${strikes}/${LISTENER_ERROR_STRIKE_LIMIT}, keeping listener):`, String(err?.message || err || ""));
|
|
9762
|
+
}
|
|
9626
9763
|
}
|
|
9627
9764
|
};
|
|
9628
9765
|
const scheduleBackoffRetry = (listener) => {
|
|
@@ -9670,11 +9807,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9670
9807
|
if (result && typeof result.catch === "function") {
|
|
9671
9808
|
result.then(() => {
|
|
9672
9809
|
rateLimitBackoff.delete(listener);
|
|
9810
|
+
listenerErrorStrikes.delete(listener);
|
|
9673
9811
|
}).catch((err) => handleListenerRejection(listener, err));
|
|
9812
|
+
} else {
|
|
9813
|
+
listenerErrorStrikes.delete(listener);
|
|
9674
9814
|
}
|
|
9675
9815
|
} catch (err) {
|
|
9676
|
-
|
|
9677
|
-
removeListener(listener, "sync error");
|
|
9816
|
+
handleListenerRejection(listener, err);
|
|
9678
9817
|
}
|
|
9679
9818
|
}
|
|
9680
9819
|
};
|
|
@@ -10820,7 +10959,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
10820
10959
|
// ── Listener Registration ──
|
|
10821
10960
|
registerListener: async (callback, context) => {
|
|
10822
10961
|
authorizeRequest(context, metadata.sharing, "view");
|
|
10962
|
+
const MAX_LISTENERS = 64;
|
|
10823
10963
|
listeners.push(callback);
|
|
10964
|
+
while (listeners.length > MAX_LISTENERS) listeners.shift();
|
|
10824
10965
|
const replayMessages = messages.slice(-50);
|
|
10825
10966
|
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
10826
10967
|
for (const msg of replayMessages) {
|
|
@@ -13698,6 +13839,25 @@ var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
|
13698
13839
|
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
13699
13840
|
});
|
|
13700
13841
|
|
|
13842
|
+
const COMPLETED_REQUESTS_MAX = 500;
|
|
13843
|
+
function recordCompletedRequest(existing, id, entry, max = COMPLETED_REQUESTS_MAX) {
|
|
13844
|
+
const out = { ...existing || {} };
|
|
13845
|
+
delete out[id];
|
|
13846
|
+
const slim = {
|
|
13847
|
+
completedAt: typeof entry.completedAt === "number" ? entry.completedAt : Date.now(),
|
|
13848
|
+
status: String(entry.status ?? "")
|
|
13849
|
+
};
|
|
13850
|
+
if (typeof entry.tool === "string") slim.tool = entry.tool;
|
|
13851
|
+
if (entry.reason !== void 0) slim.reason = entry.reason;
|
|
13852
|
+
if (entry.mode !== void 0) slim.mode = entry.mode;
|
|
13853
|
+
if (entry.decision !== void 0) slim.decision = entry.decision;
|
|
13854
|
+
if (Array.isArray(entry.allowedTools)) slim.allowedTools = entry.allowedTools;
|
|
13855
|
+
out[id] = slim;
|
|
13856
|
+
const keys = Object.keys(out);
|
|
13857
|
+
for (let i = 0; i < keys.length - max; i++) delete out[keys[i]];
|
|
13858
|
+
return out;
|
|
13859
|
+
}
|
|
13860
|
+
|
|
13701
13861
|
function applyPermissionResolution(sessionService, requestId, approved) {
|
|
13702
13862
|
const reqs = { ...sessionService._agentState?.requests };
|
|
13703
13863
|
if (!(requestId in reqs)) return;
|
|
@@ -13707,14 +13867,12 @@ function applyPermissionResolution(sessionService, requestId, approved) {
|
|
|
13707
13867
|
sessionService.updateAgentState({
|
|
13708
13868
|
controlledByUser: false,
|
|
13709
13869
|
requests: reqs,
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
}
|
|
13717
|
-
}
|
|
13870
|
+
// #1041: bounded + slimmed, same as the Claude permission path in daemon/run.ts.
|
|
13871
|
+
completedRequests: recordCompletedRequest(completedReqs, requestId, {
|
|
13872
|
+
...existingReq || {},
|
|
13873
|
+
completedAt: Date.now(),
|
|
13874
|
+
status: approved ? "approved" : "denied"
|
|
13875
|
+
})
|
|
13718
13876
|
});
|
|
13719
13877
|
}
|
|
13720
13878
|
function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, log, onTurnEnd, getModelLabel) {
|
|
@@ -15732,6 +15890,30 @@ function classifyRestoreContinuation(opts) {
|
|
|
15732
15890
|
if (opts.wasProcessing && opts.hasResumeId) return "auto-continue";
|
|
15733
15891
|
return "none";
|
|
15734
15892
|
}
|
|
15893
|
+
const PERMANENT_RESTORE_PATTERNS = [
|
|
15894
|
+
/failed to create directory/i,
|
|
15895
|
+
/no such file or directory/i,
|
|
15896
|
+
/\benoent\b/i,
|
|
15897
|
+
// spawn ENOENT = launcher/path missing
|
|
15898
|
+
/no agent launchers?/i,
|
|
15899
|
+
/cli (?:is )?(?:not )?installed|command not found/i,
|
|
15900
|
+
/not authorized|permission denied|\beacces\b/i,
|
|
15901
|
+
/\binvalid\b|malformed|unsupported/i
|
|
15902
|
+
];
|
|
15903
|
+
function classifyRestoreFailure(errorMessage) {
|
|
15904
|
+
const m = errorMessage || "";
|
|
15905
|
+
for (const re of PERMANENT_RESTORE_PATTERNS) if (re.test(m)) return "permanent";
|
|
15906
|
+
return "transient";
|
|
15907
|
+
}
|
|
15908
|
+
const RESTORE_MAX_RETRIES = 3;
|
|
15909
|
+
const RESTORE_BACKOFF_MS = [500, 1500, 4e3];
|
|
15910
|
+
function restoreBackoffMs(attempt) {
|
|
15911
|
+
const i = attempt < 0 ? 0 : Math.min(attempt, RESTORE_BACKOFF_MS.length - 1);
|
|
15912
|
+
return RESTORE_BACKOFF_MS[i];
|
|
15913
|
+
}
|
|
15914
|
+
function shouldRetryRestore(kind, attempt, maxRetries = RESTORE_MAX_RETRIES) {
|
|
15915
|
+
return kind === "transient" && attempt < maxRetries;
|
|
15916
|
+
}
|
|
15735
15917
|
|
|
15736
15918
|
function getShareBaseUrl() {
|
|
15737
15919
|
return getSvampWebBaseUrl();
|
|
@@ -18035,6 +18217,13 @@ function shouldRunZombieProbe(args) {
|
|
|
18035
18217
|
function nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures) {
|
|
18036
18218
|
return consecutiveHeartbeatFailures + 1;
|
|
18037
18219
|
}
|
|
18220
|
+
function shouldRunServiceProbe(args) {
|
|
18221
|
+
return !args.inGrace && !args.zombieProbeRan;
|
|
18222
|
+
}
|
|
18223
|
+
function decideDisconnectRecovery(args) {
|
|
18224
|
+
if (args.shutdownRequested) return "stay";
|
|
18225
|
+
return args.supervised ? "exit-for-respawn" : "stay";
|
|
18226
|
+
}
|
|
18038
18227
|
function shouldForceReconnect(consecutiveHeartbeatFailures) {
|
|
18039
18228
|
if (consecutiveHeartbeatFailures < 2) return false;
|
|
18040
18229
|
return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
|
|
@@ -20082,7 +20271,7 @@ async function startDaemon(options) {
|
|
|
20082
20271
|
try {
|
|
20083
20272
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
20084
20273
|
if (!dir) return;
|
|
20085
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
20274
|
+
const { reconcileServiceLinks } = await import('./agentCommands-DefL1pIo.mjs');
|
|
20086
20275
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
20087
20276
|
const config = readSvampConfig(configPath);
|
|
20088
20277
|
const entries = Array.from(urls.entries());
|
|
@@ -20104,7 +20293,7 @@ async function startDaemon(options) {
|
|
|
20104
20293
|
try {
|
|
20105
20294
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
20106
20295
|
if (!dir) return;
|
|
20107
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
20296
|
+
const { reconcileServiceLinks } = await import('./agentCommands-DefL1pIo.mjs');
|
|
20108
20297
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
20109
20298
|
const config = readSvampConfig(configPath);
|
|
20110
20299
|
const incoming = [{
|
|
@@ -20125,7 +20314,7 @@ async function startDaemon(options) {
|
|
|
20125
20314
|
try {
|
|
20126
20315
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
20127
20316
|
if (!dir) return;
|
|
20128
|
-
const { dropServiceLinks } = await import('./agentCommands-
|
|
20317
|
+
const { dropServiceLinks } = await import('./agentCommands-DefL1pIo.mjs');
|
|
20129
20318
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
20130
20319
|
const config = readSvampConfig(configPath);
|
|
20131
20320
|
if (dropServiceLinks(config, "serve", mountName)) {
|
|
@@ -20199,7 +20388,14 @@ async function startDaemon(options) {
|
|
|
20199
20388
|
supervised: process.env.SVAMP_SUPERVISED === "1"
|
|
20200
20389
|
});
|
|
20201
20390
|
server.on("disconnected", (reason) => {
|
|
20202
|
-
|
|
20391
|
+
const supervised2 = process.env.SVAMP_SUPERVISED === "1";
|
|
20392
|
+
const recovery = decideDisconnectRecovery({ supervised: supervised2, shutdownRequested });
|
|
20393
|
+
if (recovery === "exit-for-respawn") {
|
|
20394
|
+
logger.log(`Hypha connection permanently lost: ${reason}. Exiting for a clean supervised respawn (sessions restore via --resume).`);
|
|
20395
|
+
setTimeout(() => process.exit(1), 250);
|
|
20396
|
+
} else {
|
|
20397
|
+
logger.log(`Hypha connection permanently lost: ${reason}. Daemon continues running (unsupervised) \u2014 restart manually to reconnect.`);
|
|
20398
|
+
}
|
|
20203
20399
|
});
|
|
20204
20400
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
20205
20401
|
let sessionCoreRef = {
|
|
@@ -21388,15 +21584,19 @@ ${parts.join("\n")}`);
|
|
|
21388
21584
|
sessionService.updateAgentState({
|
|
21389
21585
|
controlledByUser: false,
|
|
21390
21586
|
requests: reqs,
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
|
|
21587
|
+
// #1041: bounded + slimmed. `arguments: toolInput` used to
|
|
21588
|
+
// be stored here — the whole tool input, e.g. a full Write
|
|
21589
|
+
// body — in a map that grew forever and is re-walked by the
|
|
21590
|
+
// reducer on every streamed batch.
|
|
21591
|
+
completedRequests: recordCompletedRequest(
|
|
21592
|
+
sessionService._agentState?.completedRequests,
|
|
21593
|
+
correlationId,
|
|
21594
|
+
{
|
|
21394
21595
|
tool: toolName,
|
|
21395
|
-
arguments: toolInput,
|
|
21396
21596
|
completedAt: Date.now(),
|
|
21397
21597
|
status: result.behavior === "allow" ? "approved" : "denied"
|
|
21398
21598
|
}
|
|
21399
|
-
|
|
21599
|
+
)
|
|
21400
21600
|
});
|
|
21401
21601
|
}).catch((err) => {
|
|
21402
21602
|
logger.log(`[Session ${sessionId}] Permission handler error (request ${requestId}): ${err}`);
|
|
@@ -22497,11 +22697,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
22497
22697
|
});
|
|
22498
22698
|
},
|
|
22499
22699
|
onIssue: async (params) => {
|
|
22500
|
-
const { issueRpc } = await import('./rpc-
|
|
22700
|
+
const { issueRpc } = await import('./rpc-CZqCc9Pr.mjs');
|
|
22501
22701
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
22502
22702
|
},
|
|
22503
22703
|
onWorkflow: async (params) => {
|
|
22504
|
-
const { workflowRpc } = await import('./rpc-
|
|
22704
|
+
const { workflowRpc } = await import('./rpc-BCLZpI5H.mjs');
|
|
22505
22705
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
22506
22706
|
},
|
|
22507
22707
|
onRipgrep: async (args, cwd) => {
|
|
@@ -23207,11 +23407,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
23207
23407
|
});
|
|
23208
23408
|
},
|
|
23209
23409
|
onIssue: async (params) => {
|
|
23210
|
-
const { issueRpc } = await import('./rpc-
|
|
23410
|
+
const { issueRpc } = await import('./rpc-CZqCc9Pr.mjs');
|
|
23211
23411
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
23212
23412
|
},
|
|
23213
23413
|
onWorkflow: async (params) => {
|
|
23214
|
-
const { workflowRpc } = await import('./rpc-
|
|
23414
|
+
const { workflowRpc } = await import('./rpc-BCLZpI5H.mjs');
|
|
23215
23415
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
23216
23416
|
},
|
|
23217
23417
|
onRipgrep: async (args, cwd) => {
|
|
@@ -24119,7 +24319,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24119
24319
|
const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
|
|
24120
24320
|
if (channelHttpPort > 0) {
|
|
24121
24321
|
try {
|
|
24122
|
-
const { createChannelHttpServer } = await import('./httpServer-
|
|
24322
|
+
const { createChannelHttpServer } = await import('./httpServer-Be1Hcddr.mjs');
|
|
24123
24323
|
const channelHttpServer = createChannelHttpServer({
|
|
24124
24324
|
getSessionIds: () => {
|
|
24125
24325
|
const ids = [];
|
|
@@ -24297,7 +24497,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24297
24497
|
}
|
|
24298
24498
|
logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
|
|
24299
24499
|
const restoreConcurrency = Math.max(1, parseInt(process.env.SVAMP_RESTORE_CONCURRENCY || "8", 10) || 8);
|
|
24300
|
-
const restoreOne = async (persisted) => {
|
|
24500
|
+
const restoreOne = async (persisted, attempt = 0) => {
|
|
24301
24501
|
try {
|
|
24302
24502
|
const isOrphaned = persisted.machineId && persisted.machineId !== machineId;
|
|
24303
24503
|
if (isOrphaned) {
|
|
@@ -24354,10 +24554,26 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24354
24554
|
sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
|
|
24355
24555
|
}
|
|
24356
24556
|
} else {
|
|
24357
|
-
|
|
24557
|
+
const reason = result.errorMessage || result.type;
|
|
24558
|
+
const kind = classifyRestoreFailure(reason);
|
|
24559
|
+
if (shouldRetryRestore(kind, attempt)) {
|
|
24560
|
+
const wait = restoreBackoffMs(attempt);
|
|
24561
|
+
logger.log(`Restore of ${persisted.sessionId} failed (${reason}) \u2014 transient, retrying in ${wait}ms (attempt ${attempt + 1}/${RESTORE_MAX_RETRIES})`);
|
|
24562
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
24563
|
+
return restoreOne(persisted, attempt + 1);
|
|
24564
|
+
}
|
|
24565
|
+
logger.log(`Failed to restore session ${persisted.sessionId}: ${reason} (${kind}; preserved on disk for a later restart)`);
|
|
24358
24566
|
}
|
|
24359
24567
|
} catch (err) {
|
|
24360
|
-
|
|
24568
|
+
const reason = err?.message || String(err);
|
|
24569
|
+
const kind = classifyRestoreFailure(reason);
|
|
24570
|
+
if (shouldRetryRestore(kind, attempt)) {
|
|
24571
|
+
const wait = restoreBackoffMs(attempt);
|
|
24572
|
+
logger.log(`Error restoring ${persisted.sessionId}: ${reason} \u2014 transient, retrying in ${wait}ms (attempt ${attempt + 1}/${RESTORE_MAX_RETRIES})`);
|
|
24573
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
24574
|
+
return restoreOne(persisted, attempt + 1);
|
|
24575
|
+
}
|
|
24576
|
+
logger.error(`Error restoring session ${persisted.sessionId} (${kind}, giving up this lifetime):`, reason);
|
|
24361
24577
|
}
|
|
24362
24578
|
};
|
|
24363
24579
|
let restoreCursor = 0;
|
|
@@ -24548,7 +24764,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24548
24764
|
const PING_TIMEOUT_MS = 15e3;
|
|
24549
24765
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
24550
24766
|
const RECONNECT_JITTER_MS = 2500;
|
|
24551
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
24767
|
+
const { WorkflowScheduler } = await import('./scheduler-s_j089nA.mjs');
|
|
24552
24768
|
const workflowProjectRoots = () => {
|
|
24553
24769
|
const dirs = /* @__PURE__ */ new Set();
|
|
24554
24770
|
for (const s of pidToTrackedSession.values()) {
|
|
@@ -24627,7 +24843,9 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24627
24843
|
const INBOUND_SILENCE_THRESHOLD_MS = 12e4;
|
|
24628
24844
|
const inboundSilenceMs = Date.now() - machineService.getLastInboundRpcAt();
|
|
24629
24845
|
const hasActiveSessions = pidToTrackedSession.size > 0;
|
|
24846
|
+
let zombieProbeRan = false;
|
|
24630
24847
|
if (shouldRunZombieProbe({ hasActiveSessions, inboundSilenceMs, consecutiveHeartbeatFailures, inGrace, thresholdMs: INBOUND_SILENCE_THRESHOLD_MS })) {
|
|
24848
|
+
zombieProbeRan = true;
|
|
24631
24849
|
logger.log(`No inbound RPC for ${Math.round(inboundSilenceMs / 1e3)}s with ${pidToTrackedSession.size} active session(s) \u2014 zombie probe`);
|
|
24632
24850
|
try {
|
|
24633
24851
|
const machineServiceId = `${server.config.client_id}:default`;
|
|
@@ -24640,7 +24858,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24640
24858
|
consecutiveHeartbeatFailures = nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures);
|
|
24641
24859
|
}
|
|
24642
24860
|
}
|
|
24643
|
-
if (
|
|
24861
|
+
if (shouldRunServiceProbe({ inGrace, zombieProbeRan })) {
|
|
24644
24862
|
try {
|
|
24645
24863
|
const pingStart = Date.now();
|
|
24646
24864
|
const machineServiceId = `${server.config.client_id}:default`;
|