@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.
Files changed (2) hide show
  1. package/dist/index.mjs +193 -35
  2. 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 buildDockerRunArgs({
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 args = ["run", `--name=${name}`, ...ISOLATION_FLAGS, `--memory=${memory}`, `--cpus=${cpus}`];
9290
- if (network) args.push(`--network=${network}`);
9291
- args.push(...extraFlags);
9292
- 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 ?? {})) {
9293
9395
  if (v === void 0 || v === null) continue;
9294
9396
  args.push("-e", `${k}=${v}`);
9295
9397
  }
9296
- if (jobDir) args.push("-v", `${jobDir}:/job:ro`);
9297
- args.push("-v", `${workspace}:/workspace`);
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 * DAY_MS;
10032
- if (windowKey === "30d") return nowMs - 30 * DAY_MS;
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) / DAY_MS)) : 0;
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 / DAY_MS) * DAY_MS; cursor <= toMs2; cursor += DAY_MS) {
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 / DAY_MS) * DAY_MS; cursor <= toMs2; cursor += DAY_MS) {
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 * DAY_MS : 30 * DAY_MS;
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 DAY_MS, HOUR_MS, COSTS_WINDOWS, QUAD_KEYS, TRIGGER_TAIL, TRIGGER_TAIL_LABELS;
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
- DAY_MS = 24 * 60 * 60 * 1e3;
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
- return { missing: true };
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 [pausedStates, countsPer, workerList, dayRaw, weekRaw, monthRaw, tokenRaw, schedulerLists, activeLists, stallHash] = await Promise.all([
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. HGETALL of an absent key is `{}`, so a never-stalled deployment shows 0 stalls.
40352
- 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([])
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
- 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])),
40386
40526
  schedulerStallMax: paths.schedulerStallMax,
40387
- runs: listRuns({ logsDir: paths.logsDir, limit: RUNS_ON_DASHBOARD }),
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: readTriggers({ triggersPath: paths.triggersPath }),
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
- { title: "RUNS", lines: renderRunList(buildRows(snapshot, runSort), selected, 24) },
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.7.0";
42125
- RECEIVER_VERSION = "1.4.0";
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
- const data = params.jobId ? readRun({ logsDir: paths.logsDir, jobId: params.jobId }) : listRuns({ logsDir: paths.logsDir, limit: params.limit ?? 10 });
42313
- return toolText(JSON.stringify(data));
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 tail = readLogTail({ logsDir, jobId, lines });
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.7.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",