@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.
- package/dist/index.mjs +48 -18
- 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
|
|
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
|
|
9368
|
-
if (
|
|
9369
|
-
|
|
9370
|
-
|
|
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
|
-
|
|
9375
|
-
args.push(
|
|
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
|
|
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.
|
|
40462
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|