@madarco/agentbox 0.13.0 → 0.15.0

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 (74) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +11 -8
  3. package/dist/{_cloud-attach-HJC672UR.js → _cloud-attach-R6TRWG5L.js} +4 -4
  4. package/dist/{chunk-QYRK5H6Q.js → chunk-43Q5GWP6.js} +108 -56
  5. package/dist/chunk-43Q5GWP6.js.map +1 -0
  6. package/dist/{chunk-ECLLV5JH.js → chunk-72CJTXN6.js} +156 -5
  7. package/dist/chunk-72CJTXN6.js.map +1 -0
  8. package/dist/{chunk-R5XIDQFR.js → chunk-BKU34KYY.js} +170 -6
  9. package/dist/chunk-BKU34KYY.js.map +1 -0
  10. package/dist/{chunk-4NQXNQ53.js → chunk-E7CHS7ZR.js} +168 -58
  11. package/dist/chunk-E7CHS7ZR.js.map +1 -0
  12. package/dist/chunk-MCOU6CZS.js +346 -0
  13. package/dist/chunk-MCOU6CZS.js.map +1 -0
  14. package/dist/{chunk-B4QG2MCW.js → chunk-MLMFNN4T.js} +762 -483
  15. package/dist/chunk-MLMFNN4T.js.map +1 -0
  16. package/dist/{chunk-2LF5YILI.js → chunk-RSKG7AFU.js} +80 -6
  17. package/dist/chunk-RSKG7AFU.js.map +1 -0
  18. package/dist/{chunk-SNTHHWKY.js → chunk-XKH7NTT7.js} +80 -22
  19. package/dist/chunk-XKH7NTT7.js.map +1 -0
  20. package/dist/{dist-7KVUIKJX.js → dist-AGTIA7AD.js} +37 -226
  21. package/dist/dist-AGTIA7AD.js.map +1 -0
  22. package/dist/{dist-OPIBZ7XM.js → dist-FIFEFKJ7.js} +14 -69
  23. package/dist/dist-FIFEFKJ7.js.map +1 -0
  24. package/dist/dist-JZ3XO6EB.js +662 -0
  25. package/dist/dist-JZ3XO6EB.js.map +1 -0
  26. package/dist/{dist-OG6NW6SM.js → dist-OGJGZETZ.js} +5 -3
  27. package/dist/{dist-JAN5VABY.js → dist-S4XR4ACV.js} +25 -177
  28. package/dist/dist-S4XR4ACV.js.map +1 -0
  29. package/dist/index.js +2229 -1314
  30. package/dist/index.js.map +1 -1
  31. package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js → prepared-state-MQHD3M5F-Q27AZU53.js} +2 -2
  32. package/package.json +6 -4
  33. package/runtime/docker/Dockerfile.box +21 -26
  34. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +67 -1
  35. package/runtime/docker/packages/ctl/dist/bin.cjs +361 -43
  36. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +17 -6
  37. package/runtime/docker/packages/sandbox-docker/scripts/chromium-resolver +57 -0
  38. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +2 -1
  39. package/runtime/e2b/agentbox-checkpoint-cleanup +52 -0
  40. package/runtime/e2b/agentbox-codex-hooks.json +68 -0
  41. package/runtime/e2b/agentbox-open +28 -0
  42. package/runtime/e2b/agentbox-setup-skill.md +263 -0
  43. package/runtime/e2b/agentbox-vnc-start +102 -0
  44. package/runtime/e2b/attach-helper.cjs +167 -0
  45. package/runtime/e2b/claude-managed-settings.json +116 -0
  46. package/runtime/e2b/ctl.cjs +24158 -0
  47. package/runtime/e2b/custom-system-CLAUDE.md +46 -0
  48. package/runtime/e2b/gh-shim +344 -0
  49. package/runtime/e2b/git-shim +131 -0
  50. package/runtime/e2b/scripts/build-template.sh +295 -0
  51. package/runtime/hetzner/agentbox-setup-skill.md +67 -1
  52. package/runtime/hetzner/agentbox-vnc-start +17 -6
  53. package/runtime/hetzner/claude-managed-settings.json +2 -1
  54. package/runtime/hetzner/ctl.cjs +361 -43
  55. package/runtime/relay/bin.cjs +380 -233
  56. package/runtime/vercel/agentbox-setup-skill.md +67 -1
  57. package/runtime/vercel/agentbox-vnc-start +17 -6
  58. package/runtime/vercel/claude-managed-settings.json +2 -1
  59. package/runtime/vercel/ctl.cjs +361 -43
  60. package/share/agentbox-setup/SKILL.md +67 -1
  61. package/share/host-skills/agentbox-info/SKILL.md +47 -35
  62. package/dist/chunk-2LF5YILI.js.map +0 -1
  63. package/dist/chunk-4NQXNQ53.js.map +0 -1
  64. package/dist/chunk-B4QG2MCW.js.map +0 -1
  65. package/dist/chunk-ECLLV5JH.js.map +0 -1
  66. package/dist/chunk-QYRK5H6Q.js.map +0 -1
  67. package/dist/chunk-R5XIDQFR.js.map +0 -1
  68. package/dist/chunk-SNTHHWKY.js.map +0 -1
  69. package/dist/dist-7KVUIKJX.js.map +0 -1
  70. package/dist/dist-JAN5VABY.js.map +0 -1
  71. package/dist/dist-OPIBZ7XM.js.map +0 -1
  72. /package/dist/{_cloud-attach-HJC672UR.js.map → _cloud-attach-R6TRWG5L.js.map} +0 -0
  73. /package/dist/{dist-OG6NW6SM.js.map → dist-OGJGZETZ.js.map} +0 -0
  74. /package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js.map → prepared-state-MQHD3M5F-Q27AZU53.js.map} +0 -0
@@ -3057,15 +3057,15 @@ var require_windows = __commonJS({
3057
3057
  }
3058
3058
  return false;
3059
3059
  }
3060
- function checkStat(stat, path6, options) {
3061
- if (!stat.isSymbolicLink() && !stat.isFile()) {
3060
+ function checkStat(stat2, path6, options) {
3061
+ if (!stat2.isSymbolicLink() && !stat2.isFile()) {
3062
3062
  return false;
3063
3063
  }
3064
3064
  return checkPathExt(path6, options);
3065
3065
  }
3066
3066
  function isexe(path6, options, cb) {
3067
- fs.stat(path6, function(er, stat) {
3068
- cb(er, er ? false : checkStat(stat, path6, options));
3067
+ fs.stat(path6, function(er, stat2) {
3068
+ cb(er, er ? false : checkStat(stat2, path6, options));
3069
3069
  });
3070
3070
  }
3071
3071
  function sync(path6, options) {
@@ -3082,20 +3082,20 @@ var require_mode = __commonJS({
3082
3082
  isexe.sync = sync;
3083
3083
  var fs = require("fs");
3084
3084
  function isexe(path6, options, cb) {
3085
- fs.stat(path6, function(er, stat) {
3086
- cb(er, er ? false : checkStat(stat, options));
3085
+ fs.stat(path6, function(er, stat2) {
3086
+ cb(er, er ? false : checkStat(stat2, options));
3087
3087
  });
3088
3088
  }
3089
3089
  function sync(path6, options) {
3090
3090
  return checkStat(fs.statSync(path6), options);
3091
3091
  }
3092
- function checkStat(stat, options) {
3093
- return stat.isFile() && checkMode(stat, options);
3092
+ function checkStat(stat2, options) {
3093
+ return stat2.isFile() && checkMode(stat2, options);
3094
3094
  }
3095
- function checkMode(stat, options) {
3096
- var mod = stat.mode;
3097
- var uid = stat.uid;
3098
- var gid = stat.gid;
3095
+ function checkMode(stat2, options) {
3096
+ var mod = stat2.mode;
3097
+ var uid = stat2.uid;
3098
+ var gid = stat2.gid;
3099
3099
  var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid();
3100
3100
  var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid();
3101
3101
  var u2 = parseInt("100", 8);
@@ -11808,11 +11808,11 @@ var replacements = Object.entries(specialMainSymbols);
11808
11808
  // ../../node_modules/.pnpm/yoctocolors@2.1.2/node_modules/yoctocolors/base.js
11809
11809
  var import_node_tty = __toESM(require("tty"), 1);
11810
11810
  var hasColors = import_node_tty.default?.WriteStream?.prototype?.hasColors?.() ?? false;
11811
- var format = (open, close) => {
11811
+ var format = (open2, close) => {
11812
11812
  if (!hasColors) {
11813
11813
  return (input) => input;
11814
11814
  }
11815
- const openCode = `\x1B[${open}m`;
11815
+ const openCode = `\x1B[${open2}m`;
11816
11816
  const closeCode = `\x1B[${close}m`;
11817
11817
  return (input) => {
11818
11818
  const string = input + "";
@@ -18367,6 +18367,21 @@ var HostInitiatedTokens = class {
18367
18367
  var import_node_crypto2 = require("crypto");
18368
18368
  var PendingPrompts = class {
18369
18369
  entries = /* @__PURE__ */ new Map();
18370
+ autoApprove = null;
18371
+ /** Install the per-box auto-approve policy (relay server, once at startup). */
18372
+ setAutoApprovePolicy(policy) {
18373
+ this.autoApprove = policy;
18374
+ }
18375
+ /**
18376
+ * True when this box opted into `box.autoApproveHostActions`. Records the
18377
+ * bypass to the audit sink as a side effect so the caller short-circuits
18378
+ * with a trail. Returns false when no policy is installed.
18379
+ */
18380
+ consumeAutoApprove(boxId, params) {
18381
+ if (!this.autoApprove || !this.autoApprove.shouldAutoApprove(boxId)) return false;
18382
+ this.autoApprove.audit(boxId, params);
18383
+ return true;
18384
+ }
18370
18385
  add(boxId, ev) {
18371
18386
  return new Promise((resolve2) => {
18372
18387
  this.entries.set(ev.id, {
@@ -18453,6 +18468,9 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
18453
18468
  if (process.env.AGENTBOX_PROMPT === "off") {
18454
18469
  return { answer: "y" };
18455
18470
  }
18471
+ if (prompts.consumeAutoApprove(boxId, params)) {
18472
+ return { answer: "y" };
18473
+ }
18456
18474
  const ev = { id: (0, import_node_crypto2.randomUUID)(), ...params };
18457
18475
  const promise = prompts.add(boxId, ev);
18458
18476
  subscribers.broadcast(boxId, "prompt-ask", ev);
@@ -18492,6 +18510,10 @@ async function resolveCloudBackend(name) {
18492
18510
  const pkg = "@agentbox/sandbox-vercel";
18493
18511
  return loadCloudBackend(pkg, async () => (await import(pkg)).vercelBackend);
18494
18512
  }
18513
+ if (name === "e2b") {
18514
+ const pkg = "@agentbox/sandbox-e2b";
18515
+ return loadCloudBackend(pkg, async () => (await import(pkg)).e2bBackend);
18516
+ }
18495
18517
  throw new Error(`no host executor for cloud backend '${name}'`);
18496
18518
  }
18497
18519
  async function loadCloudBackend(pkg, load2) {
@@ -19426,6 +19448,21 @@ function createRelayServer(opts) {
19426
19448
  const prompts = new PendingPrompts();
19427
19449
  const subscribers = new PromptSubscribers();
19428
19450
  const notices = new BoxNotices(subscribers);
19451
+ prompts.setAutoApprovePolicy({
19452
+ shouldAutoApprove: (boxId) => registry.get(boxId)?.autoApproveHostActions === true,
19453
+ audit: (boxId, params) => {
19454
+ events.append({
19455
+ boxId,
19456
+ type: "host-action-auto-approved",
19457
+ payload: {
19458
+ command: params.context?.command,
19459
+ argv: params.context?.argv,
19460
+ message: params.message
19461
+ }
19462
+ });
19463
+ log(`auto-approved host action for ${boxId}: ${params.context?.command ?? params.message}`);
19464
+ }
19465
+ });
19429
19466
  const hostInitiatedTokens = new HostInitiatedTokens();
19430
19467
  let queuePoke = null;
19431
19468
  const host = opts.host ?? "0.0.0.0";
@@ -19613,11 +19650,22 @@ function createRelayServer(opts) {
19613
19650
  send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
19614
19651
  return;
19615
19652
  }
19653
+ if (params.exclude !== void 0 && (!Array.isArray(params.exclude) || params.exclude.some((p) => typeof p !== "string"))) {
19654
+ send(res, 400, { error: "cp.* exclude must be an array of strings" });
19655
+ return;
19656
+ }
19616
19657
  const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
19658
+ const pathDetail = body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`;
19659
+ const detailParts = [pathDetail];
19660
+ if (params.exclude && params.exclude.length > 0) {
19661
+ detailParts.push(`exclude: ${params.exclude.join(", ")}`);
19662
+ }
19663
+ if (params.defaultExcludes === false) detailParts.push("(default excludes off)");
19664
+ if (params.yes) detailParts.push("(over size limit \u2014 confirmed)");
19617
19665
  const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
19618
19666
  kind: "confirm",
19619
19667
  message: `Allow cp (${direction}) on ${reg.name}?`,
19620
- detail: body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`,
19668
+ detail: detailParts.join("\n"),
19621
19669
  defaultAnswer: "n",
19622
19670
  context: {
19623
19671
  command: body.method,
@@ -19783,7 +19831,8 @@ function createRelayServer(opts) {
19783
19831
  worktrees,
19784
19832
  previewUrl: typeof body.previewUrl === "string" && body.previewUrl.length > 0 ? body.previewUrl : void 0,
19785
19833
  previewToken: typeof body.previewToken === "string" && body.previewToken.length > 0 ? body.previewToken : void 0,
19786
- bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0
19834
+ bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0,
19835
+ autoApproveHostActions: body.autoApproveHostActions === true
19787
19836
  };
19788
19837
  registry.register(reg);
19789
19838
  log(
@@ -19891,6 +19940,15 @@ function createRelayServer(opts) {
19891
19940
  send(res, 200, { boxes: redacted });
19892
19941
  return;
19893
19942
  }
19943
+ if (route === "GET /admin/prompts") {
19944
+ const boxId = url.searchParams.get("boxId") ?? "";
19945
+ if (boxId.length === 0) {
19946
+ send(res, 400, { error: "missing boxId query param" });
19947
+ return;
19948
+ }
19949
+ send(res, 200, { prompts: prompts.forBox(boxId) });
19950
+ return;
19951
+ }
19894
19952
  if (route === "GET /admin/prompts/stream") {
19895
19953
  const boxId = url.searchParams.get("boxId") ?? "";
19896
19954
  if (boxId.length === 0) {
@@ -20208,7 +20266,11 @@ async function handleCpRpc(reg, method, params) {
20208
20266
  };
20209
20267
  }
20210
20268
  const boxRef = `${reg.name}:${params.boxPath}`;
20211
- const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath] : [process.execPath, entry, "cp", params.hostPath, boxRef];
20269
+ const flags = [];
20270
+ for (const pat of params.exclude ?? []) flags.push("--exclude", pat);
20271
+ if (params.defaultExcludes === false) flags.push("--no-default-excludes");
20272
+ if (params.yes) flags.push("--yes");
20273
+ const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath, ...flags] : [process.execPath, entry, "cp", params.hostPath, boxRef, ...flags];
20212
20274
  return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
20213
20275
  }
20214
20276
  async function handleDownloadRpc(reg, kind) {
@@ -20306,8 +20368,8 @@ async function startRelayServer(opts) {
20306
20368
  }
20307
20369
 
20308
20370
  // src/autopause.ts
20309
- var import_node_child_process8 = require("child_process");
20310
- var import_promises16 = require("fs/promises");
20371
+ var import_node_child_process9 = require("child_process");
20372
+ var import_promises18 = require("fs/promises");
20311
20373
 
20312
20374
  // ../config/dist/index.js
20313
20375
  var import_yaml = __toESM(require_dist(), 1);
@@ -20325,15 +20387,18 @@ var BUILT_IN_DEFAULTS = {
20325
20387
  defaultCheckpointDaytona: "",
20326
20388
  defaultCheckpointHetzner: "",
20327
20389
  defaultCheckpointVercel: "",
20390
+ defaultCheckpointE2b: "",
20328
20391
  size: "",
20329
20392
  sizeDocker: "",
20330
20393
  sizeDaytona: "",
20331
20394
  sizeHetzner: "",
20332
20395
  sizeVercel: "",
20396
+ sizeE2b: "",
20333
20397
  withPlaywright: false,
20334
20398
  withEnv: false,
20335
20399
  resyncOnStart: true,
20336
20400
  vnc: true,
20401
+ autoApproveHostActions: false,
20337
20402
  isolateClaudeConfig: false,
20338
20403
  isolateCodexConfig: false,
20339
20404
  isolateOpencodeConfig: false,
@@ -20342,6 +20407,7 @@ var BUILT_IN_DEFAULTS = {
20342
20407
  imageDaytona: "",
20343
20408
  imageHetzner: "",
20344
20409
  imageVercel: "",
20410
+ imageE2b: "",
20345
20411
  // Mirrors BOX_IMAGE_REGISTRY in @agentbox/sandbox-docker. Empty disables the
20346
20412
  // registry pull (always build the docker base image locally).
20347
20413
  imageRegistry: "ghcr.io/madarco/agentbox/box",
@@ -20353,7 +20419,8 @@ var BUILT_IN_DEFAULTS = {
20353
20419
  bundleDepth: void 0,
20354
20420
  vercelVcpus: 2,
20355
20421
  vercelTimeoutMs: 27e5,
20356
- vercelNetworkPolicy: ""
20422
+ vercelNetworkPolicy: "",
20423
+ cpMaxBytes: 100 * 1024 * 1024
20357
20424
  },
20358
20425
  checkpoint: {
20359
20426
  maxLayers: 3
@@ -20409,7 +20476,8 @@ var BUILT_IN_DEFAULTS = {
20409
20476
  enabled: true,
20410
20477
  maxConcurrent: 5,
20411
20478
  maxWorking: 0,
20412
- idleGraceSeconds: 15
20479
+ idleGraceSeconds: 15,
20480
+ openIn: "none"
20413
20481
  },
20414
20482
  cloud: {
20415
20483
  useCurrentBranch: false
@@ -20423,8 +20491,8 @@ var KEY_REGISTRY = [
20423
20491
  {
20424
20492
  key: "box.provider",
20425
20493
  type: "enum",
20426
- enumValues: ["docker", "daytona", "hetzner", "vercel"],
20427
- description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
20494
+ enumValues: ["docker", "daytona", "hetzner", "vercel", "e2b"],
20495
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, Vercel Sandboxes, or E2B microVMs."
20428
20496
  },
20429
20497
  {
20430
20498
  key: "box.hostSnapshot",
@@ -20460,6 +20528,12 @@ var KEY_REGISTRY = [
20460
20528
  description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
20461
20529
  advanced: true
20462
20530
  },
20531
+ {
20532
+ key: "box.defaultCheckpointE2b",
20533
+ type: "string",
20534
+ description: "Per-provider override of `box.defaultCheckpoint` for e2b. Wins over the global when set; set via `agentbox checkpoint set-default --provider e2b`.",
20535
+ advanced: true
20536
+ },
20463
20537
  {
20464
20538
  key: "box.size",
20465
20539
  type: "string",
@@ -20489,6 +20563,12 @@ var KEY_REGISTRY = [
20489
20563
  description: "Per-provider override of `box.size` for vercel. Reserved \u2014 vercel sizing is controlled via `box.vercelVcpus`.",
20490
20564
  advanced: true
20491
20565
  },
20566
+ {
20567
+ key: "box.sizeE2b",
20568
+ type: "string",
20569
+ description: "Per-provider override of `box.size` for e2b. Reserved \u2014 e2b sizing is template-level (set at `agentbox prepare --provider e2b` time via --vcpus / --memory).",
20570
+ advanced: true
20571
+ },
20492
20572
  {
20493
20573
  key: "checkpoint.maxLayers",
20494
20574
  type: "int",
@@ -20515,6 +20595,11 @@ var KEY_REGISTRY = [
20515
20595
  type: "bool",
20516
20596
  description: "Run the per-box Xvnc + noVNC stack."
20517
20597
  },
20598
+ {
20599
+ key: "box.autoApproveHostActions",
20600
+ type: "bool",
20601
+ description: "Auto-approve host-action confirmations (git push, cp host<->box, gh PR writes, checkpoint) for this box without an interactive prompt. Off by default; intended for unattended orchestration of trusted boxes. Each auto-approval is recorded as a relay event (visible in `agentbox agent` / the dashboard)."
20602
+ },
20518
20603
  {
20519
20604
  key: "box.isolateClaudeConfig",
20520
20605
  type: "bool",
@@ -20560,6 +20645,12 @@ var KEY_REGISTRY = [
20560
20645
  description: "Per-provider override of `box.image` for vercel (snapshot id, e.g. `snap_\u2026`). Written by `agentbox prepare --provider vercel`.",
20561
20646
  advanced: true
20562
20647
  },
20648
+ {
20649
+ key: "box.imageE2b",
20650
+ type: "string",
20651
+ description: "Per-provider override of `box.image` for e2b (template id or `name:tag`, e.g. `agentbox-base:latest`). Written by `agentbox prepare --provider e2b`.",
20652
+ advanced: true
20653
+ },
20563
20654
  {
20564
20655
  key: "box.imageRegistry",
20565
20656
  type: "string",
@@ -20607,6 +20698,12 @@ var KEY_REGISTRY = [
20607
20698
  type: "int",
20608
20699
  description: "Max session length (ms) for new --provider vercel boxes before the VM auto-snapshots; persistent mode auto-resumes on the next call. Default 2700000 (45 min, the Hobby ceiling). Vercel-only."
20609
20700
  },
20701
+ {
20702
+ key: "box.cpMaxBytes",
20703
+ type: "int",
20704
+ description: "Max bytes a single host\u2192box copy may transfer after excludes, shared by `agentbox cp` (blocked with a size breakdown unless --yes) and each `carry:` entry (rejected at resolve time). Default 104857600 (100 MiB).",
20705
+ advanced: true
20706
+ },
20610
20707
  {
20611
20708
  key: "box.vercelNetworkPolicy",
20612
20709
  type: "string",
@@ -20754,6 +20851,12 @@ var KEY_REGISTRY = [
20754
20851
  type: "int",
20755
20852
  description: "Seconds an agent must stay non-working before it frees its working slot (debounce against brief idle flaps between turns). Only used when queue.maxWorking > 0."
20756
20853
  },
20854
+ {
20855
+ key: "queue.openIn",
20856
+ type: "enum",
20857
+ enumValues: ["none", "split", "window", "tab"],
20858
+ description: "When a background `-i` job finishes creating its box, where the host relay opens an attached terminal onto it: `none` (default \u2014 open nothing, just queue), `split`, `window`, or `tab`. Honored only when the submitting shell runs inside tmux, cmux, or iTerm2 (the targeting is captured at submit time). Under cmux, `split` splits the pane you submitted from (falling back to the parent workspace, then a new workspace), `tab` adds a tab in the parent workspace, and `window` opens a separate workspace; iTerm2 opens relative to the frontmost window. Unlike `attach.openIn` there is no `same` mode \u2014 the box is created asynchronously, so it is always a fresh terminal."
20859
+ },
20757
20860
  {
20758
20861
  key: "cloud.useCurrentBranch",
20759
20862
  type: "bool",
@@ -20900,210 +21003,18 @@ var GLOBAL_CONFIG_FILE = (0, import_path2.join)(STATE_DIR2, "config.yaml");
20900
21003
  var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
20901
21004
  var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
20902
21005
 
20903
- // src/autopause.ts
20904
- function selectBoxesToPause(entries, cfg) {
20905
- if (!cfg.enabled) return [];
20906
- const runningCount = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
20907
- const excess = runningCount - cfg.maxRunningBoxes;
20908
- if (excess <= 0) return [];
20909
- const idleThresholdMs = cfg.idleMinutes * 6e4;
20910
- const candidates = entries.filter(
20911
- (e) => e.running && e.claudeState === "idle" && e.idleMs != null && e.idleMs >= idleThresholdMs
20912
- );
20913
- candidates.sort(
20914
- (a2, b) => b.idleMs - a2.idleMs || a2.createdAt - b.createdAt || (a2.boxId < b.boxId ? -1 : a2.boxId > b.boxId ? 1 : 0)
20915
- );
20916
- return candidates.slice(0, excess).map((e) => e.boxId);
20917
- }
20918
- async function loadAutopauseConfig() {
20919
- const d = BUILT_IN_DEFAULTS.autopause;
20920
- let global3 = {};
20921
- try {
20922
- global3 = parseUserConfig(await (0, import_promises16.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
20923
- } catch {
20924
- }
20925
- const a2 = global3.autopause ?? {};
20926
- return {
20927
- enabled: a2.enabled ?? d.enabled,
20928
- maxRunningBoxes: a2.maxRunningBoxes ?? d.maxRunningBoxes,
20929
- idleMinutes: a2.idleMinutes ?? d.idleMinutes
20930
- };
20931
- }
20932
- var DEFAULT_INTERVAL_MS = 6e4;
20933
- function startAutopauseLoop(deps) {
20934
- const loadConfig = deps.loadConfig ?? loadAutopauseConfig;
20935
- const inspectStatus = deps.inspectStatus ?? inspectContainerState;
20936
- const pause = deps.pause ?? pauseContainer;
20937
- const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
20938
- const { registry, statusStore, events, log } = deps;
20939
- let ticking = false;
20940
- let stopped = false;
20941
- let inFlight = Promise.resolve();
20942
- async function tick() {
20943
- if (ticking) return;
20944
- ticking = true;
20945
- try {
20946
- const cfg = await loadConfig();
20947
- if (!cfg.enabled) return;
20948
- const entries = [];
20949
- for (const reg of registry.list()) {
20950
- if (!reg.containerName) continue;
20951
- const state = await inspectStatus(reg.containerName);
20952
- const claude = readClaude(statusStore.get(reg.boxId));
20953
- const idleMs = claude.state === "idle" && claude.updatedAt ? msSince(claude.updatedAt) : null;
20954
- entries.push({
20955
- boxId: reg.boxId,
20956
- containerName: reg.containerName,
20957
- running: state === "running",
20958
- claudeState: claude.state,
20959
- idleMs,
20960
- createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0
20961
- });
20962
- }
20963
- const toPause = selectBoxesToPause(entries, cfg);
20964
- if (toPause.length === 0) return;
20965
- const byId = new Map(entries.map((e) => [e.boxId, e]));
20966
- const runningBefore = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
20967
- for (const boxId of toPause) {
20968
- const e = byId.get(boxId);
20969
- if (!e) continue;
20970
- try {
20971
- await pause(e.containerName);
20972
- const mins = e.idleMs != null ? Math.round(e.idleMs / 6e4) : null;
20973
- events.append({
20974
- boxId,
20975
- type: "autopause",
20976
- payload: {
20977
- containerName: e.containerName,
20978
- action: "paused",
20979
- idleMs: e.idleMs,
20980
- runningBefore,
20981
- max: cfg.maxRunningBoxes
20982
- }
20983
- });
20984
- log(
20985
- `autopause: paused box ${boxId} (${e.containerName})` + (mins != null ? ` after ~${String(mins)}m idle` : "") + `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`
20986
- );
20987
- } catch (err) {
20988
- const msg = err instanceof Error ? err.message : String(err);
20989
- log(`autopause: docker pause ${e.containerName} failed: ${msg}`);
20990
- events.append({
20991
- boxId,
20992
- type: "autopause",
20993
- payload: { containerName: e.containerName, action: "pause-failed", error: msg }
20994
- });
20995
- }
20996
- }
20997
- } catch (err) {
20998
- const msg = err instanceof Error ? err.message : String(err);
20999
- log(`autopause: tick error: ${msg}`);
21000
- } finally {
21001
- ticking = false;
21002
- }
21003
- }
21004
- const timer = setInterval(() => {
21005
- if (stopped) return;
21006
- inFlight = tick();
21007
- }, intervalMs);
21008
- timer.unref();
21009
- return {
21010
- stop: async () => {
21011
- stopped = true;
21012
- clearInterval(timer);
21013
- await inFlight.catch(() => {
21014
- });
21015
- }
21016
- };
21017
- }
21018
- function readClaude(snap) {
21019
- const c3 = snap && typeof snap === "object" ? snap.claude : void 0;
21020
- if (!c3 || typeof c3 !== "object") return { state: null, updatedAt: null };
21021
- const o2 = c3;
21022
- const state = o2.state === "working" || o2.state === "idle" || o2.state === "waiting" || o2.state === "unknown" ? o2.state : null;
21023
- return { state, updatedAt: typeof o2.updatedAt === "string" ? o2.updatedAt : null };
21024
- }
21025
- function msSince(iso) {
21026
- const t = Date.parse(iso);
21027
- return Number.isNaN(t) ? null : Date.now() - t;
21028
- }
21029
- function toEpoch(iso) {
21030
- const t = Date.parse(iso);
21031
- return Number.isNaN(t) ? 0 : t;
21032
- }
21033
- var INSPECT_TIMEOUT_MS = 15e3;
21034
- var PAUSE_TIMEOUT_MS = 3e4;
21035
- function runDocker(args, timeoutMs) {
21036
- return new Promise((resolve2) => {
21037
- const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
21038
- let stdout = "";
21039
- let stderr = "";
21040
- let settled = false;
21041
- const finish = (exitCode) => {
21042
- if (settled) return;
21043
- settled = true;
21044
- resolve2({ exitCode, stdout, stderr });
21045
- };
21046
- const timer = setTimeout(() => {
21047
- child.kill("SIGTERM");
21048
- stderr += `
21049
- relay: docker ${args.join(" ")} timed out after ${String(timeoutMs)}ms
21050
- `;
21051
- finish(124);
21052
- }, timeoutMs);
21053
- child.stdout?.on("data", (c3) => {
21054
- stdout += c3.toString("utf8");
21055
- });
21056
- child.stderr?.on("data", (c3) => {
21057
- stderr += c3.toString("utf8");
21058
- });
21059
- child.on("error", (err) => {
21060
- clearTimeout(timer);
21061
- stderr += String(err.message ?? err);
21062
- finish(127);
21063
- });
21064
- child.on("close", (code) => {
21065
- clearTimeout(timer);
21066
- finish(code ?? -1);
21067
- });
21068
- });
21069
- }
21070
- async function inspectContainerState(name) {
21071
- const r = await runDocker(["inspect", "--format", "{{.State.Status}}", name], INSPECT_TIMEOUT_MS);
21072
- if (r.exitCode !== 0) return "missing";
21073
- switch (r.stdout.trim()) {
21074
- case "running":
21075
- return "running";
21076
- case "paused":
21077
- return "paused";
21078
- case "created":
21079
- case "exited":
21080
- case "dead":
21081
- case "restarting":
21082
- case "removing":
21083
- return "stopped";
21084
- default:
21085
- return "missing";
21086
- }
21087
- }
21088
- async function pauseContainer(name) {
21089
- const r = await runDocker(["pause", name], PAUSE_TIMEOUT_MS);
21090
- if (r.exitCode !== 0) {
21091
- throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);
21092
- }
21093
- }
21094
-
21095
21006
  // src/queue.ts
21096
- var import_node_child_process9 = require("child_process");
21097
- var import_promises17 = require("fs/promises");
21007
+ var import_node_child_process8 = require("child_process");
21008
+ var import_promises16 = require("fs/promises");
21098
21009
  var import_node_fs6 = require("fs");
21099
21010
  var import_node_path8 = require("path");
21100
- var import_promises18 = require("timers/promises");
21011
+ var import_promises17 = require("timers/promises");
21101
21012
  var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
21102
21013
  async function loadQueueConfig() {
21103
21014
  const d = BUILT_IN_DEFAULTS.queue;
21104
21015
  let global3 = {};
21105
21016
  try {
21106
- global3 = parseUserConfig(await (0, import_promises17.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
21017
+ global3 = parseUserConfig(await (0, import_promises16.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
21107
21018
  } catch {
21108
21019
  }
21109
21020
  const q = global3.queue ?? {};
@@ -21115,25 +21026,32 @@ async function loadQueueConfig() {
21115
21026
  };
21116
21027
  }
21117
21028
  async function writeJob(job) {
21118
- await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
21029
+ await (0, import_promises16.mkdir)(QUEUE_DIR, { recursive: true });
21119
21030
  const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
21120
21031
  const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
21121
- await (0, import_promises17.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
21122
- await (0, import_promises17.rename)(tmp, final);
21032
+ await (0, import_promises16.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
21033
+ await (0, import_promises16.rename)(tmp, final);
21123
21034
  }
21124
21035
  async function readJob(id) {
21125
21036
  try {
21126
- const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
21037
+ const raw = await (0, import_promises16.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
21127
21038
  return JSON.parse(raw);
21128
21039
  } catch (err) {
21129
21040
  if (err.code === "ENOENT") return null;
21130
21041
  throw err;
21131
21042
  }
21132
21043
  }
21044
+ async function deleteJob(id) {
21045
+ try {
21046
+ await (0, import_promises16.unlink)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`));
21047
+ } catch (err) {
21048
+ if (err.code !== "ENOENT") throw err;
21049
+ }
21050
+ }
21133
21051
  async function loadQueue() {
21134
21052
  let entries;
21135
21053
  try {
21136
- entries = await (0, import_promises17.readdir)(QUEUE_DIR);
21054
+ entries = await (0, import_promises16.readdir)(QUEUE_DIR);
21137
21055
  } catch (err) {
21138
21056
  if (err.code === "ENOENT") return [];
21139
21057
  throw err;
@@ -21142,7 +21060,7 @@ async function loadQueue() {
21142
21060
  for (const name of entries) {
21143
21061
  if (!name.endsWith(".json")) continue;
21144
21062
  try {
21145
- const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
21063
+ const raw = await (0, import_promises16.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
21146
21064
  out.push(JSON.parse(raw));
21147
21065
  } catch {
21148
21066
  }
@@ -21216,22 +21134,23 @@ function parseTime(iso) {
21216
21134
  const t = Date.parse(iso);
21217
21135
  return Number.isNaN(t) ? 0 : t;
21218
21136
  }
21219
- function msSince2(iso) {
21137
+ function msSince(iso) {
21220
21138
  if (!iso) return null;
21221
21139
  const t = Date.parse(iso);
21222
21140
  return Number.isNaN(t) ? null : Date.now() - t;
21223
21141
  }
21224
- var DEFAULT_INTERVAL_MS2 = 2e3;
21142
+ var DEFAULT_INTERVAL_MS = 2e3;
21225
21143
  function startQueueLoop(deps) {
21226
21144
  const loadConfig = deps.loadConfig ?? loadQueueConfig;
21227
21145
  const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
21228
21146
  const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
21229
- const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
21147
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
21230
21148
  const { log, onStatusChange } = deps;
21231
21149
  const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
21232
21150
  let ticking = false;
21233
21151
  let stopped = false;
21234
21152
  let warnedNoWorkingDeps = false;
21153
+ let lastSweepAt = 0;
21235
21154
  let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
21236
21155
  log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
21237
21156
  });
@@ -21241,6 +21160,13 @@ function startQueueLoop(deps) {
21241
21160
  try {
21242
21161
  const cfg = await loadConfig();
21243
21162
  if (!cfg.enabled) return;
21163
+ const now = Date.now();
21164
+ if (now - lastSweepAt >= SWEEP_INTERVAL_MS) {
21165
+ lastSweepAt = now;
21166
+ await sweepTerminalJobs(log, now).catch((err) => {
21167
+ log(`queue: sweep failed: ${err instanceof Error ? err.message : String(err)}`);
21168
+ });
21169
+ }
21244
21170
  const jobs = await loadQueue();
21245
21171
  const hasQueued = jobs.some((j) => j.status === "queued");
21246
21172
  if (!hasQueued) return;
@@ -21343,6 +21269,21 @@ async function recoverOrphanedWorkers(log, onChange) {
21343
21269
  log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
21344
21270
  }
21345
21271
  }
21272
+ var TERMINAL_RETENTION_MS = 60 * 60 * 1e3;
21273
+ var SWEEP_INTERVAL_MS = 60 * 1e3;
21274
+ var TERMINAL_STATUSES = ["done", "failed", "cancelled"];
21275
+ async function sweepTerminalJobs(log, now) {
21276
+ const jobs = await loadQueue();
21277
+ let swept = 0;
21278
+ for (const j of jobs) {
21279
+ if (!TERMINAL_STATUSES.includes(j.status)) continue;
21280
+ const since = Date.parse(j.finishedAt ?? j.createdAt);
21281
+ if (Number.isNaN(since) || now - since < TERMINAL_RETENTION_MS) continue;
21282
+ await deleteJob(j.id);
21283
+ swept += 1;
21284
+ }
21285
+ if (swept > 0) log(`queue: swept ${String(swept)} stale terminal manifest(s)`);
21286
+ }
21346
21287
  function processAlive(pid) {
21347
21288
  try {
21348
21289
  process.kill(pid, 0);
@@ -21359,8 +21300,8 @@ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
21359
21300
  return {
21360
21301
  key: b.boxId,
21361
21302
  agentState: active.state,
21362
- sinceUpdateMs: msSince2(active.updatedAt),
21363
- sinceCreateMs: msSince2(b.createdAt)
21303
+ sinceUpdateMs: msSince(active.updatedAt),
21304
+ sinceCreateMs: msSince(b.createdAt)
21364
21305
  };
21365
21306
  });
21366
21307
  const count2 = countWorkingSlots(entries, idleGraceMs);
@@ -21432,7 +21373,7 @@ async function uncachedBoxStateCount() {
21432
21373
  }
21433
21374
  function inspectDockerState(containerName) {
21434
21375
  return new Promise((resolveP) => {
21435
- const child = (0, import_node_child_process9.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
21376
+ const child = (0, import_node_child_process8.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
21436
21377
  stdio: ["ignore", "pipe", "pipe"]
21437
21378
  });
21438
21379
  let out = "";
@@ -21466,9 +21407,9 @@ async function defaultSpawnWorker(job) {
21466
21407
  `AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
21467
21408
  );
21468
21409
  }
21469
- await (0, import_promises17.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
21410
+ await (0, import_promises16.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
21470
21411
  const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
21471
- const child = (0, import_node_child_process9.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
21412
+ const child = (0, import_node_child_process8.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
21472
21413
  detached: true,
21473
21414
  stdio: ["ignore", fd, fd],
21474
21415
  env: process.env
@@ -21482,6 +21423,212 @@ async function defaultSpawnWorker(job) {
21482
21423
  }
21483
21424
  var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
21484
21425
 
21426
+ // src/autopause.ts
21427
+ function selectBoxesToPause(entries, cfg) {
21428
+ if (!cfg.enabled) return [];
21429
+ const runningCount = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
21430
+ const excess = runningCount - cfg.maxRunningBoxes;
21431
+ if (excess <= 0) return [];
21432
+ const idleThresholdMs = cfg.idleMinutes * 6e4;
21433
+ const candidates = entries.filter(
21434
+ (e) => e.running && e.claudeState === "idle" && e.idleMs != null && e.idleMs >= idleThresholdMs
21435
+ );
21436
+ candidates.sort(
21437
+ (a2, b) => b.idleMs - a2.idleMs || a2.createdAt - b.createdAt || (a2.boxId < b.boxId ? -1 : a2.boxId > b.boxId ? 1 : 0)
21438
+ );
21439
+ return candidates.slice(0, excess).map((e) => e.boxId);
21440
+ }
21441
+ async function loadAutopauseConfig() {
21442
+ const d = BUILT_IN_DEFAULTS.autopause;
21443
+ let global3 = {};
21444
+ try {
21445
+ global3 = parseUserConfig(await (0, import_promises18.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
21446
+ } catch {
21447
+ }
21448
+ const a2 = global3.autopause ?? {};
21449
+ return {
21450
+ enabled: a2.enabled ?? d.enabled,
21451
+ maxRunningBoxes: a2.maxRunningBoxes ?? d.maxRunningBoxes,
21452
+ idleMinutes: a2.idleMinutes ?? d.idleMinutes
21453
+ };
21454
+ }
21455
+ var DEFAULT_INTERVAL_MS2 = 6e4;
21456
+ function startAutopauseLoop(deps) {
21457
+ const loadConfig = deps.loadConfig ?? loadAutopauseConfig;
21458
+ const inspectStatus = deps.inspectStatus ?? inspectContainerState;
21459
+ const pause = deps.pause ?? pauseContainer;
21460
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
21461
+ const { registry, statusStore, events, log } = deps;
21462
+ let ticking = false;
21463
+ let stopped = false;
21464
+ let inFlight = Promise.resolve();
21465
+ async function tick() {
21466
+ if (ticking) return;
21467
+ ticking = true;
21468
+ try {
21469
+ const cfg = await loadConfig();
21470
+ if (!cfg.enabled) return;
21471
+ const entries = [];
21472
+ for (const reg of registry.list()) {
21473
+ if (!reg.containerName) continue;
21474
+ const state = await inspectStatus(reg.containerName);
21475
+ const active = readPauseState(statusStore.get(reg.boxId));
21476
+ const idleMs = active.state === "idle" && active.updatedAt ? msSince2(active.updatedAt) : null;
21477
+ entries.push({
21478
+ boxId: reg.boxId,
21479
+ containerName: reg.containerName,
21480
+ running: state === "running",
21481
+ claudeState: active.state,
21482
+ idleMs,
21483
+ createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0
21484
+ });
21485
+ }
21486
+ const toPause = selectBoxesToPause(entries, cfg);
21487
+ if (toPause.length === 0) return;
21488
+ const byId = new Map(entries.map((e) => [e.boxId, e]));
21489
+ const runningBefore = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
21490
+ for (const boxId of toPause) {
21491
+ const e = byId.get(boxId);
21492
+ if (!e) continue;
21493
+ try {
21494
+ await pause(e.containerName);
21495
+ const mins = e.idleMs != null ? Math.round(e.idleMs / 6e4) : null;
21496
+ events.append({
21497
+ boxId,
21498
+ type: "autopause",
21499
+ payload: {
21500
+ containerName: e.containerName,
21501
+ action: "paused",
21502
+ idleMs: e.idleMs,
21503
+ runningBefore,
21504
+ max: cfg.maxRunningBoxes
21505
+ }
21506
+ });
21507
+ log(
21508
+ `autopause: paused box ${boxId} (${e.containerName})` + (mins != null ? ` after ~${String(mins)}m idle` : "") + `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`
21509
+ );
21510
+ } catch (err) {
21511
+ const msg = err instanceof Error ? err.message : String(err);
21512
+ log(`autopause: docker pause ${e.containerName} failed: ${msg}`);
21513
+ events.append({
21514
+ boxId,
21515
+ type: "autopause",
21516
+ payload: { containerName: e.containerName, action: "pause-failed", error: msg }
21517
+ });
21518
+ }
21519
+ }
21520
+ } catch (err) {
21521
+ const msg = err instanceof Error ? err.message : String(err);
21522
+ log(`autopause: tick error: ${msg}`);
21523
+ } finally {
21524
+ ticking = false;
21525
+ }
21526
+ }
21527
+ const timer = setInterval(() => {
21528
+ if (stopped) return;
21529
+ inFlight = tick();
21530
+ }, intervalMs);
21531
+ timer.unref();
21532
+ return {
21533
+ stop: async () => {
21534
+ stopped = true;
21535
+ clearInterval(timer);
21536
+ await inFlight.catch(() => {
21537
+ });
21538
+ }
21539
+ };
21540
+ }
21541
+ function readPauseState(snap) {
21542
+ const active = readActiveAgent(snap);
21543
+ return { state: coarsePauseState(active.state), updatedAt: active.updatedAt };
21544
+ }
21545
+ function coarsePauseState(s) {
21546
+ switch (s) {
21547
+ case "idle":
21548
+ return "idle";
21549
+ case "waiting":
21550
+ return "waiting";
21551
+ case "working":
21552
+ case "compacting":
21553
+ return "working";
21554
+ case null:
21555
+ return null;
21556
+ // end-plan / question / error / unknown: a live session expecting attention
21557
+ // — never auto-pause it (maps to a non-idle, non-candidate state).
21558
+ default:
21559
+ return "unknown";
21560
+ }
21561
+ }
21562
+ function msSince2(iso) {
21563
+ const t = Date.parse(iso);
21564
+ return Number.isNaN(t) ? null : Date.now() - t;
21565
+ }
21566
+ function toEpoch(iso) {
21567
+ const t = Date.parse(iso);
21568
+ return Number.isNaN(t) ? 0 : t;
21569
+ }
21570
+ var INSPECT_TIMEOUT_MS = 15e3;
21571
+ var PAUSE_TIMEOUT_MS = 3e4;
21572
+ function runDocker(args, timeoutMs) {
21573
+ return new Promise((resolve2) => {
21574
+ const child = (0, import_node_child_process9.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
21575
+ let stdout = "";
21576
+ let stderr = "";
21577
+ let settled = false;
21578
+ const finish = (exitCode) => {
21579
+ if (settled) return;
21580
+ settled = true;
21581
+ resolve2({ exitCode, stdout, stderr });
21582
+ };
21583
+ const timer = setTimeout(() => {
21584
+ child.kill("SIGTERM");
21585
+ stderr += `
21586
+ relay: docker ${args.join(" ")} timed out after ${String(timeoutMs)}ms
21587
+ `;
21588
+ finish(124);
21589
+ }, timeoutMs);
21590
+ child.stdout?.on("data", (c3) => {
21591
+ stdout += c3.toString("utf8");
21592
+ });
21593
+ child.stderr?.on("data", (c3) => {
21594
+ stderr += c3.toString("utf8");
21595
+ });
21596
+ child.on("error", (err) => {
21597
+ clearTimeout(timer);
21598
+ stderr += String(err.message ?? err);
21599
+ finish(127);
21600
+ });
21601
+ child.on("close", (code) => {
21602
+ clearTimeout(timer);
21603
+ finish(code ?? -1);
21604
+ });
21605
+ });
21606
+ }
21607
+ async function inspectContainerState(name) {
21608
+ const r = await runDocker(["inspect", "--format", "{{.State.Status}}", name], INSPECT_TIMEOUT_MS);
21609
+ if (r.exitCode !== 0) return "missing";
21610
+ switch (r.stdout.trim()) {
21611
+ case "running":
21612
+ return "running";
21613
+ case "paused":
21614
+ return "paused";
21615
+ case "created":
21616
+ case "exited":
21617
+ case "dead":
21618
+ case "restarting":
21619
+ case "removing":
21620
+ return "stopped";
21621
+ default:
21622
+ return "missing";
21623
+ }
21624
+ }
21625
+ async function pauseContainer(name) {
21626
+ const r = await runDocker(["pause", name], PAUSE_TIMEOUT_MS);
21627
+ if (r.exitCode !== 0) {
21628
+ throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);
21629
+ }
21630
+ }
21631
+
21485
21632
  // src/bin.ts
21486
21633
  var program2 = new Command();
21487
21634
  program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");