svamp-cli 0.2.307 → 0.2.309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/{adminCommands-DZ9MECkY.mjs → adminCommands-BTLHQXmA.mjs} +2 -2
  2. package/dist/{agentCommands-D68wBf-y.mjs → agentCommands-DdNUACV8.mjs} +6 -6
  3. package/dist/{auth-DDgr4YFE.mjs → auth-Bn8TbaOP.mjs} +2 -2
  4. package/dist/{cli-Bk6IPB9M.mjs → cli-BaDWWDiz.mjs} +76 -70
  5. package/dist/cli.mjs +3 -3
  6. package/dist/{commands-Dh2S9Gf1.mjs → commands-2uOorIjB.mjs} +2 -2
  7. package/dist/{commands-BlIR3KBq.mjs → commands-BPgxgkXW.mjs} +3 -3
  8. package/dist/{commands-CJVqMnNf.mjs → commands-BTN9XxVx.mjs} +3 -2
  9. package/dist/{commands-rGFSTXKu.mjs → commands-Bb0oF3eZ.mjs} +37 -6
  10. package/dist/{commands-Bl9AtWkD.mjs → commands-C2u988x1.mjs} +3 -3
  11. package/dist/{commands-DsfIIri_.mjs → commands-DGbLwT_h.mjs} +23 -11
  12. package/dist/{commands-DJwSJOPD.mjs → commands-DPvy0iVT.mjs} +2 -2
  13. package/dist/{commands-CESl13XD.mjs → commands-r86haNJD.mjs} +3 -3
  14. package/dist/{fleet-okzhGLEY.mjs → fleet-CeLE0t97.mjs} +40 -4
  15. package/dist/{frpc-NDcBtpwa.mjs → frpc-Cv6J2Mex.mjs} +5 -2
  16. package/dist/{headlessCli-BkU5B5Jp.mjs → headlessCli-BXc9Fvzb.mjs} +3 -3
  17. package/dist/index.mjs +2 -2
  18. package/dist/{notifyCommands-Oh0kvQgj.mjs → notifyCommands-iVnI_Lb2.mjs} +2 -2
  19. package/dist/{package-k1G6ygGP.mjs → package-CeseSmYI.mjs} +2 -2
  20. package/dist/{pinnedClaudeCode-BaMR97BE.mjs → pinnedClaudeCode-C6MSkqS9.mjs} +1 -1
  21. package/dist/{rpc-DSyBSuTF.mjs → rpc-BwxwusfB.mjs} +2 -2
  22. package/dist/{rpc-K5xzddMF.mjs → rpc-CkcBAtcs.mjs} +2 -2
  23. package/dist/{run-09G0RL2B.mjs → run-2Stz5l7J.mjs} +2 -2
  24. package/dist/{run-DYVmYsvg.mjs → run-DLG-_xa7.mjs} +471 -94
  25. package/dist/{scheduler-CtKlSyIq.mjs → scheduler-CurN4UtP.mjs} +5 -5
  26. package/dist/{serveCommands-BnOmV2Z3.mjs → serveCommands-CtVZhR-V.mjs} +5 -5
  27. package/dist/{sideband-CdJ0O6L6.mjs → sideband-C_3PVGh-.mjs} +2 -2
  28. package/package.json +2 -2
@@ -18,9 +18,9 @@ import os, { homedir as homedir$1, platform } from 'node:os';
18
18
  import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
19
19
  import { EventEmitter } from 'node:events';
20
20
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
21
+ import { promisify as promisify$1 } from 'node:util';
21
22
  import { createInterface } from 'node:readline';
22
23
  import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1, rename as rename$1 } from 'node:fs/promises';
23
- import { promisify as promisify$1 } from 'node:util';
24
24
  import { parse, stringify } from 'yaml';
25
25
 
26
26
  let connectToServerFn = null;
@@ -2172,6 +2172,61 @@ function killOrphanedCaddy(log) {
2172
2172
  } catch {
2173
2173
  }
2174
2174
  }
2175
+ function reapOrphanedManagedProcesses(pidDir, log) {
2176
+ if (process.platform === "win32") return;
2177
+ let files;
2178
+ try {
2179
+ files = fs.readdirSync(pidDir).filter((f) => f.endsWith(".pid"));
2180
+ } catch {
2181
+ return;
2182
+ }
2183
+ if (!files.length) return;
2184
+ const cmdByPid = /* @__PURE__ */ new Map();
2185
+ try {
2186
+ const out = execSync("ps -axo pid=,command=", { encoding: "utf-8" });
2187
+ for (const line of out.split("\n")) {
2188
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
2189
+ if (m) cmdByPid.set(m[1], m[2]);
2190
+ }
2191
+ } catch {
2192
+ }
2193
+ let reaped = 0;
2194
+ for (const f of files) {
2195
+ const full = path.join(pidDir, f);
2196
+ let rec = {};
2197
+ try {
2198
+ rec = JSON.parse(fs.readFileSync(full, "utf-8"));
2199
+ } catch {
2200
+ }
2201
+ const pid = Number(rec.pid);
2202
+ if (pid > 0 && pid !== process.pid) {
2203
+ const liveCmd = cmdByPid.get(String(pid));
2204
+ const base = rec.command ? path.basename(rec.command) : "";
2205
+ const args = Array.isArray(rec.args) ? rec.args : [];
2206
+ const argOk = args.length === 0 || args.some((a) => a && liveCmd?.includes(a));
2207
+ if (liveCmd && base && liveCmd.includes(base) && argOk) {
2208
+ try {
2209
+ process.kill(-pid, "SIGKILL");
2210
+ reaped++;
2211
+ } catch {
2212
+ try {
2213
+ process.kill(pid, "SIGKILL");
2214
+ reaped++;
2215
+ } catch {
2216
+ }
2217
+ }
2218
+ log(`Reaped orphaned managed process pid=${pid} (port ${rec.port ?? "?"}) from a prior daemon crash (#0758).`);
2219
+ } else if (liveCmd) {
2220
+ log(`Managed pidfile ${f}: live pid=${pid} command does not match recorded '${rec.command} ${args.join(" ")}' \u2014 NOT reaping (likely PID reuse); dropping stale pidfile.`);
2221
+ }
2222
+ }
2223
+ try {
2224
+ fs.unlinkSync(full);
2225
+ } catch {
2226
+ }
2227
+ }
2228
+ if (reaped) log(`Reaped ${reaped} orphaned managed process(es) on startup (#0758).`);
2229
+ }
2175
2230
  const MOUNT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
2176
2231
  function validateMountName(name) {
2177
2232
  if (!name || name.length > 128) {
@@ -2196,13 +2251,19 @@ class ServeManager {
2196
2251
  auth = null;
2197
2252
  /** Live child processes for managed mounts. Keyed by mount name. */
2198
2253
  managedProcs = /* @__PURE__ */ new Map();
2254
+ /** #0783: per-mount 'stopping' barriers — a wake (ensureManagedRunning) awaits any pending stop
2255
+ * so it never spawns a replacement on a port the terminating process still holds (EADDRINUSE). */
2256
+ stoppingProcs = /* @__PURE__ */ new Map();
2199
2257
  /** Single timer that scans managed mounts every 30s for idle eviction. */
2200
2258
  idleTimer = null;
2201
2259
  persistFile;
2260
+ /** #0758: dir holding per-managed-mount pidfiles so a crash-orphaned process is reapable on restart. */
2261
+ managedPidDir;
2202
2262
  log;
2203
2263
  hyphaServerUrl;
2204
2264
  constructor(svampHome, logger, hyphaServerUrl) {
2205
2265
  this.persistFile = path.join(svampHome, "serve-mounts.json");
2266
+ this.managedPidDir = path.join(svampHome, "serve-managed");
2206
2267
  this.log = logger || ((msg) => console.log(`[SERVE] ${msg}`));
2207
2268
  const resolvedServerUrl = hyphaServerUrl || getHyphaServerUrl();
2208
2269
  if (!resolvedServerUrl) {
@@ -2443,6 +2504,13 @@ class ServeManager {
2443
2504
  async ensureManagedRunning(name) {
2444
2505
  const mount = this.mounts.get(name);
2445
2506
  if (!mount?.process) return;
2507
+ const pendingStop = this.stoppingProcs.get(name);
2508
+ if (pendingStop) {
2509
+ try {
2510
+ await pendingStop;
2511
+ } catch {
2512
+ }
2513
+ }
2446
2514
  const handle = this.managedProcs.get(name);
2447
2515
  if (handle && handle.warmupPromise) {
2448
2516
  return handle.warmupPromise;
@@ -2474,7 +2542,10 @@ class ServeManager {
2474
2542
  child.on("exit", (code, signal) => {
2475
2543
  this.log(`Managed process '${name}' exited (code=${code}, signal=${signal})`);
2476
2544
  const h = this.managedProcs.get(name);
2477
- if (h && h.child === child) this.managedProcs.delete(name);
2545
+ if (h && h.child === child) {
2546
+ this.managedProcs.delete(name);
2547
+ this.removeManagedPidFile(name);
2548
+ }
2478
2549
  });
2479
2550
  const warmupPath = cfg.warmupPath ?? "/";
2480
2551
  const warmupTimeoutMs = cfg.warmupTimeoutMs ?? 3e4;
@@ -2500,6 +2571,7 @@ class ServeManager {
2500
2571
  warmupPromise
2501
2572
  };
2502
2573
  this.managedProcs.set(name, newHandle);
2574
+ this.writeManagedPidFile(name, child, cfg);
2503
2575
  this.log(`Managed process '${name}' starting: ${cfg.command} ${(cfg.args ?? []).join(" ")} (port ${cfg.port})`);
2504
2576
  return warmupPromise;
2505
2577
  }
@@ -2509,6 +2581,25 @@ class ServeManager {
2509
2581
  * that a direct child.kill() would orphan, leaving the port bound. Falls back to a direct
2510
2582
  * child.kill() if the group signal fails (e.g. pid already gone).
2511
2583
  */
2584
+ // ── #0758: managed-process pidfiles (crash-orphan reaping) ──────────────
2585
+ managedPidFile(name) {
2586
+ const safe = name.replace(/[^A-Za-z0-9._-]/g, "_");
2587
+ return path.join(this.managedPidDir, `${safe}.pid`);
2588
+ }
2589
+ writeManagedPidFile(name, child, cfg) {
2590
+ try {
2591
+ fs.mkdirSync(this.managedPidDir, { recursive: true, mode: 448 });
2592
+ const rec = { pid: child.pid ?? 0, command: cfg.command, args: cfg.args ?? [], port: cfg.port, startedAt: Date.now() };
2593
+ fs.writeFileSync(this.managedPidFile(name), JSON.stringify(rec), { mode: 384 });
2594
+ } catch {
2595
+ }
2596
+ }
2597
+ removeManagedPidFile(name) {
2598
+ try {
2599
+ fs.unlinkSync(this.managedPidFile(name));
2600
+ } catch {
2601
+ }
2602
+ }
2512
2603
  killManagedTree(child, signal) {
2513
2604
  const pid = child.pid;
2514
2605
  if (pid && pid > 0) {
@@ -2558,18 +2649,30 @@ class ServeManager {
2558
2649
  const h = this.managedProcs.get(name);
2559
2650
  if (!h) return;
2560
2651
  this.managedProcs.delete(name);
2652
+ this.removeManagedPidFile(name);
2561
2653
  const child = h.child;
2562
- this.killManagedTree(child, "SIGTERM");
2563
- await new Promise((resolve) => {
2564
- const t = setTimeout(() => {
2565
- this.killManagedTree(child, "SIGKILL");
2566
- resolve();
2567
- }, 5e3);
2568
- child.once("exit", () => {
2569
- clearTimeout(t);
2570
- resolve();
2654
+ const done = (async () => {
2655
+ if (child.exitCode !== null) return;
2656
+ this.killManagedTree(child, "SIGTERM");
2657
+ await new Promise((resolve) => {
2658
+ const onExit = () => {
2659
+ clearTimeout(t);
2660
+ clearTimeout(cap);
2661
+ resolve();
2662
+ };
2663
+ const t = setTimeout(() => {
2664
+ this.killManagedTree(child, "SIGKILL");
2665
+ }, 5e3);
2666
+ const cap = setTimeout(onExit, 8e3);
2667
+ child.once("exit", onExit);
2571
2668
  });
2572
- });
2669
+ })();
2670
+ this.stoppingProcs.set(name, done);
2671
+ try {
2672
+ await done;
2673
+ } finally {
2674
+ if (this.stoppingProcs.get(name) === done) this.stoppingProcs.delete(name);
2675
+ }
2573
2676
  this.log(`Managed process '${name}' stopped`);
2574
2677
  }
2575
2678
  /** Idle eviction loop — stops processes that have been idle longer than configured. */
@@ -2631,6 +2734,7 @@ class ServeManager {
2631
2734
  async ensureRunning() {
2632
2735
  if (this.proxyServer) return;
2633
2736
  killOrphanedCaddy((m) => this.log(m));
2737
+ reapOrphanedManagedProcesses(this.managedPidDir, (m) => this.log(m));
2634
2738
  this.port = await tryReservePort(this.persistedPort);
2635
2739
  if (this.persistedPort && this.port !== this.persistedPort) {
2636
2740
  this.log(`\u26A0 Previous serve port ${this.persistedPort} unavailable \u2014 using ${this.port}. Downstream configs referencing the old port will need updating.`);
@@ -3047,7 +3151,7 @@ Connection: close\r
3047
3151
  const mount = this.mounts.get(mountName);
3048
3152
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
3049
3153
  try {
3050
- const { FrpcTunnel } = await import('./frpc-NDcBtpwa.mjs');
3154
+ const { FrpcTunnel } = await import('./frpc-Cv6J2Mex.mjs');
3051
3155
  let tunnel;
3052
3156
  tunnel = new FrpcTunnel({
3053
3157
  name: tunnelName,
@@ -3974,6 +4078,26 @@ function cronMatches(expr, date) {
3974
4078
  if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
3975
4079
  return domOk && dowOk;
3976
4080
  }
4081
+ function inZone(date, tz) {
4082
+ if (!tz) return date;
4083
+ try {
4084
+ const p = new Intl.DateTimeFormat("en-US", {
4085
+ timeZone: tz,
4086
+ hour12: false,
4087
+ year: "numeric",
4088
+ month: "2-digit",
4089
+ day: "2-digit",
4090
+ hour: "2-digit",
4091
+ minute: "2-digit"
4092
+ }).formatToParts(date).reduce((o, x) => {
4093
+ o[x.type] = x.value;
4094
+ return o;
4095
+ }, {});
4096
+ return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
4097
+ } catch {
4098
+ return date;
4099
+ }
4100
+ }
3977
4101
  function resolvePath(ctx, path) {
3978
4102
  return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
3979
4103
  }
@@ -3992,8 +4116,8 @@ function sleepSync$1(ms) {
3992
4116
  }
3993
4117
  }
3994
4118
  function withFileLock(lockPath, fn, opts) {
3995
- const deadlineMs = 50;
3996
- const staleMs = 5e3;
4119
+ const deadlineMs = opts?.deadlineMs ?? 50;
4120
+ const staleMs = opts?.staleMs ?? 5e3;
3997
4121
  const deadline = Date.now() + deadlineMs;
3998
4122
  let held = false;
3999
4123
  while (Date.now() < deadline) {
@@ -4277,12 +4401,14 @@ class ChannelStore {
4277
4401
  return this._writeChannel(channel);
4278
4402
  }
4279
4403
  remove(id) {
4280
- const p = this._path(id);
4281
- if (existsSync(p)) {
4282
- rmSync$1(p);
4283
- return true;
4284
- }
4285
- return false;
4404
+ return withFileLock(this._lock(id), () => {
4405
+ const p = this._path(id);
4406
+ if (existsSync(p)) {
4407
+ rmSync$1(p);
4408
+ return true;
4409
+ }
4410
+ return false;
4411
+ });
4286
4412
  }
4287
4413
  // #0679: setEnabled/recordCall/addCaller are read-modify-write mutators — multiple ChannelStore
4288
4414
  // instances (one per session) point at the same .svamp/channels/<id>.json, so without
@@ -4711,10 +4837,14 @@ class ChannelOutbox {
4711
4837
  /** Append a reply addressed to `to`. Assigns seq + ts, persists, and wakes waiters. */
4712
4838
  append(channelId, r) {
4713
4839
  this.reload();
4714
- const seq = Math.max(this.highWater.get(channelId) || 0, this.seqByChannel.get(channelId) || 0) + 1;
4715
- this.seqByChannel.set(channelId, seq);
4716
- this.highWater.set(channelId, seq);
4717
- this._persistHighWater();
4840
+ const seq = withFileLock(`${this.seqFile}.lock`, () => {
4841
+ this._loadHighWater();
4842
+ const s = Math.max(this.highWater.get(channelId) || 0, this.seqByChannel.get(channelId) || 0) + 1;
4843
+ this.seqByChannel.set(channelId, s);
4844
+ this.highWater.set(channelId, s);
4845
+ this._persistHighWater();
4846
+ return s;
4847
+ });
4718
4848
  const reply = { seq, ts: Date.now(), to: r.to, body: r.body, ...r.correlationId ? { correlationId: r.correlationId } : {} };
4719
4849
  const arr = this.byChannel.get(channelId) || [];
4720
4850
  arr.push(reply);
@@ -4841,6 +4971,7 @@ const SESSION_METHOD_MIN_ROLE = {
4841
4971
  archiveSession: "admin",
4842
4972
  readFile: "admin",
4843
4973
  writeFile: "admin",
4974
+ writeFileChunk: "admin",
4844
4975
  listDirectory: "admin",
4845
4976
  bash: "admin",
4846
4977
  issue: "admin",
@@ -4857,8 +4988,43 @@ const SESSION_METHOD_MIN_ROLE = {
4857
4988
  refineLastReply: "admin",
4858
4989
  undoLastEdit: "admin"
4859
4990
  };
4991
+ const SESSION_VIEW_METHODS = /* @__PURE__ */ new Set([
4992
+ // read-only (authorizeRequest at view-role)
4993
+ "getMessages",
4994
+ "getLatestMessages",
4995
+ "getMessageCount",
4996
+ "getMetadata",
4997
+ "getSharing",
4998
+ "getInbox",
4999
+ "getEffectiveRole",
5000
+ "getActivityState",
5001
+ "getAgentState",
5002
+ "getChannelSkill",
5003
+ "getMeetingActivity",
5004
+ "listChannels",
5005
+ "outpostList",
5006
+ "registerListener",
5007
+ // self-authenticating (own capability key / token) or liveness — machine role stays view
5008
+ "channelDescribe",
5009
+ "channelExecTool",
5010
+ "channelGetProfile",
5011
+ "channelList",
5012
+ "channelOwns",
5013
+ "channelReceive",
5014
+ "channelSend",
5015
+ "channelUpload",
5016
+ "outpostHello",
5017
+ "outpostPoll",
5018
+ "outpostResult",
5019
+ "disconnect",
5020
+ "reregister",
5021
+ "recordMeetingActivity"
5022
+ ]);
4860
5023
  function sessionMethodMinRole(method) {
4861
- return SESSION_METHOD_MIN_ROLE[method] ?? "view";
5024
+ const mapped = SESSION_METHOD_MIN_ROLE[method];
5025
+ if (mapped) return mapped;
5026
+ if (SESSION_VIEW_METHODS.has(method)) return "view";
5027
+ return "admin";
4862
5028
  }
4863
5029
  function getParamNames(fn) {
4864
5030
  const src = fn.toString();
@@ -6088,7 +6254,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6088
6254
  const tunnels = handlers.tunnels;
6089
6255
  if (!tunnels) throw new Error("Tunnel management not available");
6090
6256
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
6091
- const { FrpcTunnel } = await import('./frpc-NDcBtpwa.mjs');
6257
+ const { FrpcTunnel } = await import('./frpc-Cv6J2Mex.mjs');
6092
6258
  const tunnel = new FrpcTunnel({
6093
6259
  name: params.name,
6094
6260
  ports: params.ports,
@@ -6573,7 +6739,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6573
6739
  let owner;
6574
6740
  try {
6575
6741
  role2 = (await rpc.getEffectiveRole(context))?.role ?? null;
6576
- if (!role2) return { success: false, error: "Not authorized for this session" };
6742
+ if (!roleAtLeast(role2, "interact")) return { success: false, error: "Not authorized: WISE ask requires interact access to this session." };
6577
6743
  const m = await rpc.getMetadata(context);
6578
6744
  cwd = m?.metadata?.path;
6579
6745
  owner = m?.metadata?.sharing?.owner;
@@ -6582,12 +6748,12 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6582
6748
  }
6583
6749
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
6584
6750
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
6585
- const { toolsForRole } = await import('./sideband-CdJ0O6L6.mjs');
6751
+ const { toolsForRole } = await import('./sideband-C_3PVGh-.mjs');
6586
6752
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
6587
6753
  return fmt(r2);
6588
6754
  }
6589
6755
  const role = getEffectiveRole(context, currentMetadata.sharing);
6590
- if (!role) return { success: false, error: "Not authorized on this machine" };
6756
+ if (!roleAtLeast(role, "interact")) return { success: false, error: "Not authorized: WISE ask requires interact access to this machine." };
6591
6757
  const machineDeps = buildMachineDeps(
6592
6758
  {
6593
6759
  getSessionIds: handlers.getSessionIds,
@@ -6687,7 +6853,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6687
6853
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
6688
6854
  }
6689
6855
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
6690
- const { queryCore } = await import('./commands-CJVqMnNf.mjs');
6856
+ const { queryCore } = await import('./commands-BTN9XxVx.mjs');
6691
6857
  const timeout = c.reply?.timeout_sec || 120;
6692
6858
  let result;
6693
6859
  try {
@@ -7197,9 +7363,17 @@ function getRateLimitRetryConfig() {
7197
7363
  return {
7198
7364
  maxRetries: envInt("SVAMP_RATELIMIT_MAX_RETRIES", 200),
7199
7365
  baseMs: envInt("SVAMP_RATELIMIT_BASE_MS", 5e3),
7200
- capMs: envInt("SVAMP_RATELIMIT_CAP_MS", 12e4)
7366
+ capMs: envInt("SVAMP_RATELIMIT_CAP_MS", 12e4),
7367
+ maxElapsedMs: envInt("SVAMP_RATELIMIT_MAX_ELAPSED_MS", 36e5)
7201
7368
  };
7202
7369
  }
7370
+ function retryStreakExhausted(attempts, streakStartedAtMs, nowMs, cfg = getRateLimitRetryConfig()) {
7371
+ if (attempts >= cfg.maxRetries) return { exhausted: true, reason: "count" };
7372
+ if (cfg.maxElapsedMs > 0 && streakStartedAtMs && streakStartedAtMs > 0 && nowMs - streakStartedAtMs >= cfg.maxElapsedMs) {
7373
+ return { exhausted: true, reason: "elapsed" };
7374
+ }
7375
+ return { exhausted: false };
7376
+ }
7203
7377
  function computeRetryDelayMs(attempt, cfg = getRateLimitRetryConfig(), rng = Math.random) {
7204
7378
  const a = Math.max(0, Math.floor(attempt));
7205
7379
  const expo = Math.min(a, 30);
@@ -7378,6 +7552,9 @@ async function deletePairingArtifact(am, artifactId, log) {
7378
7552
  }
7379
7553
  }
7380
7554
  async function sweepExpiredPairings(am, svampHome, log) {
7555
+ return withMintLock("*sweep*", () => _sweepExpiredPairingsUnlocked(am, svampHome, log));
7556
+ }
7557
+ async function _sweepExpiredPairingsUnlocked(am, svampHome, log) {
7381
7558
  const now = Date.now();
7382
7559
  const all = loadPairings(svampHome);
7383
7560
  const expired = all.filter((p) => p.expiresAt <= now);
@@ -7385,18 +7562,40 @@ async function sweepExpiredPairings(am, svampHome, log) {
7385
7562
  for (const p of expired) await deletePairingArtifact(am, p.artifactId, log);
7386
7563
  savePairings(all.filter((p) => p.expiresAt > now), svampHome);
7387
7564
  }
7565
+ let _lastSweepAt = 0;
7566
+ async function sweepExpiredPairingsThrottled(am, svampHome, log, minIntervalMs = 5 * 6e4) {
7567
+ const now = Date.now();
7568
+ if (now - _lastSweepAt < minIntervalMs) return;
7569
+ _lastSweepAt = now;
7570
+ await sweepExpiredPairings(am, svampHome, log);
7571
+ }
7572
+ let _pairingChain = Promise.resolve();
7573
+ function withMintLock(_session, fn) {
7574
+ const prior = _pairingChain.catch(() => {
7575
+ });
7576
+ const result = prior.then(fn);
7577
+ _pairingChain = result.catch(() => {
7578
+ });
7579
+ return result;
7580
+ }
7388
7581
  async function burnSessionPairings(am, session, svampHome, log) {
7389
- const all = loadPairings(svampHome);
7390
- const mine = all.filter((p) => p.session === session);
7391
- if (mine.length === 0) return;
7392
- for (const p of mine) await deletePairingArtifact(am, p.artifactId, log);
7393
- savePairings(all.filter((p) => p.session !== session), svampHome);
7582
+ return withMintLock(session, async () => {
7583
+ const all = loadPairings(svampHome);
7584
+ const mine = all.filter((p) => p.session === session);
7585
+ if (mine.length === 0) return;
7586
+ for (const p of mine) await deletePairingArtifact(am, p.artifactId, log);
7587
+ savePairings(all.filter((p) => p.session !== session), svampHome);
7588
+ });
7394
7589
  }
7395
7590
  async function mintPairingCode(opts) {
7396
- const { am, session, payload, log } = opts;
7591
+ const { session} = opts;
7397
7592
  const resolver = (opts.resolverBase || "https://hypha.aicell.io").replace(/\/$/, "");
7398
7593
  const ttlMs = opts.ttlMs && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_PAIRING_TTL_MS;
7399
- await sweepExpiredPairings(am, opts.svampHome, log).catch(() => {
7594
+ return withMintLock(session, () => _mintPairingCodeLocked(opts, resolver, ttlMs));
7595
+ }
7596
+ async function _mintPairingCodeLocked(opts, resolver, ttlMs) {
7597
+ const { am, session, payload, log } = opts;
7598
+ await _sweepExpiredPairingsUnlocked(am, opts.svampHome, log).catch(() => {
7400
7599
  });
7401
7600
  const now = Date.now();
7402
7601
  const existing = loadPairings(opts.svampHome).find((p) => p.session === session && p.expiresAt > now + 3e4);
@@ -7457,6 +7656,7 @@ class OutpostCoordinator {
7457
7656
  clearTimer;
7458
7657
  execTimeoutMs;
7459
7658
  staleMs;
7659
+ randToken;
7460
7660
  sweeper = null;
7461
7661
  constructor(opts = {}) {
7462
7662
  this.now = opts.now || (() => Date.now());
@@ -7464,6 +7664,7 @@ class OutpostCoordinator {
7464
7664
  this.clearTimer = opts.clearTimer || ((t) => clearTimeout(t));
7465
7665
  this.execTimeoutMs = opts.execTimeoutMs ?? OUTPOST_EXEC_TIMEOUT_MS;
7466
7666
  this.staleMs = opts.staleMs ?? OUTPOST_STALE_MS;
7667
+ this.randToken = opts.randToken || (() => randomBytes(9).toString("base64url"));
7467
7668
  }
7468
7669
  isStale(c, at) {
7469
7670
  return at - c.lastSeen > this.staleMs;
@@ -7575,7 +7776,7 @@ class OutpostCoordinator {
7575
7776
  * machine is not connected.
7576
7777
  */
7577
7778
  enqueueExec(machine, cmd, cwd) {
7578
- const id = `x${(++this.seq).toString(36)}_${this.now().toString(36)}`;
7779
+ const id = `x${(++this.seq).toString(36)}_${this.now().toString(36)}_${this.randToken()}`;
7579
7780
  if (!this.isConnected(machine)) {
7580
7781
  return {
7581
7782
  id,
@@ -8035,6 +8236,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8035
8236
  const channelOutbox = new ChannelOutbox(initialMetadata.path);
8036
8237
  const outpostCoordinator = new OutpostCoordinator();
8037
8238
  outpostCoordinator.startSweeper();
8239
+ void (async () => {
8240
+ try {
8241
+ const am = await server.getService("public/artifact-manager");
8242
+ if (am) await sweepExpiredPairingsThrottled(am, void 0, (m) => console.log(m));
8243
+ } catch {
8244
+ }
8245
+ })();
8038
8246
  const outpostProjectDir = () => metadata.path || process.cwd();
8039
8247
  const announceOutpost = (conn) => {
8040
8248
  const who = conn.label || conn.machine;
@@ -8810,7 +9018,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8810
9018
  });
8811
9019
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
8812
9020
  const cursor = Math.max(0, Number(params.cursor) || 0);
8813
- const waitMs = Math.min(Math.max(0, Number(params.wait ?? 25) * 1e3), 6e4);
9021
+ const waitSecRaw = Number(params.wait);
9022
+ const waitSec = Number.isFinite(waitSecRaw) ? waitSecRaw : 25;
9023
+ const waitMs = Math.min(Math.max(0, waitSec * 1e3), 6e4);
8814
9024
  const out = await channelOutbox.wait(c.id, cursor, r.sender.name, waitMs, params.correlationId);
8815
9025
  return { ok: true, replies: out.replies, cursor: out.cursor };
8816
9026
  },
@@ -8921,6 +9131,12 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8921
9131
  await callbacks.onWriteFile(path, content);
8922
9132
  return { success: true };
8923
9133
  },
9134
+ writeFileChunk: async (path, content, uploadId, chunkIndex, totalChunks, isLast, context) => {
9135
+ authorizeRequest(context, metadata.sharing, "admin");
9136
+ if (!callbacks.onWriteFileChunk) throw new Error("writeFileChunk not supported");
9137
+ await callbacks.onWriteFileChunk(path, content, uploadId, chunkIndex, totalChunks, isLast);
9138
+ return { success: true };
9139
+ },
8924
9140
  listDirectory: async (path, context) => {
8925
9141
  authorizeRequest(context, metadata.sharing, "admin");
8926
9142
  if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
@@ -10470,8 +10686,23 @@ function computeCollectionConfigUpdate(existingConfig, extra) {
10470
10686
  function emailHash(email) {
10471
10687
  return createHash("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
10472
10688
  }
10689
+ function sessionIdHash(sessionId) {
10690
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 12);
10691
+ }
10692
+ function roleLabel(role) {
10693
+ switch (role) {
10694
+ case "admin":
10695
+ return "full access";
10696
+ case "interact":
10697
+ return "can interact";
10698
+ case "view":
10699
+ return "view only";
10700
+ default:
10701
+ return String(role);
10702
+ }
10703
+ }
10473
10704
  function shareAlias(sessionId, recipientEmail) {
10474
- return `share-${sessionId.slice(0, 8)}-${emailHash(recipientEmail)}`;
10705
+ return `share-${sessionIdHash(sessionId)}-${emailHash(recipientEmail)}`;
10475
10706
  }
10476
10707
  function eventAlias(eventId, recipientEmail) {
10477
10708
  return `evt-${emailHash(recipientEmail)}-${eventId.slice(0, 8)}`;
@@ -10585,8 +10816,10 @@ class SharingNotificationSync {
10585
10816
  userEmail: params.ownerEmail
10586
10817
  },
10587
10818
  title: `${typeLabel} shared with you`,
10588
- // Per-user tiers were retired; explicit users always have full access.
10589
- body: `${params.ownerEmail} shared "${params.label || "Untitled"}" with you (full access)`,
10819
+ // #0689: render the recipient's ACTUAL role (view/interact/admin) authorize.ts genuinely
10820
+ // enforces per-user roles, so the old hardcoded "(full access)" misstated a view/interact
10821
+ // invitee's access.
10822
+ body: `${params.ownerEmail} shared "${params.label || "Untitled"}" with you (${roleLabel(params.role)})`,
10590
10823
  level: "info",
10591
10824
  action: shareType === "session" ? {
10592
10825
  type: "add-session-bookmark",
@@ -10766,13 +10999,15 @@ class SharingNotificationSync {
10766
10999
  // ── Sharing config diff + sync ───────────────────────────────────
10767
11000
  async syncSharing(sessionId, oldSharing, newSharing, context) {
10768
11001
  if (!this.initialized) return;
10769
- const oldEmails = new Set(
10770
- (oldSharing?.allowedUsers || []).map((u) => u.email.toLowerCase())
11002
+ const oldRoleByEmail = new Map(
11003
+ (oldSharing?.allowedUsers || []).map((u) => [u.email.toLowerCase(), u.role])
10771
11004
  );
11005
+ const oldEmails = new Set(oldRoleByEmail.keys());
10772
11006
  const newUsers = newSharing.allowedUsers || [];
10773
11007
  const newEmails = new Set(newUsers.map((u) => u.email.toLowerCase()));
10774
11008
  for (const user of newUsers) {
10775
- if (!oldEmails.has(user.email.toLowerCase())) {
11009
+ const prevRole = oldRoleByEmail.get(user.email.toLowerCase());
11010
+ if (prevRole === void 0 || prevRole !== user.role) {
10776
11011
  this.notifyShare({
10777
11012
  recipientEmail: user.email,
10778
11013
  sessionId,
@@ -12209,6 +12444,16 @@ function codexCumulativeTokens(payload) {
12209
12444
  return nonCachedInput + output + Math.round(cached * 0.1);
12210
12445
  }
12211
12446
 
12447
+ const PINNED_CODEX_VERSION = "0.146.0";
12448
+ const MIN_CODEX_STEER_VERSION = { major: 0, minor: 129, patch: 0 };
12449
+ function cmpVer(a, b) {
12450
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
12451
+ }
12452
+ function codexSupportsSteer(v) {
12453
+ if (!v) return false;
12454
+ return cmpVer(v, MIN_CODEX_STEER_VERSION) >= 0;
12455
+ }
12456
+
12212
12457
  function parseCodexVersion(v) {
12213
12458
  const m = v.match(/codex-cli\s+(\d+)\.(\d+)\.(\d+)/);
12214
12459
  if (!m) return null;
@@ -12225,6 +12470,31 @@ function codexAppServerAvailable() {
12225
12470
  return null;
12226
12471
  }
12227
12472
  }
12473
+ const _execFileAsync = promisify$1(execFile$1);
12474
+ let _steerSupport = null;
12475
+ let _steerProbedAt = 0;
12476
+ let _steerProbe = null;
12477
+ const _STEER_TTL_MS = 3e4;
12478
+ const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
12479
+ function _steerFresh() {
12480
+ return _steerSupport !== null && Date.now() - _steerProbedAt < _STEER_TTL_MS;
12481
+ }
12482
+ async function codexSupportsSteerInstalledAsync() {
12483
+ if (_steerFresh()) return _steerSupport;
12484
+ if (_steerProbe) return _steerProbe;
12485
+ _steerProbe = (async () => {
12486
+ try {
12487
+ const { stdout } = await _execFileAsync("codex", ["--version"], { encoding: "utf8" });
12488
+ _steerSupport = codexSupportsSteer(parseCodexVersion(stdout.trim()));
12489
+ } catch {
12490
+ _steerSupport = false;
12491
+ }
12492
+ _steerProbedAt = Date.now();
12493
+ _steerProbe = null;
12494
+ return _steerSupport;
12495
+ })();
12496
+ return _steerProbe;
12497
+ }
12228
12498
  class CodexAppServerClient {
12229
12499
  constructor(opts) {
12230
12500
  this.opts = opts;
@@ -12381,19 +12651,36 @@ class CodexAppServerClient {
12381
12651
  /**
12382
12652
  * Inject additional input into the CURRENTLY RUNNING turn (Claude-Code-style mid-turn steering).
12383
12653
  *
12384
- * codex app-server FOLDS a second `turn/start` sent mid-turn into the active turn: the new input's
12385
- * items are emitted under the RUNNING turn's id and NO separate `turn/completed` fires for the
12386
- * injected turn (verified empirically probe-codex-midturn). So we send it WITHOUT arming a new
12387
- * `pendingTurn` slot and WITHOUT overwriting `_turnId` (which must keep pointing at the turn to
12388
- * interrupt). The single in-flight completion still resolves the original `sendTurnAndWait`.
12654
+ * Uses codex app-server's NATIVE `turn/steer` the correct primitive for "append this message to
12655
+ * the in-flight turn without interrupting it." The steered input is applied at the next model step
12656
+ * (it does NOT abort a running tool call), the assistant finishes the current work and drains the
12657
+ * steered message as user input before its next LLM call, and NO separate `turn/started` /
12658
+ * `turn/completed` fires — the original single in-flight completion still resolves `sendTurnAndWait`.
12659
+ * So we send it WITHOUT arming a new `pendingTurn` slot and WITHOUT overwriting `_turnId`.
12660
+ *
12661
+ * (The earlier #86 implementation fired a second `turn/start` mid-turn; on codex 0.144 that
12662
+ * ABORTS/REPLACES the running turn instead of folding — the "cuts off the previous task" bug.)
12389
12663
  *
12390
- * Returns false when there is NO active turn to fold into (the caller then falls back to a normal
12391
- * turn via sendPrompt). `pendingTurn` is the in-flight signal.
12664
+ * Returns false when there is no active turn to steer, or when the server REJECTS the steer
12665
+ * (no active turn / expectedTurnId mismatch / non-steerable turn kind such as review or manual
12666
+ * compact). The caller then falls back to client-side queueing (post-turn drain via sendPrompt),
12667
+ * so a rejected steer degrades gracefully rather than losing the message.
12392
12668
  */
12393
- async injectInput(prompt, o) {
12394
- if (!this._threadId || !this.pendingTurn) return false;
12395
- await this.request("turn/start", this.buildTurnParams(prompt, o));
12396
- return true;
12669
+ async injectInput(prompt, _o) {
12670
+ if (!this._threadId || !this.pendingTurn || !this._turnId) return false;
12671
+ if (!await codexSupportsSteerInstalledAsync()) return false;
12672
+ const params = {
12673
+ threadId: this._threadId,
12674
+ expectedTurnId: this._turnId,
12675
+ input: [{ type: "text", text: prompt }]
12676
+ };
12677
+ try {
12678
+ await this.request("turn/steer", params);
12679
+ return true;
12680
+ } catch (e) {
12681
+ this.log("[codex-app-server] turn/steer rejected \u2014 falling back to post-turn queue", e);
12682
+ return false;
12683
+ }
12397
12684
  }
12398
12685
  /** Interrupt the in-flight turn (real server-side abort — the old MCP path couldn't do this). */
12399
12686
  async interrupt() {
@@ -12436,17 +12723,16 @@ class CodexAppServerClient {
12436
12723
  // ── JSON-RPC plumbing ──────────────────────────────────────────────────────
12437
12724
  request(method, params, timeoutMs) {
12438
12725
  const id = this.nextId++;
12726
+ const effTimeout = timeoutMs ?? this.opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12439
12727
  return new Promise((resolve, reject) => {
12440
12728
  this.pending.set(id, { resolve, reject, method });
12441
- if (timeoutMs) {
12442
- const t = setTimeout(() => {
12443
- if (this.pending.has(id)) {
12444
- this.pending.delete(id);
12445
- reject(new Error(`codex ${method} timed out after ${timeoutMs}ms`));
12446
- }
12447
- }, timeoutMs);
12448
- t.unref?.();
12449
- }
12729
+ const t = setTimeout(() => {
12730
+ if (this.pending.has(id)) {
12731
+ this.pending.delete(id);
12732
+ reject(new Error(`codex ${method} timed out after ${effTimeout}ms`));
12733
+ }
12734
+ }, effTimeout);
12735
+ t.unref?.();
12450
12736
  this.write({ jsonrpc: "2.0", id, method, params });
12451
12737
  });
12452
12738
  }
@@ -14746,8 +15032,8 @@ function isWorkflowEnabled(wf) {
14746
15032
  function workflowSteps(wf) {
14747
15033
  return Object.values(wf.jobs || {}).flatMap((j) => j?.steps || []);
14748
15034
  }
14749
- function workflowCrons(wf) {
14750
- return (wf.on?.schedule || []).map((s) => s?.cron).filter((c) => !!c);
15035
+ function workflowSchedules(wf) {
15036
+ return (wf.on?.schedule || []).filter((s) => !!s?.cron);
14751
15037
  }
14752
15038
  function workflowsDir(projectRoot) {
14753
15039
  return join$1(projectRoot, ".svamp", "workflows");
@@ -14767,7 +15053,11 @@ function normalizeOn(on) {
14767
15053
  }
14768
15054
  const out = {};
14769
15055
  if (Array.isArray(on.schedule)) {
14770
- const sched = on.schedule.map((s) => ({ cron: String(s?.cron ?? s ?? "") })).filter((s) => s.cron);
15056
+ const sched = on.schedule.map((s) => {
15057
+ const entry = { cron: String(s?.cron ?? s ?? "") };
15058
+ if (s && typeof s === "object" && s.tz != null && String(s.tz).trim()) entry.tz = String(s.tz).trim();
15059
+ return entry;
15060
+ }).filter((s) => s.cron);
14771
15061
  if (sched.length) out.schedule = sched;
14772
15062
  } else if (typeof on.schedule === "string" && on.schedule.trim()) {
14773
15063
  out.schedule = [{ cron: on.schedule.trim() }];
@@ -14815,7 +15105,7 @@ function serializeWorkflow(wf) {
14815
15105
  if (wf.session) clean.session = wf.session;
14816
15106
  if (wf.enabled === false) clean.enabled = false;
14817
15107
  const on = {};
14818
- if (wf.on?.schedule?.length) on.schedule = wf.on.schedule.map((s) => ({ cron: s.cron }));
15108
+ if (wf.on?.schedule?.length) on.schedule = wf.on.schedule.map((s) => s.tz ? { cron: s.cron, tz: s.tz } : { cron: s.cron });
14819
15109
  if (wf.on?.workflow_dispatch) on.workflow_dispatch = {};
14820
15110
  if (wf.on?.channel) on.channel = wf.on.channel;
14821
15111
  if (wf.on?.issue?.length) on.issue = wf.on.issue;
@@ -15081,7 +15371,7 @@ function escalateWorkflowFailure(projectRoot, wf, run) {
15081
15371
  const open = listIssues(projectRoot, { label: failLabel }).filter((i) => i.status !== "archived");
15082
15372
  if (open.length > 0) return void 0;
15083
15373
  const failed = run.steps.find((s) => s.timedOut || s.exitCode !== 0 && s.exitCode !== null) || run.steps[run.steps.length - 1];
15084
- const failCause = run.error ? `runner error: ${run.error}` : failed?.timedOut ? `step timed out after ${STEP_TIMEOUT_MS / 1e3}s` : `step exited with code ${failed?.exitCode ?? "null (killed/signal)"}`;
15374
+ const failCause = run.error ? `runner error: ${run.error}` : failed?.timedOut ? `step timed out after ${stepTimeoutMs() / 1e3}s` : `step exited with code ${failed?.exitCode ?? "null (killed/signal)"}`;
15085
15375
  const defLines = workflowSteps(wf).map((s, i) => ` ${i}. ${s.run}`).join("\n") || " (no steps)";
15086
15376
  const stderrTail = failed?.stderr ? capStream(failed.stderr, 3e3) : "(none)";
15087
15377
  const stdoutTail = failed?.stdout ? capStream(failed.stdout, 1500) : "(none)";
@@ -15265,7 +15555,9 @@ function fireIdleWorkflows(root, sessionId, deps = {}) {
15265
15555
  } catch {
15266
15556
  return [];
15267
15557
  }
15268
- const matches = workflows.filter((wf) => wf.on?.idle === true && isWorkflowEnabled(wf));
15558
+ const matches = workflows.filter(
15559
+ (wf) => wf.on?.idle === true && isWorkflowEnabled(wf) && (!wf.session || wf.session === sessionId)
15560
+ );
15269
15561
  return matches.map((wf) => {
15270
15562
  deps.log?.(`[workflow] idle fired "${wf.name}" in ${root} for session ${sessionId}`);
15271
15563
  return runWorkflow(root, wf, {
@@ -16021,6 +16313,24 @@ async function readSessionFileBase64(resolvedPath) {
16021
16313
  const buffer = await fs$1.readFile(resolvedPath);
16022
16314
  return buffer.toString("base64");
16023
16315
  }
16316
+ async function writeSessionFileChunk(directory, sessionMetadata, path, content, uploadId, chunkIndex, isLast) {
16317
+ const resolvedPath = resolve$1(directory, path);
16318
+ if (sessionMetadata?.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
16319
+ throw new Error("Path outside working directory");
16320
+ }
16321
+ const safeUploadId = String(uploadId).replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64) || "upload";
16322
+ const tempPath = `${resolvedPath}.svamp-upload-${safeUploadId}.part`;
16323
+ const buffer = Buffer.from(content || "", "base64");
16324
+ if (chunkIndex === 0) {
16325
+ await fs$1.mkdir(dirname$1(resolvedPath), { recursive: true });
16326
+ await fs$1.writeFile(tempPath, buffer);
16327
+ } else {
16328
+ await fs$1.appendFile(tempPath, buffer);
16329
+ }
16330
+ if (isLast) {
16331
+ await fs$1.rename(tempPath, resolvedPath);
16332
+ }
16333
+ }
16024
16334
 
16025
16335
  const __filename$1 = fileURLToPath(import.meta.url);
16026
16336
  const __dirname$1 = dirname$1(__filename$1);
@@ -17519,7 +17829,7 @@ async function startDaemon(options) {
17519
17829
  try {
17520
17830
  const dir = loadSessionIndex()[sessionId]?.directory;
17521
17831
  if (!dir) return;
17522
- const { reconcileServiceLinks } = await import('./agentCommands-D68wBf-y.mjs');
17832
+ const { reconcileServiceLinks } = await import('./agentCommands-DdNUACV8.mjs');
17523
17833
  const configPath = getSvampConfigPath(dir, sessionId);
17524
17834
  const config = readSvampConfig(configPath);
17525
17835
  const entries = Array.from(urls.entries());
@@ -17537,7 +17847,7 @@ async function startDaemon(options) {
17537
17847
  }
17538
17848
  }
17539
17849
  async function createExposedTunnel(spec) {
17540
- const { FrpcTunnel } = await import('./frpc-NDcBtpwa.mjs');
17850
+ const { FrpcTunnel } = await import('./frpc-Cv6J2Mex.mjs');
17541
17851
  const tunnel = new FrpcTunnel({
17542
17852
  name: spec.name,
17543
17853
  ports: spec.ports,
@@ -17565,7 +17875,7 @@ async function startDaemon(options) {
17565
17875
  ensureAutoInstalledCommands(logger);
17566
17876
  (async () => {
17567
17877
  try {
17568
- const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-BaMR97BE.mjs');
17878
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-C6MSkqS9.mjs');
17569
17879
  beginClaudeVersionReconcile((msg) => logger.log(msg));
17570
17880
  } catch (e) {
17571
17881
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
@@ -17848,8 +18158,16 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
17848
18158
  if (RATELIMIT_CFG.maxRetries <= 0) return false;
17849
18159
  if (trackedSession?.stopped) return false;
17850
18160
  if (currentTurnMessage === void 0) return false;
17851
- if (rateLimitRetryCount >= RATELIMIT_CFG.maxRetries) {
17852
- logger.log(`[Session ${sessionId}] Rate-limit retries exhausted (${rateLimitRetryCount}/${RATELIMIT_CFG.maxRetries}) \u2014 surfacing error`);
18161
+ const nowTs = Date.now();
18162
+ if (rateLimitStreakStartedAt === 0) {
18163
+ rateLimitStreakStartedAt = nowTs;
18164
+ sessionMetadata = { ...sessionMetadata, rateLimitStreakStartedAt: nowTs };
18165
+ }
18166
+ const budget = retryStreakExhausted(rateLimitRetryCount, rateLimitStreakStartedAt, nowTs, RATELIMIT_CFG);
18167
+ if (budget.exhausted) {
18168
+ const elapsedMin = Math.round((nowTs - rateLimitStreakStartedAt) / 6e4);
18169
+ logger.log(`[Session ${sessionId}] Rate-limit retries exhausted (${budget.reason}: ${rateLimitRetryCount}/${RATELIMIT_CFG.maxRetries} attempts, ${elapsedMin}m elapsed) \u2014 surfacing error`);
18170
+ resetRateLimitStreak();
17853
18171
  return false;
17854
18172
  }
17855
18173
  const attempt = rateLimitRetryCount++;
@@ -18317,18 +18635,18 @@ ${parts.join("\n")}`);
18317
18635
  enqueueLoopMessage(push, label);
18318
18636
  if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
18319
18637
  } else {
18320
- writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: decision.reason, holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions });
18638
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: decision.reason, holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions });
18321
18639
  logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
18322
18640
  checkSvampConfig?.();
18323
18641
  }
18324
18642
  } else {
18325
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions });
18643
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions });
18326
18644
  checkSvampConfig?.();
18327
18645
  }
18328
18646
  } catch (e) {
18329
18647
  logger.log(`[Session ${sessionId}] verifyGoalCompletion error \u2014 failing open (done): ${e?.message || e}`);
18330
18648
  try {
18331
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now() });
18649
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now() });
18332
18650
  checkSvampConfig?.();
18333
18651
  } catch {
18334
18652
  }
@@ -18386,8 +18704,16 @@ ${parts.join("\n")}`);
18386
18704
  const RATELIMIT_CFG = getRateLimitRetryConfig();
18387
18705
  let currentTurnMessage;
18388
18706
  let rateLimitRetryCount = 0;
18707
+ let rateLimitStreakStartedAt = Number(sessionMetadata?.rateLimitStreakStartedAt) || 0;
18389
18708
  let rateLimitRetryTimer = null;
18390
18709
  let rateLimitRetryScheduled = false;
18710
+ const resetRateLimitStreak = () => {
18711
+ rateLimitRetryCount = 0;
18712
+ rateLimitStreakStartedAt = 0;
18713
+ if (sessionMetadata?.rateLimitStreakStartedAt) {
18714
+ sessionMetadata = { ...sessionMetadata, rateLimitStreakStartedAt: 0 };
18715
+ }
18716
+ };
18391
18717
  let checkSvampConfig;
18392
18718
  let cleanupSvampConfig;
18393
18719
  const VALID_CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
@@ -18802,7 +19128,7 @@ ${parts.join("\n")}`);
18802
19128
  "event"
18803
19129
  );
18804
19130
  }
18805
- if (!msg.is_error) rateLimitRetryCount = 0;
19131
+ if (!msg.is_error) resetRateLimitStreak();
18806
19132
  if (msg.session_id) {
18807
19133
  claudeResumeId = msg.session_id;
18808
19134
  if (sessionMetadata.claudeSessionId !== msg.session_id) {
@@ -19374,7 +19700,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19374
19700
  return;
19375
19701
  }
19376
19702
  currentTurnMessage = text;
19377
- rateLimitRetryCount = 0;
19703
+ resetRateLimitStreak();
19378
19704
  if (rateLimitRetryTimer) {
19379
19705
  clearTimeout(rateLimitRetryTimer);
19380
19706
  rateLimitRetryTimer = null;
@@ -19398,7 +19724,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19398
19724
  clearTimeout(rateLimitRetryTimer);
19399
19725
  rateLimitRetryTimer = null;
19400
19726
  }
19401
- rateLimitRetryCount = 0;
19727
+ resetRateLimitStreak();
19402
19728
  if (claudeProcess && !claudeProcess.killed) {
19403
19729
  try {
19404
19730
  const interruptMsg = JSON.stringify({ type: "control_request", request: { type: "interrupt" } });
@@ -19755,11 +20081,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19755
20081
  });
19756
20082
  },
19757
20083
  onIssue: async (params) => {
19758
- const { issueRpc } = await import('./rpc-K5xzddMF.mjs');
20084
+ const { issueRpc } = await import('./rpc-CkcBAtcs.mjs');
19759
20085
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
19760
20086
  },
19761
20087
  onWorkflow: async (params) => {
19762
- const { workflowRpc } = await import('./rpc-DSyBSuTF.mjs');
20088
+ const { workflowRpc } = await import('./rpc-BwxwusfB.mjs');
19763
20089
  return workflowRpc(params?.cwd || directory, params || {});
19764
20090
  },
19765
20091
  onRipgrep: async (args, cwd) => {
@@ -19791,6 +20117,9 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19791
20117
  await fs$1.mkdir(dirname$1(resolvedPath), { recursive: true });
19792
20118
  await fs$1.writeFile(resolvedPath, Buffer.from(content, "base64"));
19793
20119
  },
20120
+ onWriteFileChunk: async (path, content, uploadId, chunkIndex, totalChunks, isLast) => {
20121
+ await writeSessionFileChunk(directory, sessionMetadata, path, content, uploadId, chunkIndex, isLast);
20122
+ },
19794
20123
  onListDirectory: async (path) => {
19795
20124
  const resolvedDir = resolve$1(directory, path || ".");
19796
20125
  if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
@@ -19800,8 +20129,15 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19800
20129
  return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
19801
20130
  },
19802
20131
  onGetDirectoryTree: async (treePath, maxDepth) => {
20132
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svamp", ".expo", "dist", "build", ".next", ".cache", ".venv", "venv", "__pycache__", ".turbo", ".gradle", "target"]);
20133
+ const MAX_TREE_DEPTH = 8;
20134
+ const MAX_TREE_NODES = 2e4;
20135
+ const effectiveMaxDepth = Math.min(Math.max(0, Math.floor(Number(maxDepth)) || 0), MAX_TREE_DEPTH);
20136
+ let treeNodeCount = 0;
19803
20137
  async function buildTree(p, name, depth) {
19804
20138
  try {
20139
+ if (treeNodeCount >= MAX_TREE_NODES) return null;
20140
+ treeNodeCount++;
19805
20141
  const stats = await fs$1.stat(p);
19806
20142
  const node = {
19807
20143
  name,
@@ -19810,11 +20146,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19810
20146
  size: stats.size,
19811
20147
  modified: stats.mtime.getTime()
19812
20148
  };
19813
- if (stats.isDirectory() && depth < maxDepth) {
20149
+ if (stats.isDirectory() && depth < effectiveMaxDepth) {
19814
20150
  const entries = await fs$1.readdir(p, { withFileTypes: true });
19815
20151
  const children = [];
19816
20152
  await Promise.all(entries.map(async (entry) => {
19817
20153
  if (entry.isSymbolicLink()) return;
20154
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) return;
20155
+ if (treeNodeCount >= MAX_TREE_NODES) return;
19818
20156
  const childPath = join(p, entry.name);
19819
20157
  const childNode = await buildTree(childPath, entry.name, depth + 1);
19820
20158
  if (childNode) children.push(childNode);
@@ -19885,7 +20223,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19885
20223
  userMessagePending = true;
19886
20224
  turnInitiatedByUser = true;
19887
20225
  currentTurnMessage = next.text;
19888
- rateLimitRetryCount = 0;
20226
+ resetRateLimitStreak();
19889
20227
  if (rateLimitRetryTimer) {
19890
20228
  clearTimeout(rateLimitRetryTimer);
19891
20229
  rateLimitRetryTimer = null;
@@ -20164,6 +20502,30 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20164
20502
  };
20165
20503
  sessionService.updateMetadata(sessionMetadata);
20166
20504
  };
20505
+ const drainQueueHeadIfIdle = () => {
20506
+ if (acpStopped || !acpBackendReady) return;
20507
+ if (sessionMetadata.lifecycleState !== "idle") return;
20508
+ const queue = sessionMetadata.messageQueue;
20509
+ if (!queue || queue.length === 0) return;
20510
+ const next = queue[0];
20511
+ const remaining = queue.slice(1);
20512
+ sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
20513
+ sessionService.updateMetadata(sessionMetadata);
20514
+ if (typeof next.inboxHop === "number") {
20515
+ writeInboundContext(sessionId, { hopCount: next.inboxHop, threadId: next.inboxThreadId });
20516
+ }
20517
+ sessionService.markInboxHandled(next.id);
20518
+ if (!next.alreadyStored) sessionService.pushMessage(next.displayText || next.text, "user");
20519
+ sessionService.sendKeepAlive(true);
20520
+ agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
20521
+ logger.error(`[${agentName} Session ${sessionId}] Error draining queued message after inject miss: ${err?.message ?? err}`);
20522
+ if (!acpStopped) {
20523
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
20524
+ sessionService.updateMetadata(sessionMetadata);
20525
+ sessionService.sendSessionEnd();
20526
+ }
20527
+ });
20528
+ };
20167
20529
  const injectFn = agentBackend.injectInput;
20168
20530
  if (typeof injectFn === "function") {
20169
20531
  Promise.resolve(injectFn.call(agentBackend, text)).then((injected) => {
@@ -20172,10 +20534,12 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20172
20534
  } else {
20173
20535
  logger.log(`[${agentName} Session ${sessionId}] No active turn to inject \u2014 queuing message`);
20174
20536
  enqueueBusy();
20537
+ drainQueueHeadIfIdle();
20175
20538
  }
20176
20539
  }).catch((err) => {
20177
20540
  logger.error(`[${agentName} Session ${sessionId}] Mid-turn inject failed \u2014 queuing:`, err?.message ?? err);
20178
20541
  enqueueBusy();
20542
+ drainQueueHeadIfIdle();
20179
20543
  });
20180
20544
  return;
20181
20545
  }
@@ -20391,11 +20755,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20391
20755
  });
20392
20756
  },
20393
20757
  onIssue: async (params) => {
20394
- const { issueRpc } = await import('./rpc-K5xzddMF.mjs');
20758
+ const { issueRpc } = await import('./rpc-CkcBAtcs.mjs');
20395
20759
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
20396
20760
  },
20397
20761
  onWorkflow: async (params) => {
20398
- const { workflowRpc } = await import('./rpc-DSyBSuTF.mjs');
20762
+ const { workflowRpc } = await import('./rpc-BwxwusfB.mjs');
20399
20763
  return workflowRpc(params?.cwd || directory, params || {});
20400
20764
  },
20401
20765
  onRipgrep: async (args, cwd) => {
@@ -20427,6 +20791,9 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20427
20791
  await fs$1.mkdir(dirname$1(resolvedPath), { recursive: true });
20428
20792
  await fs$1.writeFile(resolvedPath, Buffer.from(content, "base64"));
20429
20793
  },
20794
+ onWriteFileChunk: async (path, content, uploadId, chunkIndex, totalChunks, isLast) => {
20795
+ await writeSessionFileChunk(directory, sessionMetadata, path, content, uploadId, chunkIndex, isLast);
20796
+ },
20430
20797
  onListDirectory: async (path) => {
20431
20798
  const resolvedDir = resolve$1(directory, path || ".");
20432
20799
  if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
@@ -20436,8 +20803,15 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20436
20803
  return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
20437
20804
  },
20438
20805
  onGetDirectoryTree: async (treePath, maxDepth) => {
20806
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svamp", ".expo", "dist", "build", ".next", ".cache", ".venv", "venv", "__pycache__", ".turbo", ".gradle", "target"]);
20807
+ const MAX_TREE_DEPTH = 8;
20808
+ const MAX_TREE_NODES = 2e4;
20809
+ const effectiveMaxDepth = Math.min(Math.max(0, Math.floor(Number(maxDepth)) || 0), MAX_TREE_DEPTH);
20810
+ let treeNodeCount = 0;
20439
20811
  async function buildTree(p, name, depth) {
20440
20812
  try {
20813
+ if (treeNodeCount >= MAX_TREE_NODES) return null;
20814
+ treeNodeCount++;
20441
20815
  const stats = await fs$1.stat(p);
20442
20816
  const node = {
20443
20817
  name,
@@ -20446,11 +20820,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
20446
20820
  size: stats.size,
20447
20821
  modified: stats.mtime.getTime()
20448
20822
  };
20449
- if (stats.isDirectory() && depth < maxDepth) {
20823
+ if (stats.isDirectory() && depth < effectiveMaxDepth) {
20450
20824
  const entries = await fs$1.readdir(p, { withFileTypes: true });
20451
20825
  const children = [];
20452
20826
  await Promise.all(entries.map(async (entry) => {
20453
20827
  if (entry.isSymbolicLink()) return;
20828
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) return;
20829
+ if (treeNodeCount >= MAX_TREE_NODES) return;
20454
20830
  const childPath = join(p, entry.name);
20455
20831
  const childNode = await buildTree(childPath, entry.name, depth + 1);
20456
20832
  if (childNode) children.push(childNode);
@@ -21341,7 +21717,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
21341
21717
  }
21342
21718
  if (persistedSessions.length > 0) {
21343
21719
  try {
21344
- const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-BaMR97BE.mjs');
21720
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-C6MSkqS9.mjs');
21345
21721
  await awaitClaudeVersionReady();
21346
21722
  } catch {
21347
21723
  }
@@ -21546,7 +21922,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
21546
21922
  const PING_TIMEOUT_MS = 15e3;
21547
21923
  const POST_RECONNECT_GRACE_MS = 2e4;
21548
21924
  const RECONNECT_JITTER_MS = 2500;
21549
- const { WorkflowScheduler } = await import('./scheduler-CtKlSyIq.mjs');
21925
+ const { WorkflowScheduler } = await import('./scheduler-CurN4UtP.mjs');
21550
21926
  const workflowProjectRoots = () => {
21551
21927
  const dirs = /* @__PURE__ */ new Set();
21552
21928
  for (const s of pidToTrackedSession.values()) {
@@ -21835,6 +22211,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
21835
22211
  logger.log(`Cleaning up (source: ${source})...`);
21836
22212
  clearInterval(heartbeatInterval);
21837
22213
  clearInterval(workflowSchedulerInterval);
22214
+ clearInterval(backendAccountRefreshInterval);
21838
22215
  if (proxyTokenRefreshInterval) clearInterval(proxyTokenRefreshInterval);
21839
22216
  if (oauthRefreshInterval) clearInterval(oauthRefreshInterval);
21840
22217
  if (unhandledRejectionResetTimer) clearTimeout(unhandledRejectionResetTimer);
@@ -22179,4 +22556,4 @@ var run = /*#__PURE__*/Object.freeze({
22179
22556
  writeStopMarker: writeStopMarker
22180
22557
  });
22181
22558
 
22182
- export { downloadSkillFile as $, removeWorkflow as A, validateWorkflowName as B, saveWorkflow as C, rawWorkflow as D, listWorkflows as E, isWorkflowEnabled as F, workflowCrons as G, cronMatches as H, summarize as I, workflowSteps as J, parseJwtEmail as K, computeCollectionConfigUpdate as L, SYSTEM_COLLECTION_CONFIG as M, loadMachineContext as N, buildMachineInstructions as O, machineToolsForRole as P, buildMachineTools as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, parseFrontmatter as T, getSkillsServer as U, getSkillsWorkspaceName as V, getSkillsCollectionName as W, fetchWithTimeout as X, searchSkills as Y, SKILLS_DIR as Z, getSkillInfo as _, createSessionStore as a, listSkillFiles as a0, resolveModel as a1, clearStopMarker as a2, stopMarkerExists as a3, formatHandle as a4, normalizeAllowedUser as a5, loadSecurityContextConfig as a6, resolveSecurityContext as a7, buildSecurityContextFromFlags as a8, mergeSecurityContexts as a9, buildSessionShareUrl as aa, computeOutboundHop as ab, registerAwaitingReply as ac, buildMachineShareUrl as ad, parseHandle as ae, handleMatchesMetadata as af, describeMisconfiguration as ag, buildMachineDeps as ah, applyClaudeProxyEnv as ai, composeSessionId as aj, generateFriendlyName as ak, generateHookSettings as al, staticFileServer as am, instanceConfig as an, claudeAuth as ao, codexProvider as ap, projectInfo as aq, DefaultTransport$1 as ar, acpBackend as as, acpAgentConfig as at, codexAppServerBackend as au, GeminiTransport$1 as av, api as aw, run as ax, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, resolveProjectRoot as j, getIssue as k, resumeIssue as l, addComment as m, addIssue as n, listIssues as o, pauseIssue as p, searchIssues as q, registerMachineService as r, startDaemon as s, isVisibleTo as t, updateIssue as u, getRun as v, listRuns as w, getWorkflow as x, runWorkflow as y, setWorkflowEnabled as z };
22559
+ export { SKILLS_DIR as $, removeWorkflow as A, validateWorkflowName as B, saveWorkflow as C, rawWorkflow as D, listWorkflows as E, isWorkflowEnabled as F, workflowSchedules as G, inZone as H, cronMatches as I, summarize as J, workflowSteps as K, parseJwtEmail as L, computeCollectionConfigUpdate as M, SYSTEM_COLLECTION_CONFIG as N, loadMachineContext as O, buildMachineInstructions as P, machineToolsForRole as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, buildMachineTools as T, PINNED_CODEX_VERSION as U, parseFrontmatter as V, getSkillsServer as W, getSkillsWorkspaceName as X, getSkillsCollectionName as Y, fetchWithTimeout as Z, searchSkills as _, createSessionStore as a, getSkillInfo as a0, downloadSkillFile as a1, listSkillFiles as a2, resolveModel as a3, clearStopMarker as a4, stopMarkerExists as a5, formatHandle as a6, normalizeAllowedUser as a7, loadSecurityContextConfig as a8, resolveSecurityContext as a9, run as aA, buildSecurityContextFromFlags as aa, mergeSecurityContexts as ab, buildSessionShareUrl as ac, computeOutboundHop as ad, registerAwaitingReply as ae, buildMachineShareUrl as af, parseHandle as ag, handleMatchesMetadata as ah, withFileLock as ai, describeMisconfiguration as aj, buildMachineDeps as ak, applyClaudeProxyEnv as al, composeSessionId as am, generateFriendlyName as an, generateHookSettings as ao, staticFileServer as ap, instanceConfig as aq, claudeAuth as ar, codexProvider as as, projectInfo as at, DefaultTransport$1 as au, acpBackend as av, acpAgentConfig as aw, codexAppServerBackend as ax, GeminiTransport$1 as ay, api as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, resolveProjectRoot as j, getIssue as k, resumeIssue as l, addComment as m, addIssue as n, listIssues as o, pauseIssue as p, searchIssues as q, registerMachineService as r, startDaemon as s, isVisibleTo as t, updateIssue as u, getRun as v, listRuns as w, getWorkflow as x, runWorkflow as y, setWorkflowEnabled as z };