@edgehero/pi-dispatch-admin 1.8.0 → 1.9.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 (2) hide show
  1. package/dist/index.mjs +48 -18
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -9347,7 +9347,7 @@ var init_import_pi = __esm({
9347
9347
  });
9348
9348
 
9349
9349
  // ../worker/src/docker-run.mjs
9350
- function buildDockerRunArgs({
9350
+ function containerSpec({
9351
9351
  image,
9352
9352
  env,
9353
9353
  jobDir,
@@ -9364,21 +9364,44 @@ function buildDockerRunArgs({
9364
9364
  if (!image) throw new Error("docker run: image is required");
9365
9365
  if (!name) throw new Error("docker run: container name is required");
9366
9366
  if (!workspace) throw new Error("docker run: workspace mount is required");
9367
- const args = ["run", `--name=${name}`, ...ISOLATION_FLAGS, `--memory=${memory}`, `--cpus=${cpus}`];
9368
- if (network) args.push(`--network=${network}`);
9369
- args.push(...extraFlags);
9370
- for (const [k, v] of Object.entries(env ?? {})) {
9367
+ const mounts = [];
9368
+ if (jobDir) mounts.push({ host: jobDir, container: "/job", readOnly: true });
9369
+ mounts.push({ host: workspace, container: "/workspace", readOnly: false });
9370
+ if (outboxDir) mounts.push({ host: outboxDir, container: "/outbox", readOnly: false });
9371
+ if (sessionDir) mounts.push({ host: sessionDir, container: CONTAINER_SESSION_DIR, readOnly: false });
9372
+ if (globalPiDir) mounts.push({ host: globalPiDir, container: CONTAINER_GLOBAL_PI_DIR, readOnly: true });
9373
+ return {
9374
+ image,
9375
+ name,
9376
+ memory,
9377
+ cpus,
9378
+ network,
9379
+ // UNCONDITIONALLY true, and there is deliberately no parameter that can unset it. The boundary is
9380
+ // not a thing a caller opts into -- CONST-ISOLATION-CONTAINER-PER-JOB is why every other flag here
9381
+ // exists -- so the spec is simply unable to describe an unisolated container, and the builder below
9382
+ // refuses one it is handed. A field that could be false would be a way to ask for less.
9383
+ isolated: true,
9384
+ mounts,
9385
+ env,
9386
+ dockerExtra: extraFlags
9387
+ };
9388
+ }
9389
+ function dockerArgsFromSpec(spec) {
9390
+ if (spec?.isolated !== true) throw new Error("docker run: refusing to build an argv for a spec that is not isolated");
9391
+ const args = ["run", `--name=${spec.name}`, ...ISOLATION_FLAGS, `--memory=${spec.memory}`, `--cpus=${spec.cpus}`];
9392
+ if (spec.network) args.push(`--network=${spec.network}`);
9393
+ args.push(...spec.dockerExtra ?? []);
9394
+ for (const [k, v] of Object.entries(spec.env ?? {})) {
9371
9395
  if (v === void 0 || v === null) continue;
9372
9396
  args.push("-e", `${k}=${v}`);
9373
9397
  }
9374
- if (jobDir) args.push("-v", `${jobDir}:/job:ro`);
9375
- args.push("-v", `${workspace}:/workspace`);
9376
- if (outboxDir) args.push("-v", `${outboxDir}:/outbox`);
9377
- if (sessionDir) args.push("-v", `${sessionDir}:${CONTAINER_SESSION_DIR}`);
9378
- if (globalPiDir) args.push("-v", `${globalPiDir}:${CONTAINER_GLOBAL_PI_DIR}:ro`);
9379
- args.push(image);
9398
+ for (const m of spec.mounts ?? []) args.push("-v", `${m.host}:${m.container}${m.readOnly ? ":ro" : ""}`);
9399
+ args.push(spec.image);
9380
9400
  return args;
9381
9401
  }
9402
+ function buildDockerRunArgs(opts) {
9403
+ return dockerArgsFromSpec(containerSpec(opts));
9404
+ }
9382
9405
  var CONTAINER_GLOBAL_PI_DIR, CONTAINER_SESSION_DIR, CONTAINER_SESSION_FILE, ISOLATION_FLAGS;
9383
9406
  var init_docker_run = __esm({
9384
9407
  "../worker/src/docker-run.mjs"() {
@@ -40175,10 +40198,11 @@ var init_render = __esm({
40175
40198
  });
40176
40199
 
40177
40200
  // ../worker/src/scheduler-stall-guard.mjs
40178
- var STALL_KEY, STALL_WINDOW_SECONDS;
40201
+ var STALL_KEY, stallKey, STALL_WINDOW_SECONDS;
40179
40202
  var init_scheduler_stall_guard = __esm({
40180
40203
  "../worker/src/scheduler-stall-guard.mjs"() {
40181
40204
  STALL_KEY = "pi-dispatch:sched-stalls";
40205
+ stallKey = (schedulerId) => `${STALL_KEY}:${schedulerId}`;
40182
40206
  STALL_WINDOW_SECONDS = 24 * 60 * 60;
40183
40207
  }
40184
40208
  });
@@ -40444,7 +40468,9 @@ function createDashboardDeps(paths, {
40444
40468
  async fetchSnapshot() {
40445
40469
  const queues = await fleetQueues();
40446
40470
  const merged = await mergedRunsOn(redis, { logsDir: paths.logsDir, limit: RUNS_ON_DASHBOARD }).catch(() => ({ runs: [], hosts: [], mirror: "off" }));
40447
- const [pausedStates, countsPer, workerList, dayRaw, weekRaw, monthRaw, tokenRaw, schedulerLists, activeLists, stallHash] = await Promise.all([
40471
+ const triggersView = readTriggers({ triggersPath: paths.triggersPath });
40472
+ const cronIds = Array.isArray(triggersView?.triggers) ? triggersView.triggers.filter((t) => t?.type === "cron" && typeof t?.id === "string" && t.id !== "").map((t) => t.id) : [];
40473
+ const [pausedStates, countsPer, workerList, dayRaw, weekRaw, monthRaw, tokenRaw, schedulerLists, activeLists, stallCounts] = await Promise.all([
40448
40474
  Promise.all(queues.map((q) => q.isPaused())),
40449
40475
  Promise.all(queues.map((q) => q.getJobCounts("waiting", "active", "paused", "delayed", "failed"))),
40450
40476
  queue.getWorkers().catch(() => []),
@@ -40458,8 +40484,10 @@ function createDashboardDeps(paths, {
40458
40484
  Promise.all(queues.map((q, i) => i === 0 ? q.getJobSchedulers(0, -1, true) : q.getJobSchedulers(0, -1, true).catch(() => []))),
40459
40485
  Promise.all(queues.map((q) => q.getActive(0, 0).catch(() => []))),
40460
40486
  // Per-scheduler stall counts (money backstop) for the cron drill-in; reuses the held client like the
40461
- // budget GETs. HGETALL of an absent key is `{}`, so a never-stalled deployment shows 0 stalls.
40462
- redis.hgetall(STALL_KEY).catch(() => ({}))
40487
+ // budget GETs. ONE MGET for every declared cron id, so this stays one round trip however many
40488
+ // schedulers exist. An absent key reads as an honest 0 -- a scheduler that has never stalled -- which
40489
+ // is the same posture `readScopedBudget` takes for a scope that has never run.
40490
+ cronIds.length > 0 ? redis.mget(...cronIds.map((id) => stallKey(id))).catch(() => []) : Promise.resolve([])
40463
40491
  ]);
40464
40492
  const workers = Array.isArray(workerList) && workerList.length > 0 ? workerList.length : "unknown";
40465
40493
  const counts = {};
@@ -40492,7 +40520,9 @@ function createDashboardDeps(paths, {
40492
40520
  queue: { pausedState, pausedPartial, counts, workers, queues: queues.length, fleetDegraded },
40493
40521
  budget: { day: Number(dayRaw ?? 0), week: Number(weekRaw ?? 0), month: Number(monthRaw ?? 0), tokensToday: Number(tokenRaw ?? 0) },
40494
40522
  schedulers: mapSchedulers(schedulerList, Date.now()),
40495
- schedulerStalls: stallHash ?? {},
40523
+ // Rebuilt into the `{ id -> count }` shape the drill-in and the LIST badge already index by, so
40524
+ // neither reader changes.
40525
+ schedulerStalls: Object.fromEntries(cronIds.map((id, i) => [id, Number(stallCounts?.[i] ?? 0) || 0])),
40496
40526
  schedulerStallMax: paths.schedulerStallMax,
40497
40527
  // Merged across the fleet (issue #57, Gap 3). This host's files stay the truth and the single-host
40498
40528
  // path; the mirror adds the other hosts' records. On a shared `PI_LOGS_DIR` the local read is
@@ -40501,7 +40531,7 @@ function createDashboardDeps(paths, {
40501
40531
  runHosts: merged.hosts,
40502
40532
  runMirror: merged.mirror,
40503
40533
  settings: readSettingsView({ settingsFile: paths.settingsFile }),
40504
- triggers: readTriggers({ triggersPath: paths.triggersPath }),
40534
+ triggers: triggersView,
40505
40535
  pauseWindows: readPauseWindows({ pauseWindowsPath: paths.pauseWindowsPath }),
40506
40536
  // The operator's staged third-party pi packages (REQ-GLOBAL-PI-OVERLAY), for the armed triggers'
40507
40537
  // trust model. Like the four reads above it is a plain file read whose fs access lives entirely in
@@ -42247,7 +42277,7 @@ var init_setup_wizard = __esm({
42247
42277
  init_deployment_pointer();
42248
42278
  init_read_model();
42249
42279
  init_index();
42250
- RUNTIME_VERSION = "1.8.0";
42280
+ RUNTIME_VERSION = "1.9.0";
42251
42281
  RECEIVER_VERSION = "1.5.0";
42252
42282
  RUNTIME_PKG = "@edgehero/pi-dispatch";
42253
42283
  RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgehero/pi-dispatch-admin",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Operator console (a pi extension) + skill for pi-dispatch: run the pi coding agent as a self-hosted service. A /dispatch TUI for the queue, spend caps, run history, editable GitHub, GitLab, Forgejo and Azure DevOps triggers (cron/label/comment/PR/MR/work item), and scheduled pause windows — plus AI-operable, human-confirmed controls. Runs against a live pi-dispatch deployment.",
5
5
  "keywords": [
6
6
  "pi-package",