svamp-cli 0.2.60 → 0.2.65

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.
@@ -3,12 +3,13 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
3
3
  import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync as unlinkSync$1, watch, rmdirSync, readdirSync } from 'fs';
4
4
  import path__default, { join, dirname, resolve, basename } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { spawn as spawn$1, execSync as execSync$1 } from 'child_process';
6
+ import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync, unlinkSync } from 'node:fs';
9
+ import { promisify } from 'util';
9
10
  import { randomUUID, createHash } from 'node:crypto';
10
11
  import { join as join$1 } from 'node:path';
11
- import { spawn, execSync, execFile, execFileSync } from 'node:child_process';
12
+ import { spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
12
13
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
13
14
  import os, { homedir, platform } from 'node:os';
14
15
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -16,7 +17,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
16
17
  import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
17
18
  import { z } from 'zod';
18
19
  import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
19
- import { promisify } from 'node:util';
20
+ import { promisify as promisify$1 } from 'node:util';
20
21
 
21
22
  let connectToServerFn = null;
22
23
  async function getConnectToServer() {
@@ -72,9 +73,10 @@ const ROLE_HIERARCHY = {
72
73
  admin: 2
73
74
  };
74
75
  function normalizeAllowedUser(input, addedBy) {
76
+ const role = input.role && input.role in ROLE_HIERARCHY ? input.role : "admin";
75
77
  return {
76
- email: input.email.trim(),
77
- role: "admin",
78
+ email: input.email.trim().toLowerCase(),
79
+ role,
78
80
  addedAt: typeof input.addedAt === "number" ? input.addedAt : Date.now(),
79
81
  addedBy: input.addedBy ?? addedBy
80
82
  };
@@ -88,7 +90,8 @@ function resolveRoleLevel(sharing, userEmail) {
88
90
  (u) => u.email.toLowerCase() === userEmail.toLowerCase()
89
91
  );
90
92
  if (sharedUser) {
91
- level = ROLE_HIERARCHY.admin;
93
+ const storedLevel = ROLE_HIERARCHY[sharedUser.role];
94
+ level = typeof storedLevel === "number" ? storedLevel : ROLE_HIERARCHY.admin;
92
95
  }
93
96
  }
94
97
  if (sharing.publicAccess) {
@@ -302,6 +305,98 @@ function applySecurityContext(baseConfig, context) {
302
305
  return config;
303
306
  }
304
307
 
308
+ const execFileAsync$1 = promisify(execFile);
309
+ function parseEtime(s) {
310
+ if (!s) return 0;
311
+ const dayParts = s.split("-");
312
+ let dayPrefix = 0;
313
+ let rest = s;
314
+ if (dayParts.length === 2) {
315
+ dayPrefix = parseInt(dayParts[0], 10) || 0;
316
+ rest = dayParts[1];
317
+ }
318
+ const parts = rest.split(":").map((p) => parseInt(p, 10) || 0);
319
+ let total = 0;
320
+ if (parts.length === 3) total = parts[0] * 3600 + parts[1] * 60 + parts[2];
321
+ else if (parts.length === 2) total = parts[0] * 60 + parts[1];
322
+ else if (parts.length === 1) total = parts[0];
323
+ return dayPrefix * 86400 + total;
324
+ }
325
+ async function readProcessTable() {
326
+ if (process.platform === "win32") return [];
327
+ let stdout;
328
+ try {
329
+ const result = await execFileAsync$1("ps", ["-A", "-o", "pid=,ppid=,etime=,%cpu=,rss=,command="], {
330
+ maxBuffer: 8 * 1024 * 1024
331
+ // 8MB — plenty for any realistic process table
332
+ });
333
+ stdout = result.stdout;
334
+ } catch {
335
+ return [];
336
+ }
337
+ const rows = [];
338
+ for (const line of stdout.split("\n")) {
339
+ const trimmed = line.trim();
340
+ if (!trimmed) continue;
341
+ const parts = trimmed.split(/\s+/);
342
+ if (parts.length < 6) continue;
343
+ const pid = parseInt(parts[0], 10);
344
+ const ppid = parseInt(parts[1], 10);
345
+ if (!pid || isNaN(ppid)) continue;
346
+ const etimeSec = parseEtime(parts[2]);
347
+ const cpu = parseFloat(parts[3]) || 0;
348
+ const rssKb = parseInt(parts[4], 10) || 0;
349
+ const command = parts.slice(5).join(" ");
350
+ rows.push({ pid, ppid, etimeSec, cpu, rssKb, command });
351
+ }
352
+ return rows;
353
+ }
354
+ async function detectDescendants(rootPid, excludePids = /* @__PURE__ */ new Set()) {
355
+ if (!rootPid || rootPid < 1) return [];
356
+ const table = await readProcessTable();
357
+ if (table.length === 0) return [];
358
+ const byPpid = /* @__PURE__ */ new Map();
359
+ for (const row of table) {
360
+ const list = byPpid.get(row.ppid);
361
+ if (list) list.push(row);
362
+ else byPpid.set(row.ppid, [row]);
363
+ }
364
+ const descendants = [];
365
+ const visited = /* @__PURE__ */ new Set([rootPid]);
366
+ const queue = [rootPid];
367
+ const MAX_NODES = 5e3;
368
+ while (queue.length > 0 && descendants.length < MAX_NODES) {
369
+ const next = queue.shift();
370
+ const children = byPpid.get(next);
371
+ if (!children) continue;
372
+ for (const child of children) {
373
+ if (visited.has(child.pid)) continue;
374
+ visited.add(child.pid);
375
+ queue.push(child.pid);
376
+ if (!excludePids.has(child.pid)) {
377
+ descendants.push(child);
378
+ }
379
+ }
380
+ }
381
+ descendants.sort((a, b) => a.etimeSec - b.etimeSec);
382
+ return descendants;
383
+ }
384
+ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
385
+ if (!rootPid || rootPid < 1) return { killed: false, reason: "no root pid" };
386
+ if (!targetPid || targetPid < 1) return { killed: false, reason: "invalid target pid" };
387
+ if (targetPid === rootPid) return { killed: false, reason: "cannot kill session root pid" };
388
+ const descendants = await detectDescendants(rootPid);
389
+ if (!descendants.some((d) => d.pid === targetPid)) {
390
+ return { killed: false, reason: "pid is not a descendant of this session" };
391
+ }
392
+ try {
393
+ process.kill(targetPid, signal);
394
+ return { killed: true };
395
+ } catch (err) {
396
+ return { killed: false, reason: err?.message ?? "kill failed" };
397
+ }
398
+ }
399
+
305
400
  function getParamNames(fn) {
306
401
  const src = fn.toString();
307
402
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -375,6 +470,20 @@ async function registerMachineService(server, machineId, metadata, daemonState,
375
470
  }
376
471
  }
377
472
  };
473
+ const ROLE_RANK = { view: 1, interact: 2, admin: 3 };
474
+ const authorizeSessionAccess = async (sessionId, requiredRole, context) => {
475
+ try {
476
+ authorizeRequest(context, currentMetadata.sharing, requiredRole);
477
+ return;
478
+ } catch {
479
+ }
480
+ const rpc = handlers.getSessionRPCHandlers?.(sessionId);
481
+ if (!rpc) throw new Error(`Session '${sessionId}' not found`);
482
+ const role = await rpc.getEffectiveRole(context).catch(() => null);
483
+ if (!role || (ROLE_RANK[role] ?? 0) < ROLE_RANK[requiredRole]) {
484
+ throw new Error(`Access denied: ${requiredRole} role required on session ${sessionId}`);
485
+ }
486
+ };
378
487
  const notifyListeners = (update) => {
379
488
  const snapshot = [...listeners];
380
489
  for (let i = snapshot.length - 1; i >= 0; i--) {
@@ -737,10 +846,22 @@ async function registerMachineService(server, machineId, metadata, daemonState,
737
846
  authorizeRequest(context, currentMetadata.sharing, "view");
738
847
  return { sharing: currentMetadata.sharing || null };
739
848
  },
849
+ /** Returns the caller's effective role on this machine (null if no access). */
850
+ getEffectiveRole: async (context) => {
851
+ authorizeRequest(context, currentMetadata.sharing, "view");
852
+ const role = getEffectiveRole(context, currentMetadata.sharing);
853
+ return { role };
854
+ },
740
855
  updateSharing: async (newSharing, context) => {
741
856
  authorizeRequest(context, currentMetadata.sharing, "admin");
742
- if (currentMetadata.sharing && context?.user?.email && currentMetadata.sharing.owner && context.user.email.toLowerCase() !== currentMetadata.sharing.owner.toLowerCase()) {
743
- throw new Error("Only the machine owner can update sharing settings");
857
+ const currentOwner = currentMetadata.sharing?.owner;
858
+ const callerEmail = context?.user?.email;
859
+ const callerIsOwner = !!currentOwner && !!callerEmail && callerEmail.toLowerCase() === currentOwner.toLowerCase();
860
+ if (currentOwner && newSharing.owner && newSharing.owner.toLowerCase() !== currentOwner.toLowerCase() && !callerIsOwner) {
861
+ throw new Error("Only the machine owner can transfer ownership");
862
+ }
863
+ if (currentOwner && !newSharing.owner) {
864
+ newSharing = { ...newSharing, owner: currentOwner };
744
865
  }
745
866
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
746
867
  newSharing = { ...newSharing, owner: context.user.email };
@@ -1096,6 +1217,126 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1096
1217
  if (!handlers.supervisor) return [];
1097
1218
  return handlers.supervisor.getLogs(params.idOrName, params.last ?? 50);
1098
1219
  },
1220
+ // ── Session-scoped Tasks panel ───────────────────────────────────
1221
+ // Frontend-facing views that filter by sessionId and also walk the
1222
+ // OS process tree to surface unsupervised child processes (e.g.
1223
+ // `node server.js &` spawned from a bash tool call). These are
1224
+ // separate from the global `process*` RPCs above so that a shared
1225
+ // user can manage their own session's tasks without machine-level
1226
+ // access.
1227
+ /**
1228
+ * Snapshot of everything the session's Tasks panel needs to render:
1229
+ * - supervised: ProcessSpec/state filtered to this session
1230
+ * - detected: descendants of the agent PID that aren't supervised
1231
+ */
1232
+ sessionProcessList: async (params, context) => {
1233
+ await authorizeSessionAccess(params.sessionId, "view", context);
1234
+ const supervisor = handlers.supervisor;
1235
+ const all = supervisor ? supervisor.list() : [];
1236
+ const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
1237
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1238
+ let detected = [];
1239
+ if (agentPid) {
1240
+ const supervisedPids = /* @__PURE__ */ new Set();
1241
+ for (const info of all) {
1242
+ if (info.state.pid) supervisedPids.add(info.state.pid);
1243
+ }
1244
+ detected = await detectDescendants(agentPid, supervisedPids);
1245
+ }
1246
+ return { supervised, detected, agentPid };
1247
+ },
1248
+ /**
1249
+ * Cheap counts for the input-bar badge — avoids the cost of a full
1250
+ * detection walk when the panel is closed. Counts are still
1251
+ * accurate enough to indicate "is anything running for this session".
1252
+ */
1253
+ sessionProcessCounts: async (params, context) => {
1254
+ await authorizeSessionAccess(params.sessionId, "view", context);
1255
+ const supervisor = handlers.supervisor;
1256
+ const all = supervisor ? supervisor.list() : [];
1257
+ const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
1258
+ const supervisedRunning = supervised.filter((p) => p.state.status === "running" || p.state.status === "starting").length;
1259
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1260
+ let detected = 0;
1261
+ if (agentPid) {
1262
+ const supervisedPids = /* @__PURE__ */ new Set();
1263
+ for (const info of all) if (info.state.pid) supervisedPids.add(info.state.pid);
1264
+ const list = await detectDescendants(agentPid, supervisedPids);
1265
+ detected = list.length;
1266
+ }
1267
+ return { supervised: supervised.length, supervisedRunning, detected, agentPid };
1268
+ },
1269
+ /** Logs for a supervised process, gated by session ownership. */
1270
+ sessionProcessLogs: async (params, context) => {
1271
+ await authorizeSessionAccess(params.sessionId, "view", context);
1272
+ if (!handlers.supervisor) return [];
1273
+ const info = handlers.supervisor.get(params.idOrName);
1274
+ if (!info || info.spec.sessionId !== params.sessionId) return [];
1275
+ return handlers.supervisor.getLogs(params.idOrName, params.last ?? 100);
1276
+ },
1277
+ /**
1278
+ * Lifecycle actions on a session-bound supervised process.
1279
+ * `remove` requires admin; start/stop/restart require interact.
1280
+ */
1281
+ sessionProcessAction: async (params, context) => {
1282
+ const requiredRole = params.action === "remove" ? "admin" : "interact";
1283
+ await authorizeSessionAccess(params.sessionId, requiredRole, context);
1284
+ if (!handlers.supervisor) throw new Error("Process supervisor not available");
1285
+ const info = handlers.supervisor.get(params.idOrName);
1286
+ if (!info) throw new Error(`Process '${params.idOrName}' not found`);
1287
+ if (info.spec.sessionId !== params.sessionId) {
1288
+ throw new Error(`Process '${params.idOrName}' does not belong to this session`);
1289
+ }
1290
+ switch (params.action) {
1291
+ case "start":
1292
+ return handlers.supervisor.start(params.idOrName);
1293
+ case "stop":
1294
+ return handlers.supervisor.stop(params.idOrName);
1295
+ case "restart":
1296
+ return handlers.supervisor.restart(params.idOrName);
1297
+ case "remove":
1298
+ return handlers.supervisor.remove(params.idOrName);
1299
+ }
1300
+ },
1301
+ /**
1302
+ * Kill an unsupervised descendant of the agent. Verified by the
1303
+ * detection helper so callers can only target PIDs that are
1304
+ * actually children of this session.
1305
+ */
1306
+ sessionProcessKill: async (params, context) => {
1307
+ await authorizeSessionAccess(params.sessionId, "admin", context);
1308
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1309
+ if (!agentPid) return { killed: false, reason: "no agent pid for session" };
1310
+ return killDescendant(agentPid, Number(params.pid), params.signal ?? "SIGTERM");
1311
+ },
1312
+ /**
1313
+ * Promote a detected child into supervision. Kills the original
1314
+ * PID and re-spawns the same command under the supervisor, so the
1315
+ * new process is properly tracked (PID re-parenting isn't
1316
+ * achievable without ptrace).
1317
+ */
1318
+ sessionProcessAdopt: async (params, context) => {
1319
+ await authorizeSessionAccess(params.sessionId, "admin", context);
1320
+ if (!handlers.supervisor) throw new Error("Process supervisor not available");
1321
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1322
+ if (!agentPid) throw new Error("No agent pid for session");
1323
+ if (params.pid && params.pid > 0) {
1324
+ const res = await killDescendant(agentPid, Number(params.pid), "SIGTERM");
1325
+ if (!res.killed && res.reason !== "pid is not a descendant of this session") ;
1326
+ }
1327
+ const spec = {
1328
+ name: params.name,
1329
+ command: params.command,
1330
+ args: params.args ?? [],
1331
+ workdir: params.workdir ?? process.cwd(),
1332
+ keepAlive: params.keepAlive ?? false,
1333
+ maxRestarts: 0,
1334
+ restartDelay: 2,
1335
+ sessionId: params.sessionId
1336
+ };
1337
+ const result = await handlers.supervisor.apply(spec);
1338
+ return result.info;
1339
+ },
1099
1340
  // ── Service / Tunnel management ──────────────────────────────────
1100
1341
  /** List active tunnels (replaces the old agent-sandbox serviceList). */
1101
1342
  serviceList: async (context) => {
@@ -1804,8 +2045,14 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1804
2045
  },
1805
2046
  updateSharing: async (newSharing, context) => {
1806
2047
  authorizeRequest(context, metadata.sharing, "admin");
1807
- if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
1808
- throw new Error("Only the session owner can update sharing settings");
2048
+ const currentOwner = metadata.sharing?.owner;
2049
+ const callerEmail = context?.user?.email;
2050
+ const callerIsOwner = !!currentOwner && !!callerEmail && callerEmail.toLowerCase() === currentOwner.toLowerCase();
2051
+ if (currentOwner && newSharing.owner && newSharing.owner.toLowerCase() !== currentOwner.toLowerCase() && !callerIsOwner) {
2052
+ throw new Error("Only the session owner can transfer ownership");
2053
+ }
2054
+ if (currentOwner && !newSharing.owner) {
2055
+ newSharing = { ...newSharing, owner: currentOwner };
1809
2056
  }
1810
2057
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
1811
2058
  newSharing = { ...newSharing, owner: context.user.email };
@@ -1828,12 +2075,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1828
2075
  return { success: true, sharing: newSharing };
1829
2076
  },
1830
2077
  /** Update security context and restart the agent process with new rules.
1831
- * Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely. */
2078
+ * Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely.
2079
+ * Admin tier (owner or admin-shared user) can perform this. */
1832
2080
  updateSecurityContext: async (newSecurityContext, context) => {
1833
2081
  authorizeRequest(context, metadata.sharing, "admin");
1834
- if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
1835
- throw new Error("Only the session owner can update security context");
1836
- }
1837
2082
  if (!callbacks.onUpdateSecurityContext) {
1838
2083
  throw new Error("Security context updates are not supported for this session");
1839
2084
  }
@@ -4655,7 +4900,7 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
4655
4900
  GeminiTransport: GeminiTransport
4656
4901
  });
4657
4902
 
4658
- const execFileAsync = promisify(execFile);
4903
+ const execFileAsync = promisify$1(execFile$1);
4659
4904
  const SVAMP_TOOLS_DIR = join$1(homedir(), ".svamp", "tools");
4660
4905
  const SVAMP_BIN_DIR = join$1(SVAMP_TOOLS_DIR, "bin");
4661
4906
  async function checkCommand(command, versionArgs) {
@@ -5401,9 +5646,13 @@ class ServeAuth {
5401
5646
  }
5402
5647
  /**
5403
5648
  * Check if a user email is authorized for a mount's access level.
5649
+ * Note: 'link' mounts are gated by the auth proxy at the path-segment
5650
+ * level (capability token), not by this method — they should never reach
5651
+ * here. We treat them as deny-by-default just in case.
5404
5652
  */
5405
5653
  isAuthorized(email, access, ownerEmail) {
5406
5654
  if (access === "public") return true;
5655
+ if (access === "link") return false;
5407
5656
  if (!email) return false;
5408
5657
  if (access === "owner") {
5409
5658
  return !!ownerEmail && email.toLowerCase() === ownerEmail.toLowerCase();
@@ -6156,6 +6405,71 @@ You may be running in parallel with other agents \u2014 possibly sharing the sam
6156
6405
  `;
6157
6406
  }
6158
6407
 
6408
+ const STANDARD_WINDOWS = [2e5, 1e6];
6409
+ function readWindow(entry) {
6410
+ if (!entry) return 0;
6411
+ const cw = entry.contextWindow ?? entry.context_window;
6412
+ return typeof cw === "number" && cw > 0 && Number.isFinite(cw) ? cw : 0;
6413
+ }
6414
+ function computeObservedContext(usage) {
6415
+ if (!usage) return 0;
6416
+ return (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
6417
+ }
6418
+ function inferWindowFromModelName(model) {
6419
+ if (!model) return 0;
6420
+ if (/\[1m\]/i.test(model)) return 1e6;
6421
+ if (/opus-?4-?[7-9]/i.test(model)) return 1e6;
6422
+ if (/opus-?[5-9]/i.test(model)) return 1e6;
6423
+ if (/sonnet-?4-?[5-9]/i.test(model)) return 1e6;
6424
+ if (/sonnet-?[5-9]/i.test(model)) return 1e6;
6425
+ if (/opus|sonnet/i.test(model)) return 2e5;
6426
+ if (/haiku/i.test(model)) return 2e5;
6427
+ return 0;
6428
+ }
6429
+ function matchedMainModel(modelUsage, mainModel) {
6430
+ if (!modelUsage || !mainModel) return { window: 0, matched: false };
6431
+ const variants = [
6432
+ mainModel,
6433
+ `${mainModel}[1m]`,
6434
+ mainModel.replace(/\[1m\]$/i, "")
6435
+ ];
6436
+ for (const k of variants) {
6437
+ const cw = readWindow(modelUsage[k]);
6438
+ if (cw > 0) return { window: cw, matched: true };
6439
+ }
6440
+ return { window: 0, matched: false };
6441
+ }
6442
+ function maxAcrossModelUsage(modelUsage) {
6443
+ if (!modelUsage) return 0;
6444
+ let best = 0;
6445
+ for (const v of Object.values(modelUsage)) {
6446
+ const cw = readWindow(v);
6447
+ if (cw > best) best = cw;
6448
+ }
6449
+ return best;
6450
+ }
6451
+ function selfHealForObserved(candidate, observed) {
6452
+ if (observed <= candidate) return candidate;
6453
+ const next = STANDARD_WINDOWS.find((w) => w >= observed);
6454
+ if (next) return Math.max(candidate, next);
6455
+ return Math.max(candidate, Math.ceil(observed * 1.25));
6456
+ }
6457
+ function resolveContextWindow(opts) {
6458
+ const { modelUsage, usage, mainModel, currentWindow } = opts;
6459
+ const observed = computeObservedContext(usage);
6460
+ const main = matchedMainModel(modelUsage, mainModel);
6461
+ let candidate = main.window;
6462
+ const definitiveMatch = main.matched;
6463
+ if (!candidate) candidate = maxAcrossModelUsage(modelUsage);
6464
+ if (!candidate) candidate = inferWindowFromModelName(mainModel);
6465
+ candidate = selfHealForObserved(candidate, observed);
6466
+ const current = typeof currentWindow === "number" && currentWindow > 0 ? currentWindow : 0;
6467
+ if (current > 0 && candidate > 0 && candidate < current && !definitiveMatch) {
6468
+ candidate = current;
6469
+ }
6470
+ return candidate;
6471
+ }
6472
+
6159
6473
  const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
6160
6474
  function generateHookSettings(portOrOptions = {}) {
6161
6475
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
@@ -7441,7 +7755,7 @@ async function startDaemon(options) {
7441
7755
  const list = loadExposedTunnels().filter((t) => t.name !== name);
7442
7756
  saveExposedTunnels(list);
7443
7757
  }
7444
- const { ServeManager } = await import('./serveManager-C9pzi-2O.mjs');
7758
+ const { ServeManager } = await import('./serveManager-pDviHaH8.mjs');
7445
7759
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
7446
7760
  ensureAutoInstalledSkills(logger).catch(() => {
7447
7761
  });
@@ -7645,6 +7959,7 @@ async function startDaemon(options) {
7645
7959
  let lastSpawnMeta = persisted?.spawnMeta || {};
7646
7960
  let sessionWasProcessing = !!options2.wasProcessing;
7647
7961
  let lastAssistantText = "";
7962
+ let lastMainModel;
7648
7963
  let consecutiveRalphErrors = 0;
7649
7964
  const MAX_RALPH_ERRORS = 3;
7650
7965
  let spawnHasReceivedInit = false;
@@ -7738,6 +8053,9 @@ async function startDaemon(options) {
7738
8053
  let isKillingClaude = false;
7739
8054
  let isRestartingClaude = false;
7740
8055
  let isSwitchingMode = false;
8056
+ const OVERLOAD_BAIL_THRESHOLD = 3;
8057
+ let consecutiveOverloadRetries = 0;
8058
+ let overloadBailedThisTurn = false;
7741
8059
  let checkSvampConfig;
7742
8060
  let cleanupSvampConfig;
7743
8061
  const VALID_CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
@@ -7969,6 +8287,12 @@ async function startDaemon(options) {
7969
8287
  "event"
7970
8288
  );
7971
8289
  }
8290
+ if (msg.type === "assistant") {
8291
+ const assistantModel = msg.message?.model;
8292
+ if (typeof assistantModel === "string" && assistantModel.length > 0) {
8293
+ lastMainModel = assistantModel;
8294
+ }
8295
+ }
7972
8296
  const assistantContent = msg.type === "assistant" ? msg.message?.content ?? msg.content : void 0;
7973
8297
  if (Array.isArray(assistantContent)) {
7974
8298
  for (const block of assistantContent) {
@@ -7993,13 +8317,21 @@ async function startDaemon(options) {
7993
8317
  logger.log(`[Session ${sessionId}] Startup failure detected \u2014 scheduling silent retry without --resume`);
7994
8318
  startupFailureRetryPending = true;
7995
8319
  lastErrorMessagePushed = true;
8320
+ } else if (overloadBailedThisTurn) {
8321
+ logger.log(`[Session ${sessionId}] Suppressing duplicate error \u2014 overload hint already pushed for this turn`);
8322
+ lastErrorMessagePushed = true;
7996
8323
  } else {
7997
8324
  const lower = resultText.toLowerCase();
8325
+ const isOverload = msg.api_error_status === 529 || lower.includes("529") || lower.includes("overload");
7998
8326
  const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
7999
8327
  const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
8000
8328
  const isBillingIssue = lower.includes("credit") || lower.includes("balance") || lower.includes("billing") || lower.includes("quota") || lower.includes("subscription") || lower.includes("payment");
8001
8329
  let hint = "";
8002
- if (isBillingIssue) {
8330
+ if (isOverload) {
8331
+ const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
8332
+ const proxyHint = onHyphaProxy ? "" : "\n\u2022 Switch the daemon to the Hypha proxy (rotates across accounts on 529): `svamp daemon auth use-hypha-proxy && svamp daemon restart`";
8333
+ hint = "\n\nAnthropic returned HTTP 529 Overloaded \u2014 server-side capacity, not your account. This session may be pinned to a hot backend shard.\n\u2022 Start a fresh session (different conversation id \u2192 different shard).\n\u2022 Wait a minute and resend.\n\u2022 Check https://status.claude.com." + proxyHint;
8334
+ } else if (isBillingIssue) {
8003
8335
  hint = "\n\nCheck your Claude account credits or subscription at https://console.anthropic.com.";
8004
8336
  } else if (isLoginIssue) {
8005
8337
  checkAndRefreshOAuthToken(true, logger).then((r) => {
@@ -8064,15 +8396,15 @@ async function startDaemon(options) {
8064
8396
  }
8065
8397
  signalProcessing(false);
8066
8398
  sessionWasProcessing = false;
8067
- if (msg.modelUsage && typeof msg.modelUsage === "object") {
8068
- const modelKeys = Object.keys(msg.modelUsage);
8069
- if (modelKeys.length > 0) {
8070
- const firstModel = msg.modelUsage[modelKeys[0]];
8071
- if (firstModel?.contextWindow && firstModel.contextWindow !== sessionMetadata.contextWindow) {
8072
- sessionMetadata = { ...sessionMetadata, contextWindow: firstModel.contextWindow };
8073
- sessionService.updateMetadata(sessionMetadata);
8074
- }
8075
- }
8399
+ const resolvedWindow = resolveContextWindow({
8400
+ modelUsage: msg.modelUsage,
8401
+ usage: msg.usage,
8402
+ mainModel: lastMainModel,
8403
+ currentWindow: sessionMetadata.contextWindow
8404
+ });
8405
+ if (resolvedWindow > 0 && resolvedWindow !== sessionMetadata.contextWindow) {
8406
+ sessionMetadata = { ...sessionMetadata, contextWindow: resolvedWindow };
8407
+ sessionService.updateMetadata(sessionMetadata);
8076
8408
  }
8077
8409
  if (claudeResumeId && !trackedSession.stopped) {
8078
8410
  saveSession({
@@ -8287,6 +8619,8 @@ The automated loop has finished. Review the progress above and let me know if yo
8287
8619
  sessionService.pushMessage(msg, "agent");
8288
8620
  } else if (msg.type === "system" && msg.subtype === "init") {
8289
8621
  lastAssistantText = "";
8622
+ consecutiveOverloadRetries = 0;
8623
+ overloadBailedThisTurn = false;
8290
8624
  if (!userMessagePending) {
8291
8625
  turnInitiatedByUser = false;
8292
8626
  logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
@@ -8295,6 +8629,9 @@ The automated loop has finished. Review the progress above and let me know if yo
8295
8629
  if (msg.slash_commands && Array.isArray(msg.slash_commands)) {
8296
8630
  sessionMetadata = { ...sessionMetadata, slashCommands: msg.slash_commands };
8297
8631
  }
8632
+ if (typeof msg.model === "string" && msg.model.length > 0) {
8633
+ lastMainModel = msg.model;
8634
+ }
8298
8635
  if (msg.session_id) {
8299
8636
  const isResumeFailure = !spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
8300
8637
  const isConversationClear = spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
@@ -8331,6 +8668,34 @@ The automated loop has finished. Review the progress above and let me know if yo
8331
8668
  sessionService.updateMetadata(sessionMetadata);
8332
8669
  }
8333
8670
  sessionService.pushMessage(msg, "session");
8671
+ } else if (msg.type === "system" && msg.subtype === "api_retry") {
8672
+ if (msg.error_status === 529) {
8673
+ consecutiveOverloadRetries++;
8674
+ if (consecutiveOverloadRetries >= OVERLOAD_BAIL_THRESHOLD && !overloadBailedThisTurn) {
8675
+ overloadBailedThisTurn = true;
8676
+ const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
8677
+ const proxyHint = onHyphaProxy ? "" : "\n\u2022 Switch the daemon to the Hypha proxy (which rotates across accounts on 529): `svamp daemon auth use-hypha-proxy && svamp daemon restart`";
8678
+ const hint = `Anthropic returned HTTP 529 Overloaded ${consecutiveOverloadRetries} times in a row. This is server-side capacity, not your account \u2014 typically a hot backend shard pinned to this session. Stopping early so you don't wait for the SDK's full ~4 min retry storm.
8679
+
8680
+ **Try:**
8681
+ \u2022 Start a fresh session (different conversation id \u2192 different shard).
8682
+ \u2022 Wait a minute and resend.
8683
+ \u2022 Check https://status.claude.com.` + proxyHint;
8684
+ logger.log(`[Session ${sessionId}] Bailing on ${consecutiveOverloadRetries}\xD7 consecutive 529 retries \u2014 sending interrupt`);
8685
+ sessionService.pushMessage({ type: "message", message: hint, level: "warning" }, "event");
8686
+ try {
8687
+ if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
8688
+ const interruptMsg = JSON.stringify({ type: "control_request", request: { type: "interrupt" } });
8689
+ claudeProcess.stdin.write(interruptMsg + "\n");
8690
+ }
8691
+ } catch (err) {
8692
+ logger.log(`[Session ${sessionId}] Failed to send overload interrupt: ${err.message}`);
8693
+ }
8694
+ }
8695
+ } else {
8696
+ consecutiveOverloadRetries = 0;
8697
+ }
8698
+ sessionService.pushMessage(msg, "agent");
8334
8699
  } else if (msg.type === "system" && msg.subtype === "task_notification" && msg.status === "completed") {
8335
8700
  backgroundTaskCount = Math.max(0, backgroundTaskCount - 1);
8336
8701
  if (backgroundTaskNames.length > 0) {
@@ -9820,6 +10185,14 @@ The automated loop has finished. Review the progress above and let me know if yo
9820
10185
  }
9821
10186
  return ids;
9822
10187
  },
10188
+ getSessionPid: (sessionId) => {
10189
+ for (const [, session] of pidToTrackedSession) {
10190
+ if (session.svampSessionId === sessionId && !session.stopped) {
10191
+ return session.pid;
10192
+ }
10193
+ }
10194
+ return void 0;
10195
+ },
9823
10196
  supervisor,
9824
10197
  tunnels,
9825
10198
  serveManager,
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
5
- import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-Dhliie9Z.mjs';
5
+ import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-EPzdDXeY.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -13,6 +13,7 @@ import 'path';
13
13
  import 'url';
14
14
  import 'child_process';
15
15
  import 'crypto';
16
+ import 'util';
16
17
  import '@agentclientprotocol/sdk';
17
18
  import '@modelcontextprotocol/sdk/client/index.js';
18
19
  import '@modelcontextprotocol/sdk/client/stdio.js';