@madarco/agentbox 0.13.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 +65 -0
  2. package/README.md +11 -8
  3. package/dist/{_cloud-attach-HJC672UR.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-QYRK5H6Q.js → chunk-BYCLD6D6.js} +17 -9
  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-4NQXNQ53.js → chunk-TBSIJVSN.js} +149 -47
  13. package/dist/chunk-TBSIJVSN.js.map +1 -0
  14. package/dist/{chunk-B4QG2MCW.js → chunk-TCS5HXJX.js} +381 -174
  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-OPIBZ7XM.js → dist-3IMQNTTV.js} +14 -69
  23. package/dist/dist-3IMQNTTV.js.map +1 -0
  24. package/dist/{dist-OG6NW6SM.js → dist-4DPOL5A7.js} +5 -3
  25. package/dist/{dist-JAN5VABY.js → dist-57M6ZA7H.js} +25 -177
  26. package/dist/dist-57M6ZA7H.js.map +1 -0
  27. package/dist/{dist-7KVUIKJX.js → dist-J2IHD5T7.js} +37 -226
  28. package/dist/dist-J2IHD5T7.js.map +1 -0
  29. package/dist/index.js +1376 -1029
  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 +8 -6
  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 +40 -16
  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 +40 -16
  55. package/runtime/relay/bin.cjs +297 -228
  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 +40 -16
  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-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-GUBB5RH2.js.map} +0 -0
  73. /package/dist/{dist-OG6NW6SM.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",
@@ -20423,8 +20430,8 @@ var KEY_REGISTRY = [
20423
20430
  {
20424
20431
  key: "box.provider",
20425
20432
  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."
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."
20428
20435
  },
20429
20436
  {
20430
20437
  key: "box.hostSnapshot",
@@ -20460,6 +20467,12 @@ var KEY_REGISTRY = [
20460
20467
  description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
20461
20468
  advanced: true
20462
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
+ },
20463
20476
  {
20464
20477
  key: "box.size",
20465
20478
  type: "string",
@@ -20489,6 +20502,12 @@ var KEY_REGISTRY = [
20489
20502
  description: "Per-provider override of `box.size` for vercel. Reserved \u2014 vercel sizing is controlled via `box.vercelVcpus`.",
20490
20503
  advanced: true
20491
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
+ },
20492
20511
  {
20493
20512
  key: "checkpoint.maxLayers",
20494
20513
  type: "int",
@@ -20560,6 +20579,12 @@ var KEY_REGISTRY = [
20560
20579
  description: "Per-provider override of `box.image` for vercel (snapshot id, e.g. `snap_\u2026`). Written by `agentbox prepare --provider vercel`.",
20561
20580
  advanced: true
20562
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
+ },
20563
20588
  {
20564
20589
  key: "box.imageRegistry",
20565
20590
  type: "string",
@@ -20900,210 +20925,18 @@ var GLOBAL_CONFIG_FILE = (0, import_path2.join)(STATE_DIR2, "config.yaml");
20900
20925
  var PROJECTS_DIR = (0, import_path2.join)(STATE_DIR2, "projects");
20901
20926
  var PROJECT_GC_COUNTER_FILE = (0, import_path3.join)(PROJECTS_DIR, ".gc.json");
20902
20927
 
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
20928
  // src/queue.ts
21096
- var import_node_child_process9 = require("child_process");
21097
- var import_promises17 = require("fs/promises");
20929
+ var import_node_child_process8 = require("child_process");
20930
+ var import_promises16 = require("fs/promises");
21098
20931
  var import_node_fs6 = require("fs");
21099
20932
  var import_node_path8 = require("path");
21100
- var import_promises18 = require("timers/promises");
20933
+ var import_promises17 = require("timers/promises");
21101
20934
  var QUEUE_DIR = (0, import_node_path8.join)(STATE_DIR, "queue");
21102
20935
  async function loadQueueConfig() {
21103
20936
  const d = BUILT_IN_DEFAULTS.queue;
21104
20937
  let global3 = {};
21105
20938
  try {
21106
- 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);
21107
20940
  } catch {
21108
20941
  }
21109
20942
  const q = global3.queue ?? {};
@@ -21115,25 +20948,32 @@ async function loadQueueConfig() {
21115
20948
  };
21116
20949
  }
21117
20950
  async function writeJob(job) {
21118
- await (0, import_promises17.mkdir)(QUEUE_DIR, { recursive: true });
20951
+ await (0, import_promises16.mkdir)(QUEUE_DIR, { recursive: true });
21119
20952
  const final = (0, import_node_path8.join)(QUEUE_DIR, `${job.id}.json`);
21120
20953
  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);
20954
+ await (0, import_promises16.writeFile)(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
20955
+ await (0, import_promises16.rename)(tmp, final);
21123
20956
  }
21124
20957
  async function readJob(id) {
21125
20958
  try {
21126
- 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");
21127
20960
  return JSON.parse(raw);
21128
20961
  } catch (err) {
21129
20962
  if (err.code === "ENOENT") return null;
21130
20963
  throw err;
21131
20964
  }
21132
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
+ }
21133
20973
  async function loadQueue() {
21134
20974
  let entries;
21135
20975
  try {
21136
- entries = await (0, import_promises17.readdir)(QUEUE_DIR);
20976
+ entries = await (0, import_promises16.readdir)(QUEUE_DIR);
21137
20977
  } catch (err) {
21138
20978
  if (err.code === "ENOENT") return [];
21139
20979
  throw err;
@@ -21142,7 +20982,7 @@ async function loadQueue() {
21142
20982
  for (const name of entries) {
21143
20983
  if (!name.endsWith(".json")) continue;
21144
20984
  try {
21145
- 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");
21146
20986
  out.push(JSON.parse(raw));
21147
20987
  } catch {
21148
20988
  }
@@ -21216,22 +21056,23 @@ function parseTime(iso) {
21216
21056
  const t = Date.parse(iso);
21217
21057
  return Number.isNaN(t) ? 0 : t;
21218
21058
  }
21219
- function msSince2(iso) {
21059
+ function msSince(iso) {
21220
21060
  if (!iso) return null;
21221
21061
  const t = Date.parse(iso);
21222
21062
  return Number.isNaN(t) ? null : Date.now() - t;
21223
21063
  }
21224
- var DEFAULT_INTERVAL_MS2 = 2e3;
21064
+ var DEFAULT_INTERVAL_MS = 2e3;
21225
21065
  function startQueueLoop(deps) {
21226
21066
  const loadConfig = deps.loadConfig ?? loadQueueConfig;
21227
21067
  const countRunning = deps.countRunning ?? defaultCountRunningBoxes;
21228
21068
  const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
21229
- const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
21069
+ const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;
21230
21070
  const { log, onStatusChange } = deps;
21231
21071
  const countWorking = deps.countWorking ?? (deps.registry && deps.statusStore ? (idleGraceMs) => defaultCountWorkingBoxes(deps.registry, deps.statusStore, idleGraceMs) : null);
21232
21072
  let ticking = false;
21233
21073
  let stopped = false;
21234
21074
  let warnedNoWorkingDeps = false;
21075
+ let lastSweepAt = 0;
21235
21076
  let inFlight = recoverOrphanedWorkers(log, onStatusChange).catch((err) => {
21236
21077
  log(`queue: orphan recovery failed: ${err instanceof Error ? err.message : String(err)}`);
21237
21078
  });
@@ -21241,6 +21082,13 @@ function startQueueLoop(deps) {
21241
21082
  try {
21242
21083
  const cfg = await loadConfig();
21243
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
+ }
21244
21092
  const jobs = await loadQueue();
21245
21093
  const hasQueued = jobs.some((j) => j.status === "queued");
21246
21094
  if (!hasQueued) return;
@@ -21343,6 +21191,21 @@ async function recoverOrphanedWorkers(log, onChange) {
21343
21191
  log(`queue: recovered orphan job ${j.id} (pid ${String(j.pid ?? "?")} not alive) -> failed`);
21344
21192
  }
21345
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
+ }
21346
21209
  function processAlive(pid) {
21347
21210
  try {
21348
21211
  process.kill(pid, 0);
@@ -21359,8 +21222,8 @@ async function defaultCountWorkingBoxes(registry, statusStore, idleGraceMs) {
21359
21222
  return {
21360
21223
  key: b.boxId,
21361
21224
  agentState: active.state,
21362
- sinceUpdateMs: msSince2(active.updatedAt),
21363
- sinceCreateMs: msSince2(b.createdAt)
21225
+ sinceUpdateMs: msSince(active.updatedAt),
21226
+ sinceCreateMs: msSince(b.createdAt)
21364
21227
  };
21365
21228
  });
21366
21229
  const count2 = countWorkingSlots(entries, idleGraceMs);
@@ -21432,7 +21295,7 @@ async function uncachedBoxStateCount() {
21432
21295
  }
21433
21296
  function inspectDockerState(containerName) {
21434
21297
  return new Promise((resolveP) => {
21435
- 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], {
21436
21299
  stdio: ["ignore", "pipe", "pipe"]
21437
21300
  });
21438
21301
  let out = "";
@@ -21466,9 +21329,9 @@ async function defaultSpawnWorker(job) {
21466
21329
  `AGENTBOX_CLI_ENTRY not set or missing (${String(entry)}); cannot spawn queue worker`
21467
21330
  );
21468
21331
  }
21469
- 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 });
21470
21333
  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], {
21334
+ const child = (0, import_node_child_process8.spawn)(process.execPath, [entry, "_run-queued-job", job.id], {
21472
21335
  detached: true,
21473
21336
  stdio: ["ignore", fd, fd],
21474
21337
  env: process.env
@@ -21482,6 +21345,212 @@ async function defaultSpawnWorker(job) {
21482
21345
  }
21483
21346
  var QUEUE_LOGS_DIR = (0, import_node_path8.join)(STATE_DIR, "logs");
21484
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
+
21485
21554
  // src/bin.ts
21486
21555
  var program2 = new Command();
21487
21556
  program2.name("agentbox-relay").description("Host-side HTTP relay for box\u2192host events and RPCs").version("0.0.0");