@madarco/agentbox 0.12.0 → 0.14.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 +96 -0
  2. package/README.md +21 -7
  3. package/dist/{_cloud-attach-XKO4SHR3.js → _cloud-attach-GUBB5RH2.js} +4 -4
  4. package/dist/{chunk-R5XIDQFR.js → chunk-BKU34KYY.js} +170 -6
  5. package/dist/chunk-BKU34KYY.js.map +1 -0
  6. package/dist/{chunk-HFV6THYG.js → chunk-BYCLD6D6.js} +308 -36
  7. package/dist/chunk-BYCLD6D6.js.map +1 -0
  8. package/dist/chunk-LDMYHWUS.js +346 -0
  9. package/dist/chunk-LDMYHWUS.js.map +1 -0
  10. package/dist/{chunk-2LF5YILI.js → chunk-RSKG7AFU.js} +80 -6
  11. package/dist/chunk-RSKG7AFU.js.map +1 -0
  12. package/dist/{chunk-DHJ7OMIP.js → chunk-TBSIJVSN.js} +149 -47
  13. package/dist/chunk-TBSIJVSN.js.map +1 -0
  14. package/dist/{chunk-IZXPJPPV.js → chunk-TCS5HXJX.js} +389 -176
  15. package/dist/chunk-TCS5HXJX.js.map +1 -0
  16. package/dist/{chunk-ECLLV5JH.js → chunk-VATTS2MR.js} +156 -5
  17. package/dist/chunk-VATTS2MR.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-34RKQ74M.js +662 -0
  21. package/dist/dist-34RKQ74M.js.map +1 -0
  22. package/dist/{dist-47LVLYUV.js → dist-3IMQNTTV.js} +14 -69
  23. package/dist/dist-3IMQNTTV.js.map +1 -0
  24. package/dist/{dist-RZZSSUNB.js → dist-4DPOL5A7.js} +5 -3
  25. package/dist/{dist-24PY2ZMO.js → dist-57M6ZA7H.js} +25 -177
  26. package/dist/dist-57M6ZA7H.js.map +1 -0
  27. package/dist/{dist-SWUOU34W.js → dist-J2IHD5T7.js} +37 -226
  28. package/dist/dist-J2IHD5T7.js.map +1 -0
  29. package/dist/index.js +1524 -921
  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 +9 -7
  33. package/runtime/docker/Dockerfile.box +21 -26
  34. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +37 -1
  35. package/runtime/docker/packages/ctl/dist/bin.cjs +46 -17
  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 +233 -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 +23864 -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 +37 -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 +46 -17
  55. package/runtime/relay/bin.cjs +305 -230
  56. package/runtime/vercel/agentbox-setup-skill.md +37 -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 +46 -17
  60. package/share/agentbox-setup/SKILL.md +37 -1
  61. package/share/host-skills/agentbox-info/SKILL.md +26 -34
  62. package/dist/chunk-2LF5YILI.js.map +0 -1
  63. package/dist/chunk-DHJ7OMIP.js.map +0 -1
  64. package/dist/chunk-ECLLV5JH.js.map +0 -1
  65. package/dist/chunk-HFV6THYG.js.map +0 -1
  66. package/dist/chunk-IZXPJPPV.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-24PY2ZMO.js.map +0 -1
  70. package/dist/dist-47LVLYUV.js.map +0 -1
  71. package/dist/dist-SWUOU34W.js.map +0 -1
  72. /package/dist/{_cloud-attach-XKO4SHR3.js.map → _cloud-attach-GUBB5RH2.js.map} +0 -0
  73. /package/dist/{dist-RZZSSUNB.js.map → dist-4DPOL5A7.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 + "";
@@ -18492,6 +18492,10 @@ async function resolveCloudBackend(name) {
18492
18492
  const pkg = "@agentbox/sandbox-vercel";
18493
18493
  return loadCloudBackend(pkg, async () => (await import(pkg)).vercelBackend);
18494
18494
  }
18495
+ if (name === "e2b") {
18496
+ const pkg = "@agentbox/sandbox-e2b";
18497
+ return loadCloudBackend(pkg, async () => (await import(pkg)).e2bBackend);
18498
+ }
18495
18499
  throw new Error(`no host executor for cloud backend '${name}'`);
18496
18500
  }
18497
18501
  async function loadCloudBackend(pkg, load2) {
@@ -20306,8 +20310,8 @@ async function startRelayServer(opts) {
20306
20310
  }
20307
20311
 
20308
20312
  // src/autopause.ts
20309
- var import_node_child_process8 = require("child_process");
20310
- var import_promises16 = require("fs/promises");
20313
+ var import_node_child_process9 = require("child_process");
20314
+ var import_promises18 = require("fs/promises");
20311
20315
 
20312
20316
  // ../config/dist/index.js
20313
20317
  var import_yaml = __toESM(require_dist(), 1);
@@ -20325,11 +20329,13 @@ var BUILT_IN_DEFAULTS = {
20325
20329
  defaultCheckpointDaytona: "",
20326
20330
  defaultCheckpointHetzner: "",
20327
20331
  defaultCheckpointVercel: "",
20332
+ defaultCheckpointE2b: "",
20328
20333
  size: "",
20329
20334
  sizeDocker: "",
20330
20335
  sizeDaytona: "",
20331
20336
  sizeHetzner: "",
20332
20337
  sizeVercel: "",
20338
+ sizeE2b: "",
20333
20339
  withPlaywright: false,
20334
20340
  withEnv: false,
20335
20341
  resyncOnStart: true,
@@ -20342,6 +20348,7 @@ var BUILT_IN_DEFAULTS = {
20342
20348
  imageDaytona: "",
20343
20349
  imageHetzner: "",
20344
20350
  imageVercel: "",
20351
+ imageE2b: "",
20345
20352
  // Mirrors BOX_IMAGE_REGISTRY in @agentbox/sandbox-docker. Empty disables the
20346
20353
  // registry pull (always build the docker base image locally).
20347
20354
  imageRegistry: "ghcr.io/madarco/agentbox/box",
@@ -20370,7 +20377,8 @@ var BUILT_IN_DEFAULTS = {
20370
20377
  sessionName: "opencode"
20371
20378
  },
20372
20379
  attach: {
20373
- openIn: "split"
20380
+ openIn: "split",
20381
+ cmuxStatus: true
20374
20382
  },
20375
20383
  code: {
20376
20384
  ide: "auto",
@@ -20422,8 +20430,8 @@ var KEY_REGISTRY = [
20422
20430
  {
20423
20431
  key: "box.provider",
20424
20432
  type: "enum",
20425
- enumValues: ["docker", "daytona", "hetzner", "vercel"],
20426
- description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
20433
+ enumValues: ["docker", "daytona", "hetzner", "vercel", "e2b"],
20434
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, Vercel Sandboxes, or E2B microVMs."
20427
20435
  },
20428
20436
  {
20429
20437
  key: "box.hostSnapshot",
@@ -20459,6 +20467,12 @@ var KEY_REGISTRY = [
20459
20467
  description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
20460
20468
  advanced: true
20461
20469
  },
20470
+ {
20471
+ key: "box.defaultCheckpointE2b",
20472
+ type: "string",
20473
+ description: "Per-provider override of `box.defaultCheckpoint` for e2b. Wins over the global when set; set via `agentbox checkpoint set-default --provider e2b`.",
20474
+ advanced: true
20475
+ },
20462
20476
  {
20463
20477
  key: "box.size",
20464
20478
  type: "string",
@@ -20488,6 +20502,12 @@ var KEY_REGISTRY = [
20488
20502
  description: "Per-provider override of `box.size` for vercel. Reserved \u2014 vercel sizing is controlled via `box.vercelVcpus`.",
20489
20503
  advanced: true
20490
20504
  },
20505
+ {
20506
+ key: "box.sizeE2b",
20507
+ type: "string",
20508
+ 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).",
20509
+ advanced: true
20510
+ },
20491
20511
  {
20492
20512
  key: "checkpoint.maxLayers",
20493
20513
  type: "int",
@@ -20559,6 +20579,12 @@ var KEY_REGISTRY = [
20559
20579
  description: "Per-provider override of `box.image` for vercel (snapshot id, e.g. `snap_\u2026`). Written by `agentbox prepare --provider vercel`.",
20560
20580
  advanced: true
20561
20581
  },
20582
+ {
20583
+ key: "box.imageE2b",
20584
+ type: "string",
20585
+ 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`.",
20586
+ advanced: true
20587
+ },
20562
20588
  {
20563
20589
  key: "box.imageRegistry",
20564
20590
  type: "string",
@@ -20640,7 +20666,12 @@ var KEY_REGISTRY = [
20640
20666
  key: "attach.openIn",
20641
20667
  type: "enum",
20642
20668
  enumValues: ["split", "window", "tab", "same"],
20643
- description: "Where `agentbox claude|codex|opencode` opens the attached session when run from tmux or iTerm2: `split` (tmux split-window / iTerm2 vertical split, default), `window` (tmux new-window / new iTerm2 window), `tab` (tmux new-window / new iTerm2 tab), or `same` (attach inline in the current terminal). Outside tmux/iTerm2 every value behaves like `same`."
20669
+ description: "Where `agentbox claude|codex|opencode` opens the attached session when run from tmux, cmux, or iTerm2: `split` (tmux split-window / cmux new-split / iTerm2 vertical split, default \u2014 same workspace), `window` (tmux new-window / cmux new-workspace / new iTerm2 window), `tab` (tmux new-window / cmux new-surface tab in the current pane, same workspace / new iTerm2 tab), or `same` (attach inline in the current terminal). Outside tmux/cmux/iTerm2 every value behaves like `same`."
20670
+ },
20671
+ {
20672
+ key: "attach.cmuxStatus",
20673
+ type: "bool",
20674
+ description: "When attached inside cmux, reflect the box agent's live activity on its cmux workspace (colour + description: blue=working, amber=needs input, idle clears; restored on detach) and, when the agent needs input, flag the box's own tab via a cmux notification (tab badge + reorder + desktop notification) so it stands out among sibling tabs. cmux only; no-op in other terminals."
20644
20675
  },
20645
20676
  {
20646
20677
  key: "code.ide",
@@ -20894,210 +20925,18 @@ var GLOBAL_CONFIG_FILE = (0, import_path2.join)(STATE_DIR2, "config.yaml");
20894
20925
  var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
20895
20926
  var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
20896
20927
 
20897
- // src/autopause.ts
20898
- function selectBoxesToPause(entries, cfg) {
20899
- if (!cfg.enabled) return [];
20900
- const runningCount = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
20901
- const excess = runningCount - cfg.maxRunningBoxes;
20902
- if (excess <= 0) return [];
20903
- const idleThresholdMs = cfg.idleMinutes * 6e4;
20904
- const candidates = entries.filter(
20905
- (e) => e.running && e.claudeState === "idle" && e.idleMs != null && e.idleMs >= idleThresholdMs
20906
- );
20907
- candidates.sort(
20908
- (a2, b) => b.idleMs - a2.idleMs || a2.createdAt - b.createdAt || (a2.boxId < b.boxId ? -1 : a2.boxId > b.boxId ? 1 : 0)
20909
- );
20910
- return candidates.slice(0, excess).map((e) => e.boxId);
20911
- }
20912
- async function loadAutopauseConfig() {
20913
- const d = BUILT_IN_DEFAULTS.autopause;
20914
- let global3 = {};
20915
- try {
20916
- global3 = parseUserConfig(await (0, import_promises16.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
20917
- } catch {
20918
- }
20919
- const a2 = global3.autopause ?? {};
20920
- return {
20921
- enabled: a2.enabled ?? d.enabled,
20922
- maxRunningBoxes: a2.maxRunningBoxes ?? d.maxRunningBoxes,
20923
- idleMinutes: a2.idleMinutes ?? d.idleMinutes
20924
- };
20925
- }
20926
- var DEFAULT_INTERVAL_MS = 6e4;
20927
- function startAutopauseLoop(deps) {
20928
- const loadConfig = deps.loadConfig ?? loadAutopauseConfig;
20929
- const inspectStatus = deps.inspectStatus ?? inspectContainerState;
20930
- const pause = deps.pause ?? pauseContainer;
20931
- const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
20932
- const { registry, statusStore, events, log } = deps;
20933
- let ticking = false;
20934
- let stopped = false;
20935
- let inFlight = Promise.resolve();
20936
- async function tick() {
20937
- if (ticking) return;
20938
- ticking = true;
20939
- try {
20940
- const cfg = await loadConfig();
20941
- if (!cfg.enabled) return;
20942
- const entries = [];
20943
- for (const reg of registry.list()) {
20944
- if (!reg.containerName) continue;
20945
- const state = await inspectStatus(reg.containerName);
20946
- const claude = readClaude(statusStore.get(reg.boxId));
20947
- const idleMs = claude.state === "idle" && claude.updatedAt ? msSince(claude.updatedAt) : null;
20948
- entries.push({
20949
- boxId: reg.boxId,
20950
- containerName: reg.containerName,
20951
- running: state === "running",
20952
- claudeState: claude.state,
20953
- idleMs,
20954
- createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0
20955
- });
20956
- }
20957
- const toPause = selectBoxesToPause(entries, cfg);
20958
- if (toPause.length === 0) return;
20959
- const byId = new Map(entries.map((e) => [e.boxId, e]));
20960
- const runningBefore = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
20961
- for (const boxId of toPause) {
20962
- const e = byId.get(boxId);
20963
- if (!e) continue;
20964
- try {
20965
- await pause(e.containerName);
20966
- const mins = e.idleMs != null ? Math.round(e.idleMs / 6e4) : null;
20967
- events.append({
20968
- boxId,
20969
- type: "autopause",
20970
- payload: {
20971
- containerName: e.containerName,
20972
- action: "paused",
20973
- idleMs: e.idleMs,
20974
- runningBefore,
20975
- max: cfg.maxRunningBoxes
20976
- }
20977
- });
20978
- log(
20979
- `autopause: paused box ${boxId} (${e.containerName})` + (mins != null ? ` after ~${String(mins)}m idle` : "") + `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`
20980
- );
20981
- } catch (err) {
20982
- const msg = err instanceof Error ? err.message : String(err);
20983
- log(`autopause: docker pause ${e.containerName} failed: ${msg}`);
20984
- events.append({
20985
- boxId,
20986
- type: "autopause",
20987
- payload: { containerName: e.containerName, action: "pause-failed", error: msg }
20988
- });
20989
- }
20990
- }
20991
- } catch (err) {
20992
- const msg = err instanceof Error ? err.message : String(err);
20993
- log(`autopause: tick error: ${msg}`);
20994
- } finally {
20995
- ticking = false;
20996
- }
20997
- }
20998
- const timer = setInterval(() => {
20999
- if (stopped) return;
21000
- inFlight = tick();
21001
- }, intervalMs);
21002
- timer.unref();
21003
- return {
21004
- stop: async () => {
21005
- stopped = true;
21006
- clearInterval(timer);
21007
- await inFlight.catch(() => {
21008
- });
21009
- }
21010
- };
21011
- }
21012
- function readClaude(snap) {
21013
- const c3 = snap && typeof snap === "object" ? snap.claude : void 0;
21014
- if (!c3 || typeof c3 !== "object") return { state: null, updatedAt: null };
21015
- const o2 = c3;
21016
- const state = o2.state === "working" || o2.state === "idle" || o2.state === "waiting" || o2.state === "unknown" ? o2.state : null;
21017
- return { state, updatedAt: typeof o2.updatedAt === "string" ? o2.updatedAt : null };
21018
- }
21019
- function msSince(iso) {
21020
- const t = Date.parse(iso);
21021
- return Number.isNaN(t) ? null : Date.now() - t;
21022
- }
21023
- function toEpoch(iso) {
21024
- const t = Date.parse(iso);
21025
- return Number.isNaN(t) ? 0 : t;
21026
- }
21027
- var INSPECT_TIMEOUT_MS = 15e3;
21028
- var PAUSE_TIMEOUT_MS = 3e4;
21029
- function runDocker(args, timeoutMs) {
21030
- return new Promise((resolve2) => {
21031
- const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
21032
- let stdout = "";
21033
- let stderr = "";
21034
- let settled = false;
21035
- const finish = (exitCode) => {
21036
- if (settled) return;
21037
- settled = true;
21038
- resolve2({ exitCode, stdout, stderr });
21039
- };
21040
- const timer = setTimeout(() => {
21041
- child.kill("SIGTERM");
21042
- stderr += `
21043
- relay: docker ${args.join(" ")} timed out after ${String(timeoutMs)}ms
21044
- `;
21045
- finish(124);
21046
- }, timeoutMs);
21047
- child.stdout?.on("data", (c3) => {
21048
- stdout += c3.toString("utf8");
21049
- });
21050
- child.stderr?.on("data", (c3) => {
21051
- stderr += c3.toString("utf8");
21052
- });
21053
- child.on("error", (err) => {
21054
- clearTimeout(timer);
21055
- stderr += String(err.message ?? err);
21056
- finish(127);
21057
- });
21058
- child.on("close", (code) => {
21059
- clearTimeout(timer);
21060
- finish(code ?? -1);
21061
- });
21062
- });
21063
- }
21064
- async function inspectContainerState(name) {
21065
- const r = await runDocker(["inspect", "--format", "{{.State.Status}}", name], INSPECT_TIMEOUT_MS);
21066
- if (r.exitCode !== 0) return "missing";
21067
- switch (r.stdout.trim()) {
21068
- case "running":
21069
- return "running";
21070
- case "paused":
21071
- return "paused";
21072
- case "created":
21073
- case "exited":
21074
- case "dead":
21075
- case "restarting":
21076
- case "removing":
21077
- return "stopped";
21078
- default:
21079
- return "missing";
21080
- }
21081
- }
21082
- async function pauseContainer(name) {
21083
- const r = await runDocker(["pause", name], PAUSE_TIMEOUT_MS);
21084
- if (r.exitCode !== 0) {
21085
- throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);
21086
- }
21087
- }
21088
-
21089
20928
  // src/queue.ts
21090
- var import_node_child_process9 = require("child_process");
21091
- var import_promises17 = require("fs/promises");
20929
+ var import_node_child_process8 = require("child_process");
20930
+ var import_promises16 = require("fs/promises");
21092
20931
  var import_node_fs6 = require("fs");
21093
20932
  var import_node_path8 = require("path");
21094
- var import_promises18 = require("timers/promises");
20933
+ var import_promises17 = require("timers/promises");
21095
20934
  var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
21096
20935
  async function loadQueueConfig() {
21097
20936
  const d = BUILT_IN_DEFAULTS.queue;
21098
20937
  let global3 = {};
21099
20938
  try {
21100
- global3 = parseUserConfig(await (0, import_promises17.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
20939
+ global3 = parseUserConfig(await (0, import_promises16.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
21101
20940
  } catch {
21102
20941
  }
21103
20942
  const q = global3.queue ?? {};
@@ -21109,25 +20948,32 @@ async function loadQueueConfig() {
21109
20948
  };
21110
20949
  }
21111
20950
  async function writeJob(job) {
21112
- await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
20951
+ await (0, import_promises16.mkdir)(QUEUE_DIR, { recursive: true });
21113
20952
  const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
21114
20953
  const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
21115
- await (0, import_promises17.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
21116
- await (0, import_promises17.rename)(tmp, final);
20954
+ await (0, import_promises16.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
20955
+ await (0, import_promises16.rename)(tmp, final);
21117
20956
  }
21118
20957
  async function readJob(id) {
21119
20958
  try {
21120
- const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
20959
+ const raw = await (0, import_promises16.readFile)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`), "utf8");
21121
20960
  return JSON.parse(raw);
21122
20961
  } catch (err) {
21123
20962
  if (err.code === "ENOENT") return null;
21124
20963
  throw err;
21125
20964
  }
21126
20965
  }
20966
+ async function deleteJob(id) {
20967
+ try {
20968
+ await (0, import_promises16.unlink)((0, import_node_path8.join)(QUEUE_DIR, `${id}.json`));
20969
+ } catch (err) {
20970
+ if (err.code !== "ENOENT") throw err;
20971
+ }
20972
+ }
21127
20973
  async function loadQueue() {
21128
20974
  let entries;
21129
20975
  try {
21130
- entries = await (0, import_promises17.readdir)(QUEUE_DIR);
20976
+ entries = await (0, import_promises16.readdir)(QUEUE_DIR);
21131
20977
  } catch (err) {
21132
20978
  if (err.code === "ENOENT") return [];
21133
20979
  throw err;
@@ -21136,7 +20982,7 @@ async function loadQueue() {
21136
20982
  for (const name of entries) {
21137
20983
  if (!name.endsWith(".json")) continue;
21138
20984
  try {
21139
- const raw = await (0, import_promises17.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
20985
+ const raw = await (0, import_promises16.readFile)((0, import_node_path8.join)(QUEUE_DIR, name), "utf8");
21140
20986
  out.push(JSON.parse(raw));
21141
20987
  } catch {
21142
20988
  }
@@ -21210,22 +21056,23 @@ function parseTime(iso) {
21210
21056
  const t = Date.parse(iso);
21211
21057
  return Number.isNaN(t) ? 0 : t;
21212
21058
  }
21213
- function msSince2(iso) {
21059
+ function msSince(iso) {
21214
21060
  if (!iso) return null;
21215
21061
  const t = Date.parse(iso);
21216
21062
  return Number.isNaN(t) ? null : Date.now() - t;
21217
21063
  }
21218
- var DEFAULT_INTERVAL_MS2 = 2e3;
21064
+ var DEFAULT_INTERVAL_MS = 2e3;
21219
21065
  function startQueueLoop(deps) {
21220
21066
  const loadConfig = deps.loadConfig ?? loadQueueConfig;
21221
21067
  const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
21222
21068
  const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
21223
- const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
21069
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
21224
21070
  const { log, onStatusChange } = deps;
21225
21071
  const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
21226
21072
  let ticking = false;
21227
21073
  let stopped = false;
21228
21074
  let warnedNoWorkingDeps = false;
21075
+ let lastSweepAt = 0;
21229
21076
  let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
21230
21077
  log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
21231
21078
  });
@@ -21235,6 +21082,13 @@ function startQueueLoop(deps) {
21235
21082
  try {
21236
21083
  const cfg = await loadConfig();
21237
21084
  if (!cfg.enabled) return;
21085
+ const now = Date.now();
21086
+ if (now - lastSweepAt >= SWEEP_INTERVAL_MS) {
21087
+ lastSweepAt = now;
21088
+ await sweepTerminalJobs(log, now).catch((err) => {
21089
+ log(`queue: sweep failed: ${err instanceof Error ? err.message : String(err)}`);
21090
+ });
21091
+ }
21238
21092
  const jobs = await loadQueue();
21239
21093
  const hasQueued = jobs.some((j) => j.status === "queued");
21240
21094
  if (!hasQueued) return;
@@ -21337,6 +21191,21 @@ async function recoverOrphanedWorkers(log, onChange) {
21337
21191
  log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
21338
21192
  }
21339
21193
  }
21194
+ var TERMINAL_RETENTION_MS = 60 * 60 * 1e3;
21195
+ var SWEEP_INTERVAL_MS = 60 * 1e3;
21196
+ var TERMINAL_STATUSES = ["done", "failed", "cancelled"];
21197
+ async function sweepTerminalJobs(log, now) {
21198
+ const jobs = await loadQueue();
21199
+ let swept = 0;
21200
+ for (const j of jobs) {
21201
+ if (!TERMINAL_STATUSES.includes(j.status)) continue;
21202
+ const since = Date.parse(j.finishedAt ?? j.createdAt);
21203
+ if (Number.isNaN(since) || now - since < TERMINAL_RETENTION_MS) continue;
21204
+ await deleteJob(j.id);
21205
+ swept += 1;
21206
+ }
21207
+ if (swept > 0) log(`queue: swept ${String(swept)} stale terminal manifest(s)`);
21208
+ }
21340
21209
  function processAlive(pid) {
21341
21210
  try {
21342
21211
  process.kill(pid, 0);
@@ -21353,8 +21222,8 @@ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
21353
21222
  return {
21354
21223
  key: b.boxId,
21355
21224
  agentState: active.state,
21356
- sinceUpdateMs: msSince2(active.updatedAt),
21357
- sinceCreateMs: msSince2(b.createdAt)
21225
+ sinceUpdateMs: msSince(active.updatedAt),
21226
+ sinceCreateMs: msSince(b.createdAt)
21358
21227
  };
21359
21228
  });
21360
21229
  const count2 = countWorkingSlots(entries, idleGraceMs);
@@ -21426,7 +21295,7 @@ async function uncachedBoxStateCount() {
21426
21295
  }
21427
21296
  function inspectDockerState(containerName) {
21428
21297
  return new Promise((resolveP) => {
21429
- const child = (0, import_node_child_process9.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
21298
+ const child = (0, import_node_child_process8.spawn)("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
21430
21299
  stdio: ["ignore", "pipe", "pipe"]
21431
21300
  });
21432
21301
  let out = "";
@@ -21460,9 +21329,9 @@ async function defaultSpawnWorker(job) {
21460
21329
  `AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
21461
21330
  );
21462
21331
  }
21463
- await (0, import_promises17.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
21332
+ await (0, import_promises16.mkdir)((0, import_node_path8.join)(STATE_DIR, "logs"), { recursive: true });
21464
21333
  const fd = (0, import_node_fs6.openSync)(job.logPath, "a");
21465
- const child = (0, import_node_child_process9.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
21334
+ const child = (0, import_node_child_process8.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
21466
21335
  detached: true,
21467
21336
  stdio: ["ignore", fd, fd],
21468
21337
  env: process.env
@@ -21476,6 +21345,212 @@ async function defaultSpawnWorker(job) {
21476
21345
  }
21477
21346
  var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
21478
21347
 
21348
+ // src/autopause.ts
21349
+ function selectBoxesToPause(entries, cfg) {
21350
+ if (!cfg.enabled) return [];
21351
+ const runningCount = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
21352
+ const excess = runningCount - cfg.maxRunningBoxes;
21353
+ if (excess <= 0) return [];
21354
+ const idleThresholdMs = cfg.idleMinutes * 6e4;
21355
+ const candidates = entries.filter(
21356
+ (e) => e.running && e.claudeState === "idle" && e.idleMs != null && e.idleMs >= idleThresholdMs
21357
+ );
21358
+ candidates.sort(
21359
+ (a2, b) => b.idleMs - a2.idleMs || a2.createdAt - b.createdAt || (a2.boxId < b.boxId ? -1 : a2.boxId > b.boxId ? 1 : 0)
21360
+ );
21361
+ return candidates.slice(0, excess).map((e) => e.boxId);
21362
+ }
21363
+ async function loadAutopauseConfig() {
21364
+ const d = BUILT_IN_DEFAULTS.autopause;
21365
+ let global3 = {};
21366
+ try {
21367
+ global3 = parseUserConfig(await (0, import_promises18.readFile)(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
21368
+ } catch {
21369
+ }
21370
+ const a2 = global3.autopause ?? {};
21371
+ return {
21372
+ enabled: a2.enabled ?? d.enabled,
21373
+ maxRunningBoxes: a2.maxRunningBoxes ?? d.maxRunningBoxes,
21374
+ idleMinutes: a2.idleMinutes ?? d.idleMinutes
21375
+ };
21376
+ }
21377
+ var DEFAULT_INTERVAL_MS2 = 6e4;
21378
+ function startAutopauseLoop(deps) {
21379
+ const loadConfig = deps.loadConfig ?? loadAutopauseConfig;
21380
+ const inspectStatus = deps.inspectStatus ?? inspectContainerState;
21381
+ const pause = deps.pause ?? pauseContainer;
21382
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
21383
+ const { registry, statusStore, events, log } = deps;
21384
+ let ticking = false;
21385
+ let stopped = false;
21386
+ let inFlight = Promise.resolve();
21387
+ async function tick() {
21388
+ if (ticking) return;
21389
+ ticking = true;
21390
+ try {
21391
+ const cfg = await loadConfig();
21392
+ if (!cfg.enabled) return;
21393
+ const entries = [];
21394
+ for (const reg of registry.list()) {
21395
+ if (!reg.containerName) continue;
21396
+ const state = await inspectStatus(reg.containerName);
21397
+ const active = readPauseState(statusStore.get(reg.boxId));
21398
+ const idleMs = active.state === "idle" && active.updatedAt ? msSince2(active.updatedAt) : null;
21399
+ entries.push({
21400
+ boxId: reg.boxId,
21401
+ containerName: reg.containerName,
21402
+ running: state === "running",
21403
+ claudeState: active.state,
21404
+ idleMs,
21405
+ createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0
21406
+ });
21407
+ }
21408
+ const toPause = selectBoxesToPause(entries, cfg);
21409
+ if (toPause.length === 0) return;
21410
+ const byId = new Map(entries.map((e) => [e.boxId, e]));
21411
+ const runningBefore = entries.reduce((n2, e) => e.running ? n2 + 1 : n2, 0);
21412
+ for (const boxId of toPause) {
21413
+ const e = byId.get(boxId);
21414
+ if (!e) continue;
21415
+ try {
21416
+ await pause(e.containerName);
21417
+ const mins = e.idleMs != null ? Math.round(e.idleMs / 6e4) : null;
21418
+ events.append({
21419
+ boxId,
21420
+ type: "autopause",
21421
+ payload: {
21422
+ containerName: e.containerName,
21423
+ action: "paused",
21424
+ idleMs: e.idleMs,
21425
+ runningBefore,
21426
+ max: cfg.maxRunningBoxes
21427
+ }
21428
+ });
21429
+ log(
21430
+ `autopause: paused box ${boxId} (${e.containerName})` + (mins != null ? ` after ~${String(mins)}m idle` : "") + `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`
21431
+ );
21432
+ } catch (err) {
21433
+ const msg = err instanceof Error ? err.message : String(err);
21434
+ log(`autopause: docker pause ${e.containerName} failed: ${msg}`);
21435
+ events.append({
21436
+ boxId,
21437
+ type: "autopause",
21438
+ payload: { containerName: e.containerName, action: "pause-failed", error: msg }
21439
+ });
21440
+ }
21441
+ }
21442
+ } catch (err) {
21443
+ const msg = err instanceof Error ? err.message : String(err);
21444
+ log(`autopause: tick error: ${msg}`);
21445
+ } finally {
21446
+ ticking = false;
21447
+ }
21448
+ }
21449
+ const timer = setInterval(() => {
21450
+ if (stopped) return;
21451
+ inFlight = tick();
21452
+ }, intervalMs);
21453
+ timer.unref();
21454
+ return {
21455
+ stop: async () => {
21456
+ stopped = true;
21457
+ clearInterval(timer);
21458
+ await inFlight.catch(() => {
21459
+ });
21460
+ }
21461
+ };
21462
+ }
21463
+ function readPauseState(snap) {
21464
+ const active = readActiveAgent(snap);
21465
+ return { state: coarsePauseState(active.state), updatedAt: active.updatedAt };
21466
+ }
21467
+ function coarsePauseState(s) {
21468
+ switch (s) {
21469
+ case "idle":
21470
+ return "idle";
21471
+ case "waiting":
21472
+ return "waiting";
21473
+ case "working":
21474
+ case "compacting":
21475
+ return "working";
21476
+ case null:
21477
+ return null;
21478
+ // end-plan / question / error / unknown: a live session expecting attention
21479
+ // — never auto-pause it (maps to a non-idle, non-candidate state).
21480
+ default:
21481
+ return "unknown";
21482
+ }
21483
+ }
21484
+ function msSince2(iso) {
21485
+ const t = Date.parse(iso);
21486
+ return Number.isNaN(t) ? null : Date.now() - t;
21487
+ }
21488
+ function toEpoch(iso) {
21489
+ const t = Date.parse(iso);
21490
+ return Number.isNaN(t) ? 0 : t;
21491
+ }
21492
+ var INSPECT_TIMEOUT_MS = 15e3;
21493
+ var PAUSE_TIMEOUT_MS = 3e4;
21494
+ function runDocker(args, timeoutMs) {
21495
+ return new Promise((resolve2) => {
21496
+ const child = (0, import_node_child_process9.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
21497
+ let stdout = "";
21498
+ let stderr = "";
21499
+ let settled = false;
21500
+ const finish = (exitCode) => {
21501
+ if (settled) return;
21502
+ settled = true;
21503
+ resolve2({ exitCode, stdout, stderr });
21504
+ };
21505
+ const timer = setTimeout(() => {
21506
+ child.kill("SIGTERM");
21507
+ stderr += `
21508
+ relay: docker ${args.join(" ")} timed out after ${String(timeoutMs)}ms
21509
+ `;
21510
+ finish(124);
21511
+ }, timeoutMs);
21512
+ child.stdout?.on("data", (c3) => {
21513
+ stdout += c3.toString("utf8");
21514
+ });
21515
+ child.stderr?.on("data", (c3) => {
21516
+ stderr += c3.toString("utf8");
21517
+ });
21518
+ child.on("error", (err) => {
21519
+ clearTimeout(timer);
21520
+ stderr += String(err.message ?? err);
21521
+ finish(127);
21522
+ });
21523
+ child.on("close", (code) => {
21524
+ clearTimeout(timer);
21525
+ finish(code ?? -1);
21526
+ });
21527
+ });
21528
+ }
21529
+ async function inspectContainerState(name) {
21530
+ const r = await runDocker(["inspect", "--format", "{{.State.Status}}", name], INSPECT_TIMEOUT_MS);
21531
+ if (r.exitCode !== 0) return "missing";
21532
+ switch (r.stdout.trim()) {
21533
+ case "running":
21534
+ return "running";
21535
+ case "paused":
21536
+ return "paused";
21537
+ case "created":
21538
+ case "exited":
21539
+ case "dead":
21540
+ case "restarting":
21541
+ case "removing":
21542
+ return "stopped";
21543
+ default:
21544
+ return "missing";
21545
+ }
21546
+ }
21547
+ async function pauseContainer(name) {
21548
+ const r = await runDocker(["pause", name], PAUSE_TIMEOUT_MS);
21549
+ if (r.exitCode !== 0) {
21550
+ throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);
21551
+ }
21552
+ }
21553
+
21479
21554
  // src/bin.ts
21480
21555
  var program2 = new Command();
21481
21556
  program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");