@edgehero/pi-dispatch-admin 1.7.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 +193 -35
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -9215,6 +9215,84 @@ var init_queue = __esm({
|
|
|
9215
9215
|
}
|
|
9216
9216
|
});
|
|
9217
9217
|
|
|
9218
|
+
// ../worker/src/run-mirror.mjs
|
|
9219
|
+
function bounded2(promise, ms) {
|
|
9220
|
+
let timer;
|
|
9221
|
+
return Promise.race([
|
|
9222
|
+
promise,
|
|
9223
|
+
new Promise((_, reject) => {
|
|
9224
|
+
timer = setTimeout(() => reject(new Error("mirror timeout")), ms);
|
|
9225
|
+
})
|
|
9226
|
+
]).finally(() => clearTimeout(timer));
|
|
9227
|
+
}
|
|
9228
|
+
async function readMirroredRuns(redis, { limit = 50, sinceMs = 0, now = () => Date.now(), timeoutMs = OP_TIMEOUT_MS } = {}) {
|
|
9229
|
+
if (!redis) return { runs: [], degraded: "off" };
|
|
9230
|
+
let ids;
|
|
9231
|
+
try {
|
|
9232
|
+
ids = await bounded2(redis.zrevrangebyscore(RUNS_INDEX, "+inf", `(${sinceMs}`, "LIMIT", 0, Math.max(1, limit)), timeoutMs);
|
|
9233
|
+
} catch (err) {
|
|
9234
|
+
return { runs: [], degraded: `unreachable (${err?.message ?? "?"})` };
|
|
9235
|
+
}
|
|
9236
|
+
if (!Array.isArray(ids) || ids.length === 0) return { runs: [], degraded: "off" };
|
|
9237
|
+
let bodies;
|
|
9238
|
+
try {
|
|
9239
|
+
bodies = await bounded2(redis.mget(...ids.map(runRecordKey)), timeoutMs);
|
|
9240
|
+
} catch (err) {
|
|
9241
|
+
return { runs: [], degraded: `unreachable (${err?.message ?? "?"})` };
|
|
9242
|
+
}
|
|
9243
|
+
const runs = [];
|
|
9244
|
+
const stale = [];
|
|
9245
|
+
for (let i = 0; i < ids.length; i++) {
|
|
9246
|
+
const raw = bodies?.[i];
|
|
9247
|
+
if (typeof raw !== "string" || raw === "") {
|
|
9248
|
+
stale.push(ids[i]);
|
|
9249
|
+
continue;
|
|
9250
|
+
}
|
|
9251
|
+
try {
|
|
9252
|
+
const rec = JSON.parse(raw);
|
|
9253
|
+
if (rec && typeof rec === "object") runs.push(rec);
|
|
9254
|
+
} catch {
|
|
9255
|
+
stale.push(ids[i]);
|
|
9256
|
+
}
|
|
9257
|
+
}
|
|
9258
|
+
if (stale.length > 0) {
|
|
9259
|
+
try {
|
|
9260
|
+
await bounded2(redis.zrem(RUNS_INDEX, ...stale), timeoutMs);
|
|
9261
|
+
} catch {
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
return { runs, degraded: runs.length >= limit ? "truncated" : "ok" };
|
|
9265
|
+
}
|
|
9266
|
+
function mergeRuns(local, mirrored, { limit = 50 } = {}) {
|
|
9267
|
+
const by = /* @__PURE__ */ new Map();
|
|
9268
|
+
const at = (r) => {
|
|
9269
|
+
const t = Date.parse(r?.endedAt ?? r?.startedAt ?? "");
|
|
9270
|
+
return Number.isFinite(t) ? t : -Infinity;
|
|
9271
|
+
};
|
|
9272
|
+
for (const r of Array.isArray(mirrored) ? mirrored : []) if (r?.jobId) by.set(r.jobId, r);
|
|
9273
|
+
for (const r of Array.isArray(local) ? local : []) {
|
|
9274
|
+
if (!r?.jobId) continue;
|
|
9275
|
+
const seen = by.get(r.jobId);
|
|
9276
|
+
if (!seen || at(r) >= at(seen)) by.set(r.jobId, r);
|
|
9277
|
+
}
|
|
9278
|
+
const out = [...by.values()].sort((a, b) => at(b) - at(a));
|
|
9279
|
+
return out.slice(0, Math.max(0, limit));
|
|
9280
|
+
}
|
|
9281
|
+
function hostsIn(runs) {
|
|
9282
|
+
const names = /* @__PURE__ */ new Set();
|
|
9283
|
+
for (const r of runs ?? []) if (typeof r?.host === "string" && r.host !== "") names.add(r.host);
|
|
9284
|
+
return [...names].sort();
|
|
9285
|
+
}
|
|
9286
|
+
var RUNS_INDEX, runRecordKey, DAY_MS, OP_TIMEOUT_MS;
|
|
9287
|
+
var init_run_mirror = __esm({
|
|
9288
|
+
"../worker/src/run-mirror.mjs"() {
|
|
9289
|
+
RUNS_INDEX = "runs:index";
|
|
9290
|
+
runRecordKey = (sanitizedJobId) => `runs:rec:${sanitizedJobId}`;
|
|
9291
|
+
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
9292
|
+
OP_TIMEOUT_MS = 2e3;
|
|
9293
|
+
}
|
|
9294
|
+
});
|
|
9295
|
+
|
|
9218
9296
|
// ../worker/src/git-dirty.mjs
|
|
9219
9297
|
import { execFileSync } from "node:child_process";
|
|
9220
9298
|
function gitDirty(folder, { exec: exec4 = execFileSync } = {}) {
|
|
@@ -9269,7 +9347,7 @@ var init_import_pi = __esm({
|
|
|
9269
9347
|
});
|
|
9270
9348
|
|
|
9271
9349
|
// ../worker/src/docker-run.mjs
|
|
9272
|
-
function
|
|
9350
|
+
function containerSpec({
|
|
9273
9351
|
image,
|
|
9274
9352
|
env,
|
|
9275
9353
|
jobDir,
|
|
@@ -9286,21 +9364,44 @@ function buildDockerRunArgs({
|
|
|
9286
9364
|
if (!image) throw new Error("docker run: image is required");
|
|
9287
9365
|
if (!name) throw new Error("docker run: container name is required");
|
|
9288
9366
|
if (!workspace) throw new Error("docker run: workspace mount is required");
|
|
9289
|
-
const
|
|
9290
|
-
if (
|
|
9291
|
-
|
|
9292
|
-
|
|
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 ?? {})) {
|
|
9293
9395
|
if (v === void 0 || v === null) continue;
|
|
9294
9396
|
args.push("-e", `${k}=${v}`);
|
|
9295
9397
|
}
|
|
9296
|
-
|
|
9297
|
-
args.push(
|
|
9298
|
-
if (outboxDir) args.push("-v", `${outboxDir}:/outbox`);
|
|
9299
|
-
if (sessionDir) args.push("-v", `${sessionDir}:${CONTAINER_SESSION_DIR}`);
|
|
9300
|
-
if (globalPiDir) args.push("-v", `${globalPiDir}:${CONTAINER_GLOBAL_PI_DIR}:ro`);
|
|
9301
|
-
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);
|
|
9302
9400
|
return args;
|
|
9303
9401
|
}
|
|
9402
|
+
function buildDockerRunArgs(opts) {
|
|
9403
|
+
return dockerArgsFromSpec(containerSpec(opts));
|
|
9404
|
+
}
|
|
9304
9405
|
var CONTAINER_GLOBAL_PI_DIR, CONTAINER_SESSION_DIR, CONTAINER_SESSION_FILE, ISOLATION_FLAGS;
|
|
9305
9406
|
var init_docker_run = __esm({
|
|
9306
9407
|
"../worker/src/docker-run.mjs"() {
|
|
@@ -10028,8 +10129,8 @@ var init_graph_model = __esm({
|
|
|
10028
10129
|
|
|
10029
10130
|
// src/costs.mjs
|
|
10030
10131
|
function costsSinceMs(windowKey, nowMs) {
|
|
10031
|
-
if (windowKey === "7d") return nowMs - 7 *
|
|
10032
|
-
if (windowKey === "30d") return nowMs - 30 *
|
|
10132
|
+
if (windowKey === "7d") return nowMs - 7 * DAY_MS2;
|
|
10133
|
+
if (windowKey === "30d") return nowMs - 30 * DAY_MS2;
|
|
10033
10134
|
const now = new Date(nowMs);
|
|
10034
10135
|
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1);
|
|
10035
10136
|
}
|
|
@@ -10152,7 +10253,7 @@ function foldCosts({ records, subscriptions, pricing, nowMs, piAiPin = null, sin
|
|
|
10152
10253
|
const requested = Number.isFinite(sinceMs);
|
|
10153
10254
|
const fromMs = requested ? Math.min(sinceMs, nowMs) : firstRunMs ?? nowMs;
|
|
10154
10255
|
const toMs2 = nowMs;
|
|
10155
|
-
const days = requested || runs.length > 0 ? Math.max(1, Math.ceil((toMs2 - fromMs) /
|
|
10256
|
+
const days = requested || runs.length > 0 ? Math.max(1, Math.ceil((toMs2 - fromMs) / DAY_MS2)) : 0;
|
|
10156
10257
|
return {
|
|
10157
10258
|
// `firstRunMs` rides along (null when nothing ran) so a renderer can pad or trim the daily series
|
|
10158
10259
|
// against the requested edge without re-deriving what the fold already knows.
|
|
@@ -10183,7 +10284,7 @@ function buildDaily(runs, fromMs, toMs2) {
|
|
|
10183
10284
|
buckets.get(day).push(r.contribution);
|
|
10184
10285
|
}
|
|
10185
10286
|
const daily = [];
|
|
10186
|
-
for (let cursor = Math.floor(fromMs /
|
|
10287
|
+
for (let cursor = Math.floor(fromMs / DAY_MS2) * DAY_MS2; cursor <= toMs2; cursor += DAY_MS2) {
|
|
10187
10288
|
const day = utcDay(cursor);
|
|
10188
10289
|
const contribs = buckets.get(day) ?? [];
|
|
10189
10290
|
daily.push({ day, cost: combineContributions(contribs), runs: contribs.length });
|
|
@@ -10207,7 +10308,7 @@ function buildDailyByFlow(runs, fromMs, toMs2) {
|
|
|
10207
10308
|
const rows = [];
|
|
10208
10309
|
for (const [flow, buckets] of byFlow) {
|
|
10209
10310
|
const days = [];
|
|
10210
|
-
for (let cursor = Math.floor(fromMs /
|
|
10311
|
+
for (let cursor = Math.floor(fromMs / DAY_MS2) * DAY_MS2; cursor <= toMs2; cursor += DAY_MS2) {
|
|
10211
10312
|
const day = utcDay(cursor);
|
|
10212
10313
|
const contribs = buckets.get(day) ?? [];
|
|
10213
10314
|
days.push({ day, cost: combineContributions(contribs), runs: contribs.length });
|
|
@@ -10451,7 +10552,7 @@ function peakWindow(w, attributed) {
|
|
|
10451
10552
|
let peakRuns = 0;
|
|
10452
10553
|
let peakTokens = 0;
|
|
10453
10554
|
if (w.rolling) {
|
|
10454
|
-
const span = w.per === "5h" ? 5 * HOUR_MS : w.per === "7d" ? 7 *
|
|
10555
|
+
const span = w.per === "5h" ? 5 * HOUR_MS : w.per === "7d" ? 7 * DAY_MS2 : 30 * DAY_MS2;
|
|
10455
10556
|
let left = 0;
|
|
10456
10557
|
let runsInWindow = 0;
|
|
10457
10558
|
let tokensInWindow = 0;
|
|
@@ -10542,11 +10643,11 @@ function medianQuad(quads) {
|
|
|
10542
10643
|
}
|
|
10543
10644
|
return quad;
|
|
10544
10645
|
}
|
|
10545
|
-
var
|
|
10646
|
+
var DAY_MS2, HOUR_MS, COSTS_WINDOWS, QUAD_KEYS, TRIGGER_TAIL, TRIGGER_TAIL_LABELS;
|
|
10546
10647
|
var init_costs = __esm({
|
|
10547
10648
|
"src/costs.mjs"() {
|
|
10548
10649
|
init_budget();
|
|
10549
|
-
|
|
10650
|
+
DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
10550
10651
|
HOUR_MS = 60 * 60 * 1e3;
|
|
10551
10652
|
COSTS_WINDOWS = ["7d", "30d", "mtd"];
|
|
10552
10653
|
QUAD_KEYS = ["input", "output", "cacheRead", "cacheWrite", "cacheWrite1h"];
|
|
@@ -11209,6 +11310,35 @@ function withTimeout(promise, ms, fallback) {
|
|
|
11209
11310
|
});
|
|
11210
11311
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
11211
11312
|
}
|
|
11313
|
+
async function mergedRunsOn(redis, { logsDir, limit = 10, fs = nodeFs3, timeoutMs = 2500 } = {}) {
|
|
11314
|
+
const local = listRuns({ logsDir, limit, fs });
|
|
11315
|
+
if (!Array.isArray(local)) return { runs: [], hosts: [], mirror: "off", ...local };
|
|
11316
|
+
try {
|
|
11317
|
+
const { runs: mirrored, degraded } = await readMirroredRuns(redis, { limit: Math.max(limit * 2, 50), timeoutMs });
|
|
11318
|
+
const runs = mergeRuns(local, mirrored, { limit });
|
|
11319
|
+
return { runs, hosts: hostsIn(runs), mirror: degraded };
|
|
11320
|
+
} catch (err) {
|
|
11321
|
+
return { runs: local, hosts: hostsIn(local), mirror: `unreachable (${err?.message ?? "?"})` };
|
|
11322
|
+
}
|
|
11323
|
+
}
|
|
11324
|
+
async function listRunsMerged({ logsDir, limit = 10, url, fs = nodeFs3, redisFn = makeRedisClient, timeoutMs = 2500 } = {}) {
|
|
11325
|
+
let redis;
|
|
11326
|
+
try {
|
|
11327
|
+
redis = redisFn(url);
|
|
11328
|
+
redis.on?.("error", () => {
|
|
11329
|
+
});
|
|
11330
|
+
return await mergedRunsOn(redis, { logsDir, limit, fs, timeoutMs });
|
|
11331
|
+
} catch (err) {
|
|
11332
|
+
const local = listRuns({ logsDir, limit, fs });
|
|
11333
|
+
const runs = Array.isArray(local) ? local : [];
|
|
11334
|
+
return { runs, hosts: hostsIn(runs), mirror: `unreachable (${err?.message ?? "?"})` };
|
|
11335
|
+
} finally {
|
|
11336
|
+
try {
|
|
11337
|
+
redis?.disconnect?.();
|
|
11338
|
+
} catch {
|
|
11339
|
+
}
|
|
11340
|
+
}
|
|
11341
|
+
}
|
|
11212
11342
|
function listRuns({ logsDir, limit = 10, fs = nodeFs3 }) {
|
|
11213
11343
|
const cap = clampLimit(limit);
|
|
11214
11344
|
let names;
|
|
@@ -11251,12 +11381,13 @@ function scanRunRecords({ logsDir, sinceMs, nowMs = Date.now(), fs = nodeFs3 })
|
|
|
11251
11381
|
function readRun({ logsDir, jobId, fs = nodeFs3 }) {
|
|
11252
11382
|
return readJsonFile(join2(logsDir, `${sanitizeJobId(jobId)}.json`), fs);
|
|
11253
11383
|
}
|
|
11254
|
-
function readLogTail({ logsDir, jobId, lines = 200, fs = nodeFs3 }) {
|
|
11384
|
+
function readLogTail({ logsDir, jobId, lines = 200, fs = nodeFs3, host = null, self = null }) {
|
|
11255
11385
|
let text;
|
|
11256
11386
|
try {
|
|
11257
11387
|
text = fs.readFileSync(join2(logsDir, `${sanitizeJobId(jobId)}.log`), "utf8");
|
|
11258
11388
|
} catch {
|
|
11259
|
-
|
|
11389
|
+
const foreign = typeof host === "string" && host !== "" && typeof self === "string" && self !== "" && host !== self;
|
|
11390
|
+
return foreign ? { missing: true, elsewhere: host } : { missing: true };
|
|
11260
11391
|
}
|
|
11261
11392
|
const all = text.split("\n").filter((line) => line !== "");
|
|
11262
11393
|
const cap = clampLines(lines);
|
|
@@ -11772,6 +11903,7 @@ var init_read_model = __esm({
|
|
|
11772
11903
|
init_connection();
|
|
11773
11904
|
init_host_registry();
|
|
11774
11905
|
init_queue();
|
|
11906
|
+
init_run_mirror();
|
|
11775
11907
|
init_flow_gate();
|
|
11776
11908
|
init_git_dirty();
|
|
11777
11909
|
init_packages();
|
|
@@ -40066,10 +40198,11 @@ var init_render = __esm({
|
|
|
40066
40198
|
});
|
|
40067
40199
|
|
|
40068
40200
|
// ../worker/src/scheduler-stall-guard.mjs
|
|
40069
|
-
var STALL_KEY, STALL_WINDOW_SECONDS;
|
|
40201
|
+
var STALL_KEY, stallKey, STALL_WINDOW_SECONDS;
|
|
40070
40202
|
var init_scheduler_stall_guard = __esm({
|
|
40071
40203
|
"../worker/src/scheduler-stall-guard.mjs"() {
|
|
40072
40204
|
STALL_KEY = "pi-dispatch:sched-stalls";
|
|
40205
|
+
stallKey = (schedulerId) => `${STALL_KEY}:${schedulerId}`;
|
|
40073
40206
|
STALL_WINDOW_SECONDS = 24 * 60 * 60;
|
|
40074
40207
|
}
|
|
40075
40208
|
});
|
|
@@ -40334,7 +40467,10 @@ function createDashboardDeps(paths, {
|
|
|
40334
40467
|
return {
|
|
40335
40468
|
async fetchSnapshot() {
|
|
40336
40469
|
const queues = await fleetQueues();
|
|
40337
|
-
const
|
|
40470
|
+
const merged = await mergedRunsOn(redis, { logsDir: paths.logsDir, limit: RUNS_ON_DASHBOARD }).catch(() => ({ runs: [], hosts: [], mirror: "off" }));
|
|
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([
|
|
40338
40474
|
Promise.all(queues.map((q) => q.isPaused())),
|
|
40339
40475
|
Promise.all(queues.map((q) => q.getJobCounts("waiting", "active", "paused", "delayed", "failed"))),
|
|
40340
40476
|
queue.getWorkers().catch(() => []),
|
|
@@ -40348,8 +40484,10 @@ function createDashboardDeps(paths, {
|
|
|
40348
40484
|
Promise.all(queues.map((q, i) => i === 0 ? q.getJobSchedulers(0, -1, true) : q.getJobSchedulers(0, -1, true).catch(() => []))),
|
|
40349
40485
|
Promise.all(queues.map((q) => q.getActive(0, 0).catch(() => []))),
|
|
40350
40486
|
// Per-scheduler stall counts (money backstop) for the cron drill-in; reuses the held client like the
|
|
40351
|
-
// budget GETs.
|
|
40352
|
-
|
|
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([])
|
|
40353
40491
|
]);
|
|
40354
40492
|
const workers = Array.isArray(workerList) && workerList.length > 0 ? workerList.length : "unknown";
|
|
40355
40493
|
const counts = {};
|
|
@@ -40382,11 +40520,18 @@ function createDashboardDeps(paths, {
|
|
|
40382
40520
|
queue: { pausedState, pausedPartial, counts, workers, queues: queues.length, fleetDegraded },
|
|
40383
40521
|
budget: { day: Number(dayRaw ?? 0), week: Number(weekRaw ?? 0), month: Number(monthRaw ?? 0), tokensToday: Number(tokenRaw ?? 0) },
|
|
40384
40522
|
schedulers: mapSchedulers(schedulerList, Date.now()),
|
|
40385
|
-
|
|
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])),
|
|
40386
40526
|
schedulerStallMax: paths.schedulerStallMax,
|
|
40387
|
-
|
|
40527
|
+
// Merged across the fleet (issue #57, Gap 3). This host's files stay the truth and the single-host
|
|
40528
|
+
// path; the mirror adds the other hosts' records. On a shared `PI_LOGS_DIR` the local read is
|
|
40529
|
+
// already the merged read and the mirror adds nothing, which is why there is no second code path.
|
|
40530
|
+
runs: merged.runs,
|
|
40531
|
+
runHosts: merged.hosts,
|
|
40532
|
+
runMirror: merged.mirror,
|
|
40388
40533
|
settings: readSettingsView({ settingsFile: paths.settingsFile }),
|
|
40389
|
-
triggers:
|
|
40534
|
+
triggers: triggersView,
|
|
40390
40535
|
pauseWindows: readPauseWindows({ pauseWindowsPath: paths.pauseWindowsPath }),
|
|
40391
40536
|
// The operator's staged third-party pi packages (REQ-GLOBAL-PI-OVERLAY), for the armed triggers'
|
|
40392
40537
|
// trust model. Like the four reads above it is a plain file read whose fs access lives entirely in
|
|
@@ -40847,7 +40992,11 @@ function renderPanel(snapshot, width, state, styler) {
|
|
|
40847
40992
|
]
|
|
40848
40993
|
},
|
|
40849
40994
|
{ title: "TRIGGERS", lines: toLines(renderTriggers({ schedulers: snapshot.schedulers, triggers: snapshot.triggers })) },
|
|
40850
|
-
|
|
40995
|
+
// The section TITLE carries the fleet fact, not a seventh cell in each row (issue #57, Gap 3). A cell
|
|
40996
|
+
// would compete with jobId and target inside one `fitLine(..., inner)` and clip silently at width 80;
|
|
40997
|
+
// the title answers the completeness question once rather than fifty times. Absent on a single host,
|
|
40998
|
+
// so that output is byte-identical.
|
|
40999
|
+
{ title: runsTitle(snapshot), lines: renderRunList(buildRows(snapshot, runSort), selected, 24) },
|
|
40851
41000
|
{ title: "SETTINGS", lines: toLines(renderSettingsView(snapshot.settings)) }
|
|
40852
41001
|
];
|
|
40853
41002
|
const scopedText = renderScopedLimits({ limits: snapshot.scopedLimits, scopedBudget: snapshot.scopedBudget });
|
|
@@ -41148,6 +41297,13 @@ function limitRow(l, used, inner, styler) {
|
|
|
41148
41297
|
if (Number.isInteger(l.concurrent)) bits.push(styler.fg("muted", `\u2264${l.concurrent} at once`));
|
|
41149
41298
|
return fitLine(bits.join(styler.fg("dim", " ")), inner, styler);
|
|
41150
41299
|
}
|
|
41300
|
+
function runsTitle(snapshot) {
|
|
41301
|
+
const hosts = Array.isArray(snapshot?.runHosts) ? snapshot.runHosts : [];
|
|
41302
|
+
if (hosts.length > 1) return `RUNS \xB7 ${hosts.length} hosts`;
|
|
41303
|
+
const mirror = snapshot?.runMirror;
|
|
41304
|
+
if (typeof mirror === "string" && mirror.startsWith("unreachable")) return "RUNS \xB7 this host only";
|
|
41305
|
+
return "RUNS";
|
|
41306
|
+
}
|
|
41151
41307
|
function runLines(rows, selected, inner, styler) {
|
|
41152
41308
|
if (!Array.isArray(rows) || rows.length === 0) return [styler.cell("(no runs)", inner, { color: "dim" })];
|
|
41153
41309
|
const { top, count } = runsWindow(rows.length, selected);
|
|
@@ -42121,8 +42277,8 @@ var init_setup_wizard = __esm({
|
|
|
42121
42277
|
init_deployment_pointer();
|
|
42122
42278
|
init_read_model();
|
|
42123
42279
|
init_index();
|
|
42124
|
-
RUNTIME_VERSION = "1.
|
|
42125
|
-
RECEIVER_VERSION = "1.
|
|
42280
|
+
RUNTIME_VERSION = "1.9.0";
|
|
42281
|
+
RECEIVER_VERSION = "1.5.0";
|
|
42126
42282
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
42127
42283
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
42128
42284
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -42309,8 +42465,9 @@ function registerTools(pi) {
|
|
|
42309
42465
|
}),
|
|
42310
42466
|
async execute(_toolCallId, params) {
|
|
42311
42467
|
const paths = resolvePaths(process.env);
|
|
42312
|
-
|
|
42313
|
-
|
|
42468
|
+
if (params.jobId) return toolText(JSON.stringify(readRun({ logsDir: paths.logsDir, jobId: params.jobId })));
|
|
42469
|
+
const merged = await listRunsMerged({ logsDir: paths.logsDir, limit: params.limit ?? 10, url: paths.valkeyUrl });
|
|
42470
|
+
return toolText(JSON.stringify(merged));
|
|
42314
42471
|
}
|
|
42315
42472
|
});
|
|
42316
42473
|
pi.registerTool({
|
|
@@ -43597,7 +43754,8 @@ async function showLogs(logsDir, tokens, ctx) {
|
|
|
43597
43754
|
return;
|
|
43598
43755
|
}
|
|
43599
43756
|
const lines = tokens[2] ? Number(tokens[2]) : void 0;
|
|
43600
|
-
const
|
|
43757
|
+
const record = readRun({ logsDir, jobId });
|
|
43758
|
+
const tail = readLogTail({ logsDir, jobId, lines, host: record?.host ?? null, self: process.env.PI_WORKER_NAME ?? null });
|
|
43601
43759
|
const custom = ctx?.ui?.custom;
|
|
43602
43760
|
if (typeof custom !== "function") {
|
|
43603
43761
|
const message = "logs viewer unavailable in this pi version -- raw logs are never sent to the model";
|
|
@@ -43616,7 +43774,7 @@ function makeLogViewer(jobId, tail) {
|
|
|
43616
43774
|
const component = {
|
|
43617
43775
|
render(_width) {
|
|
43618
43776
|
if (missing) {
|
|
43619
|
-
return [`logs ${jobId} -- no captured log (PI_CAPTURE_JOB_LOGS off or not found). Esc to close.`, ""];
|
|
43777
|
+
return tail.elsewhere ? [`logs ${jobId} -- this run happened on ${tail.elsewhere}; its raw log stays on that host. Esc to close.`, ""] : [`logs ${jobId} -- no captured log (PI_CAPTURE_JOB_LOGS off or not found). Esc to close.`, ""];
|
|
43620
43778
|
}
|
|
43621
43779
|
const out = [`logs ${jobId} -- ${lines.length} line(s). Up/Down scroll, PgUp/PgDn page, Esc close.`, ""];
|
|
43622
43780
|
for (const line of lines.slice(top, top + VIEWPORT_LINES)) out.push(line);
|
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",
|