@edgehero/pi-dispatch-admin 5.2.0 → 5.2.1

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 CHANGED
@@ -7620,6 +7620,13 @@ var init_worker_name = __esm({
7620
7620
  });
7621
7621
 
7622
7622
  // ../worker/src/job-size.mjs
7623
+ function wholeCpusOf(cpuCenti) {
7624
+ return Math.ceil(cpuCenti / 100);
7625
+ }
7626
+ function jobSizeEnv(size) {
7627
+ assertJobSize(size);
7628
+ return { PI_JOB_SIZE_CPUS: String(wholeCpusOf(size.cpuCenti)), PI_JOB_SIZE_MEMORY_MIB: String(size.memMiB) };
7629
+ }
7623
7630
  function parseMemory(value) {
7624
7631
  if (typeof value !== "string" || !/^[1-9]\d*[mg]$/.test(value)) {
7625
7632
  throw new Error(`a memory size is a whole number of megabytes or gigabytes such as "512m", "1536m" or "4g" (got ${JSON.stringify(value)})`);
@@ -8803,6 +8810,19 @@ function roundCpusUp(cpuCenti) {
8803
8810
  function rank(sorted, pct5) {
8804
8811
  return sorted[Math.max(0, Math.ceil(pct5 * sorted.length / 100) - 1)];
8805
8812
  }
8813
+ function seenOf(record3) {
8814
+ if (!CEILING_VENUES.includes(record3.backend)) return null;
8815
+ const c = record3.capacity;
8816
+ if (c === null || typeof c !== "object" || Array.isArray(c)) return null;
8817
+ const cpus = Number.isSafeInteger(c.cpus) && c.cpus >= 1 && c.cpus <= HOST_CPUS_MAX ? c.cpus : null;
8818
+ const budget = Number.isSafeInteger(c.cpuCenti) && c.cpuCenti >= 1 && c.cpuCenti <= HOST_CPUS_MAX * 100 ? c.cpuCenti : null;
8819
+ const ceilingCenti = cpuCeilingCenti(cpus, budget);
8820
+ return ceilingCenti === null ? null : seenCpusOf(ceilingCenti, cpus);
8821
+ }
8822
+ function seenCpusOf(ceilingCenti, runtimeCpus) {
8823
+ if (ceilingCenti >= 100) return Math.floor(ceilingCenti / 100);
8824
+ return Number.isSafeInteger(runtimeCpus) && runtimeCpus >= 1 ? runtimeCpus : null;
8825
+ }
8806
8826
  function evidenceOf(record3, project2, nowMs2) {
8807
8827
  if (record3 === null || typeof record3 !== "object" || Array.isArray(record3)) return null;
8808
8828
  if (record3.project !== project2) return null;
@@ -8822,7 +8842,7 @@ function evidenceOf(record3, project2, nowMs2) {
8822
8842
  };
8823
8843
  const memFullUsec = wallUsec !== null && isCount(r2.memFullUsec) ? r2.memFullUsec : null;
8824
8844
  if (memPeak === null && cpu.cpuUsec === null) return null;
8825
- return { at: at2, size, memPeak, oom: record3.reason === "oom-killed", wallUsec, ...cpu, memFullUsec };
8845
+ return { at: at2, size, memPeak, oom: record3.reason === "oom-killed", wallUsec, ...cpu, memFullUsec, seenCpus: seenOf(record3) };
8826
8846
  }
8827
8847
  function heldToCap(wanted, current, cap) {
8828
8848
  if (wanted <= current) return { suggested: null, held: "largest" };
@@ -8831,6 +8851,14 @@ function heldToCap(wanted, current, cap) {
8831
8851
  if (wanted > cap) return { suggested: cap, held: "cap" };
8832
8852
  return { suggested: wanted, held: null };
8833
8853
  }
8854
+ function oomCeilingOf(relevant) {
8855
+ const over = relevant.filter((e2) => e2.oom && e2.seenCpus !== null && e2.seenCpus > wholeCpusOf(e2.size.cpuCenti));
8856
+ if (over.length === 0) return null;
8857
+ const seen = over.map((e2) => e2.seenCpus);
8858
+ const sizes = over.map((e2) => e2.size.cpuCenti);
8859
+ const sizeCenti = Math.min(...sizes);
8860
+ return { runs: over.length, seenCpus: Math.min(...seen), seenMaxCpus: Math.max(...seen), sizeCenti, sizeVaries: sizes.some((n3) => n3 !== sizeCenti) };
8861
+ }
8834
8862
  function suggestMemory(runs, current, cap) {
8835
8863
  const measured = runs.filter((e2) => e2.memPeak !== null);
8836
8864
  const relevant = measured.filter((e2) => e2.size.memMiB >= current);
@@ -8841,7 +8869,7 @@ function suggestMemory(runs, current, cap) {
8841
8869
  const oomSizes = measured.filter((e2) => e2.oom).map((e2) => e2.size.memMiB);
8842
8870
  const largestOom = oomSizes.length > 0 ? Math.max(...oomSizes) : null;
8843
8871
  const pressured = relevant.filter((e2) => !productAbove(e2.size.memMiB * MIB, 9, e2.memPeak, 10) && e2.memFullUsec !== null && productAbove(e2.memFullUsec, 100, e2.wallUsec, 1)).length;
8844
- const evidence = { samples, smaller: measured.length - samples, p95MiB: p95 === null ? null : Math.ceil(p95 / MIB), maxPeakMiB: maxPeak === null ? null : Math.ceil(maxPeak / MIB), ooms, largestOomMiB: largestOom, pressured };
8872
+ const evidence = { samples, smaller: measured.length - samples, p95MiB: p95 === null ? null : Math.ceil(p95 / MIB), maxPeakMiB: maxPeak === null ? null : Math.ceil(maxPeak / MIB), ooms, largestOomMiB: largestOom, pressured, oomCeiling: oomCeilingOf(relevant) };
8845
8873
  const fact = pressured > 0 ? "pressure" : null;
8846
8874
  const out = (reason, suggested, more = {}) => ({ current, suggested: suggested === null || suggested === current ? null : suggested, reason, evidence, fact, held: null, wanted: null, ...more });
8847
8875
  if (ooms > 0) {
@@ -8964,6 +8992,15 @@ function notEnoughRunsText(dim, sizeText3) {
8964
8992
  const smaller = Number.isSafeInteger(e2.smaller) && e2.smaller > 0 ? `; ${e2.smaller} at smaller sizes` : "";
8965
8993
  return `not enough runs at ${sizeText3(dim.current)} (${samples}${smaller})`;
8966
8994
  }
8995
+ function oomCeilingWords(me) {
8996
+ const o = me?.oomCeiling;
8997
+ if (o === null || typeof o !== "object" || !Number.isSafeInteger(o.runs) || o.runs < 1 || !Number.isSafeInteger(o.seenCpus)) return "";
8998
+ const ooms = Number.isSafeInteger(me.ooms) && me.ooms >= o.runs ? me.ooms : o.runs;
8999
+ const whose = ooms === 1 ? "the killed run's tools" : o.runs === ooms ? "the killed runs' tools" : `the tools of ${o.runs} of the ${ooms} killed runs`;
9000
+ const seen = `${o.seenCpus}${Number.isSafeInteger(o.seenMaxCpus) && o.seenMaxCpus > o.seenCpus ? ` to ${o.seenMaxCpus}` : ""} CPUs`;
9001
+ const size = o.sizeVaries === true ? `sizes from ${cpusText(o.sizeCenti)}` : `a size of ${cpusText(o.sizeCenti)}`;
9002
+ return `size per-CPU worker pools from PI_JOB_SIZE_CPUS: ${whose} counted at least ${seen} at ${size}`;
9003
+ }
8967
9004
  function suggestionEvidence(suggestion) {
8968
9005
  const m2 = suggestion?.memory ?? {};
8969
9006
  const c = suggestion?.cpu ?? {};
@@ -8994,10 +9031,11 @@ function suggestionEvidence(suggestion) {
8994
9031
  cpu: cpuWords[c.reason] ?? "",
8995
9032
  memoryHeld: heldWords[m2.held] ?? "",
8996
9033
  memoryFact: m2.fact === "pressure" ? `${me.pressured} of ${plural(me.samples, "run")} reached the memory limit while stalled for memory more than 1% of their time` : "",
8997
- cpuFact: c.fact === "ceiling" ? `the median run was held back ${ce.throttledPct}% of its time by this host's CPU ceiling (its CPU budget, shared by every job), which a job's cpus do not change` : ""
9034
+ cpuFact: c.fact === "ceiling" ? `the median run was held back ${ce.throttledPct}% of its time by this host's CPU ceiling (its CPU budget, shared by every job), which a job's cpus do not change` : "",
9035
+ memoryCeiling: m2.reason === "oom-killed" ? oomCeilingWords(me) : ""
8998
9036
  };
8999
9037
  }
9000
- var SUGGEST_WINDOW_DAYS, SUGGEST_WINDOW_RUNS, SUGGEST_MIN_SAMPLES, SUGGEST_CLOCK_SKEW_MS, SUGGEST_MAX_WALL_MS, MEMORY_REASONS, CPU_REASONS, MEMORY_HELD, MEMORY_CAP_MISSING, SIZE_FACTS, MIB, DAY_MS, isCount, productAbove, quarterMoreMiB, quarterMoreCenti, capDim, isShare, shareOf, plural, coresText, cpusText;
9038
+ var SUGGEST_WINDOW_DAYS, SUGGEST_WINDOW_RUNS, SUGGEST_MIN_SAMPLES, SUGGEST_CLOCK_SKEW_MS, SUGGEST_MAX_WALL_MS, MEMORY_REASONS, CPU_REASONS, MEMORY_HELD, MEMORY_CAP_MISSING, SIZE_FACTS, CEILING_VENUES, MIB, DAY_MS, isCount, productAbove, quarterMoreMiB, quarterMoreCenti, capDim, isShare, shareOf, plural, coresText, cpusText;
9001
9039
  var init_size_suggest = __esm({
9002
9040
  "../worker/src/size-suggest.mjs"() {
9003
9041
  init_job_size();
@@ -9011,6 +9049,7 @@ var init_size_suggest = __esm({
9011
9049
  MEMORY_HELD = Object.freeze(["cap", "largest", "no-cap"]);
9012
9050
  MEMORY_CAP_MISSING = Object.freeze(["unread", "none-holds", "off"]);
9013
9051
  SIZE_FACTS = Object.freeze(["pressure", "ceiling"]);
9052
+ CEILING_VENUES = Object.freeze(["local", "podman"]);
9014
9053
  MIB = 1024 * 1024;
9015
9054
  DAY_MS = 24 * 60 * 60 * 1e3;
9016
9055
  isCount = (v) => Number.isSafeInteger(v) && v >= 0;
@@ -10849,6 +10888,10 @@ var init_reserved_env = __esm({
10849
10888
  "PI_RUNNER_OBSERVABLE",
10850
10889
  // Issue #693: derived from the resolved job memory, never chosen by a trigger or forwarded host value.
10851
10890
  "PI_CODEMODE_MAX_WORKERS",
10891
+ // Issue #865: the job's size as the job reads it (job-size.mjs `jobSizeEnv`). A trigger binding one through
10892
+ // run.secrets, or a forwarded host value, would tell the job's tools a size the container was not given.
10893
+ "PI_JOB_SIZE_CPUS",
10894
+ "PI_JOB_SIZE_MEMORY_MIB",
10852
10895
  "PI_PROVIDER",
10853
10896
  "PI_MODEL",
10854
10897
  "PI_FORK_ENTRY",
@@ -13192,39 +13235,25 @@ function findDuplicateKey(text4) {
13192
13235
  };
13193
13236
  const skipString = () => {
13194
13237
  i2++;
13238
+ let escaped = false;
13195
13239
  while (i2 < n3) {
13196
- const ch = s2[i2];
13197
- if (ch === '"') {
13198
- i2++;
13199
- return;
13200
- }
13201
- i2 += ch === "\\" ? 2 : 1;
13202
- }
13203
- };
13204
- const readString = () => {
13205
- i2++;
13206
- let out = "";
13207
- while (i2 < n3) {
13208
- const ch = s2[i2];
13209
- if (ch === '"') {
13240
+ const c = s2.charCodeAt(i2);
13241
+ if (c === 34) {
13210
13242
  i2++;
13211
- return out;
13243
+ return escaped;
13212
13244
  }
13213
- if (ch === "\\") {
13214
- const esc = s2[i2 + 1];
13245
+ if (c === 92) {
13246
+ escaped = true;
13215
13247
  i2 += 2;
13216
- if (esc === "u") {
13217
- out += String.fromCharCode(Number.parseInt(s2.slice(i2, i2 + 4), 16));
13218
- i2 += 4;
13219
- } else {
13220
- out += SIMPLE_ESCAPES[esc] ?? esc;
13221
- }
13222
- continue;
13248
+ } else {
13249
+ i2++;
13223
13250
  }
13224
- out += ch;
13225
- i2++;
13226
13251
  }
13227
- return out;
13252
+ return escaped;
13253
+ };
13254
+ const readKey = () => {
13255
+ const start = i2;
13256
+ return skipString() ? JSON.parse(s2.slice(start, i2)) : s2.slice(start + 1, i2 - 1);
13228
13257
  };
13229
13258
  let expectKey = false;
13230
13259
  while (i2 < n3) {
@@ -13273,8 +13302,11 @@ function findDuplicateKey(text4) {
13273
13302
  if (ch === '"') {
13274
13303
  const top = stack[stack.length - 1];
13275
13304
  if (expectKey && top && top.keys !== null) {
13276
- const key2 = readString();
13277
- if (top.keys.has(key2)) return { key: key2, at: [...path6, key2].join(".") };
13305
+ const key2 = readKey();
13306
+ if (top.keys.has(key2)) {
13307
+ const own2 = JSON.parse(JSON.stringify(key2));
13308
+ return { key: own2, at: [...path6, own2].join(".") };
13309
+ }
13278
13310
  top.keys.add(key2);
13279
13311
  path6.push(key2);
13280
13312
  top.named = true;
@@ -13288,10 +13320,9 @@ function findDuplicateKey(text4) {
13288
13320
  }
13289
13321
  return null;
13290
13322
  }
13291
- var SIMPLE_ESCAPES, STRUCTURAL;
13323
+ var STRUCTURAL;
13292
13324
  var init_json_duplicates = __esm({
13293
13325
  "../worker/src/json-duplicates.mjs"() {
13294
- SIMPLE_ESCAPES = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: " " };
13295
13326
  STRUCTURAL = /* @__PURE__ */ new Set([",", ":", "{", "}", "[", "]", '"', " ", " ", "\n", "\r"]);
13296
13327
  }
13297
13328
  });
@@ -39842,7 +39873,7 @@ function setMember(members, key2, value) {
39842
39873
  members[key2] = value;
39843
39874
  }
39844
39875
  }
39845
- var MAX_REPORTED_DEPTH, ROOT_SLOT, CREDIT_PER_APPEND, CREDIT_PER_CHARACTER, MAX_PENDING_SCALAR_LENGTH, NUMBER, NUMBER_WITHOUT_EXPONENT_DIGITS, LITERALS, QUOTE, BACKSLASH, StreamingJsonParser, ToolCallArgumentsStream, SIMPLE_ESCAPES2;
39876
+ var MAX_REPORTED_DEPTH, ROOT_SLOT, CREDIT_PER_APPEND, CREDIT_PER_CHARACTER, MAX_PENDING_SCALAR_LENGTH, NUMBER, NUMBER_WITHOUT_EXPONENT_DIGITS, LITERALS, QUOTE, BACKSLASH, StreamingJsonParser, ToolCallArgumentsStream, SIMPLE_ESCAPES;
39846
39877
  var init_streaming_json = __esm({
39847
39878
  "../node_modules/@earendil-works/pi-ai/dist/utils/streaming-json.js"() {
39848
39879
  init_json_parse();
@@ -40035,7 +40066,7 @@ var init_streaming_json = __esm({
40035
40066
  /** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */
40036
40067
  #continueEscape(open2, char) {
40037
40068
  if (open2.escape === "\\") {
40038
- const decoded2 = SIMPLE_ESCAPES2[char];
40069
+ const decoded2 = SIMPLE_ESCAPES[char];
40039
40070
  if (decoded2 !== void 0) {
40040
40071
  open2.escape = "";
40041
40072
  this.#appendString(open2, decoded2);
@@ -40239,7 +40270,7 @@ var init_streaming_json = __esm({
40239
40270
  return this.#arguments;
40240
40271
  }
40241
40272
  };
40242
- SIMPLE_ESCAPES2 = {
40273
+ SIMPLE_ESCAPES = {
40243
40274
  '"': '"',
40244
40275
  "\\": "\\",
40245
40276
  "/": "/",
@@ -114938,31 +114969,44 @@ function ranBefore(job) {
114938
114969
  return made === 1 ? "it made 1 attempt before and was waiting to retry; whatever that attempt recorded stays, and cancel writes none" : `it made ${made} attempts before and was waiting to retry; whatever those attempts recorded stays, and cancel writes none`;
114939
114970
  }
114940
114971
  async function holdInPlace(redis, jobId) {
114941
- if (typeof redis?.hexists !== "function") return false;
114972
+ if (typeof redis?.hexists !== "function") return null;
114942
114973
  try {
114943
114974
  return await redis.hexists(jobKey(jobId), "since") === 1;
114944
114975
  } catch {
114945
- return false;
114976
+ return null;
114946
114977
  }
114947
114978
  }
114979
+ function neverSent(err) {
114980
+ return err?.code === "ETIMEDOUT" && err?.message === "Command timed out" && err?.valkeyMayStillRun !== true;
114981
+ }
114982
+ function noAnswer(err) {
114983
+ return err?.code === "ETIMEDOUT" || err?.valkeyMayStillRun === true;
114984
+ }
114948
114985
  async function removeHeldJob({ redis, queue, jobId, timeoutMs = 2e3, dropDefer = false, touchStore = void 0, touchOutbox = null, now = Date.now, interventionOrigin = "human" }) {
114949
114986
  let cleared = false;
114987
+ let step = null;
114950
114988
  try {
114951
114989
  const hash2 = await redis.hgetall(jobKey(jobId));
114952
114990
  if (!hash2 || !hash2.since) return { invalid: `job ${jobId} is not waiting on a condition` };
114953
114991
  const job = await queue.getJob(jobId);
114954
114992
  if (!job) return { invalid: `job ${jobId} is no longer in the queue` };
114955
- const state5 = await job.getState().catch(() => null);
114993
+ let unread2 = null;
114994
+ const state5 = await job.getState().catch((err) => (unread2 = err, null));
114995
+ if (unread2 !== null) return { invalid: `the state of job ${jobId} could not be read, so nothing was changed: ${unread2?.message ?? String(unread2)}`, ...noAnswerOf(unread2) };
114956
114996
  if (state5 !== "delayed" && state5 !== "waiting" && state5 !== "prioritized") {
114957
114997
  return { invalid: `job ${jobId} is ${state5 ?? "in an unknown state"}, not waiting -- it has already left the hold` };
114958
114998
  }
114959
114999
  cleared = true;
115000
+ step = "hold";
114960
115001
  try {
114961
115002
  await redis.del(jobKey(jobId));
114962
115003
  } catch (err) {
114963
- if (await holdInPlace(redis, jobId)) cleared = false;
115004
+ const inPlace = await holdInPlace(redis, jobId);
115005
+ if (inPlace === true || neverSent(err)) cleared = false;
115006
+ else if (inPlace === false) step = null;
114964
115007
  throw err;
114965
115008
  }
115009
+ step = null;
114966
115010
  await redis.srem(HELD_SET, jobId).catch(() => {
114967
115011
  });
114968
115012
  if (hash2.dedupId) {
@@ -114970,24 +115014,33 @@ async function removeHeldJob({ redis, queue, jobId, timeoutMs = 2e3, dropDefer =
114970
115014
  if (holder === jobId) await redis.del(leaseKey(hash2.dedupId));
114971
115015
  }
114972
115016
  const store = touchStore === void 0 ? typeof redis?.eval === "function" ? makeTouchStore({ redis, now }) : null : touchStore;
114973
- const removal2 = interventionOrigin === "agent" ? (await job.remove(), { ok: true }) : await trackTouchAction({ store, outbox: touchOutbox, event: stopTouch(job, queue, jobId, now), act: async () => {
115017
+ const remove2 = async () => {
115018
+ step = "job";
114974
115019
  await job.remove();
114975
115020
  return { ok: true };
114976
- }, succeeded: () => !(job.attemptsMade > 0) && isWorkload(job) });
115021
+ };
115022
+ const removal2 = interventionOrigin === "agent" ? await remove2() : await trackTouchAction({ store, outbox: touchOutbox, event: stopTouch(job, queue, jobId, now), act: remove2, succeeded: () => !(job.attemptsMade > 0) && isWorkload(job) });
115023
+ step = null;
114977
115024
  await Promise.all([dropDefer ? dropDeferRecord(redis, jobId, timeoutMs) : null, dropAttemptOf(redis, queue, jobId, timeoutMs)]);
114978
115025
  return { ok: true, jobId, attemptsMade: Number.isInteger(job.attemptsMade) && job.attemptsMade > 0 ? job.attemptsMade : 0, ...removal2.touchWarning ? { touchWarning: removal2.touchWarning } : {} };
114979
115026
  } catch (err) {
114980
115027
  const invalid = err?.message ?? String(err);
114981
- return cleared ? { invalid, cleared: true } : { invalid };
115028
+ if (!cleared) return { invalid, ...noAnswerOf(err) };
115029
+ return { invalid, cleared: true, ...err?.valkeyMayStillRun === true && step !== null ? { unanswered: step } : {} };
114982
115030
  }
114983
115031
  }
114984
115032
  async function removeQueuedJob({ redis, queue, job, jobId, state: state5 = null, timeoutMs = 2e3, guard = null, touchStore = void 0, touchOutbox = null, now = Date.now, interventionOrigin = "human" }) {
114985
- const hash2 = await Promise.resolve().then(() => redis.hgetall(jobKey(jobId))).catch(() => null);
115033
+ let holdUnread = null;
115034
+ const hash2 = typeof redis?.hgetall !== "function" ? null : await Promise.resolve().then(() => redis.hgetall(jobKey(jobId))).catch((err) => (holdUnread = err, null));
115035
+ if (holdUnread !== null) {
115036
+ const words = holdUnread?.message ?? String(holdUnread);
115037
+ return { invalid: `could not read whether job ${jobId} is held on a wait condition, so nothing was changed: ${words}`, holdUnread: words, ...noAnswerOf(holdUnread) };
115038
+ }
114986
115039
  if (hash2?.since) {
114987
115040
  const stop3 = await guarded(guard, job);
114988
115041
  if (stop3 !== null) return stop3;
114989
115042
  const res = await removeHeldJob({ redis, queue, jobId, timeoutMs, dropDefer: true, touchStore, touchOutbox, now, interventionOrigin });
114990
- if (!res.ok) return res.cleared === true ? { invalid: res.invalid, cleared: true } : { invalid: res.invalid };
115043
+ if (!res.ok) return res.cleared === true ? { invalid: res.invalid, cleared: true, ...res.unanswered ? { unanswered: res.unanswered } : {} } : { invalid: res.invalid, ...res.noAnswer !== void 0 ? { noAnswer: res.noAnswer } : {} };
114991
115044
  return { ok: true, held: true, jobId: res.jobId, attemptsMade: res.attemptsMade, ...res.touchWarning ? { touchWarning: res.touchWarning } : {} };
114992
115045
  }
114993
115046
  if (!job) return { notQueued: true, state: "unknown" };
@@ -115027,7 +115080,7 @@ async function dropAttemptOf(redis, queue, jobId, timeoutMs) {
115027
115080
  function dropDeferRecord(redis, jobId, timeoutMs) {
115028
115081
  return dropKey(redis, deferKey(jobId), timeoutMs);
115029
115082
  }
115030
- var isWorkload, CANCEL_REQ_TTL_MS, CANCEL_ACK_TTL_MS, cancelReqKey, cancelAckKey, agentCancelReqKey, agentCancelAckKey, cancelRequestKeyOf, QUEUED_STATES2;
115083
+ var isWorkload, CANCEL_REQ_TTL_MS, CANCEL_ACK_TTL_MS, cancelReqKey, cancelAckKey, agentCancelReqKey, agentCancelAckKey, cancelRequestKeyOf, noAnswerOf, QUEUED_STATES2;
115031
115084
  var init_cancel_state = __esm({
115032
115085
  "../worker/src/cancel-state.mjs"() {
115033
115086
  init_touch_state();
@@ -115044,6 +115097,7 @@ var init_cancel_state = __esm({
115044
115097
  agentCancelReqKey = (jobId) => `cancel:agent:req:${jobId}`;
115045
115098
  agentCancelAckKey = (jobId) => `cancel:agent:ack:${jobId}`;
115046
115099
  cancelRequestKeyOf = (jobId, interventionOrigin) => interventionOrigin === "agent" ? agentCancelReqKey(jobId) : cancelReqKey(jobId);
115100
+ noAnswerOf = (err) => noAnswer(err) ? { noAnswer: err?.message ?? String(err) } : {};
115047
115101
  QUEUED_STATES2 = Object.freeze(["delayed", "waiting", "prioritized", "paused"]);
115048
115102
  }
115049
115103
  });
@@ -117405,10 +117459,12 @@ function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), pla
117405
117459
  const envPath = join8(cwd, ".env");
117406
117460
  let content = null;
117407
117461
  let error = null;
117462
+ let absent = false;
117408
117463
  try {
117409
117464
  content = readEnv3(envPath);
117410
117465
  } catch (err) {
117411
- if (err?.code !== "ENOENT") error = `${envPath} could not be read (${err?.code ?? err?.message})`;
117466
+ if (err?.code === "ENOENT") absent = true;
117467
+ else error = `${envPath} could not be read (${err?.code ?? err?.message})`;
117412
117468
  }
117413
117469
  const fileKeys = {};
117414
117470
  if (content !== null) {
@@ -117422,9 +117478,9 @@ function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), pla
117422
117478
  if (keys3.error) error ??= keys3.error;
117423
117479
  else for (const k of [VALKEY_SHARED_KEY, VALKEY_PASSWORD_KEY, "VALKEY_URL"]) if (typeof keys3.keys[k] === "string") fileKeys[k] = keys3.keys[k];
117424
117480
  }
117425
- return valkeyContextFromKeys({ env: env2, envPath, fileKeys, error, platform, rootRefused });
117481
+ return valkeyContextFromKeys({ env: env2, envPath, fileKeys, error, absent, platform, rootRefused });
117426
117482
  }
117427
- function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKeys = {}, error = null, platform = process.platform, rootRefused } = {}) {
117483
+ function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKeys = {}, error = null, absent = false, platform = process.platform, rootRefused } = {}) {
117428
117484
  const fileVenue = typeof fileKeys.PI_BACKENDS === "string" ? { PI_BACKENDS: fileKeys.PI_BACKENDS } : {};
117429
117485
  const shared = valkeySharedOn(fileKeys[VALKEY_SHARED_KEY]);
117430
117486
  const filePassword = fileKeys[VALKEY_PASSWORD_KEY] || null;
@@ -117438,7 +117494,7 @@ function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKe
117438
117494
  const engine = typeof env2.PI_OPENSHELL_ENGINE === "string" ? env2.PI_OPENSHELL_ENGINE : fileKeys.PI_OPENSHELL_ENGINE;
117439
117495
  const derived = platform === "linux" && (venues.podmanUsed && !venues.localUsed || openshellSetupOf(venues, { PI_OPENSHELL_ENGINE: engine }).valkeyEngine === "podman");
117440
117496
  const envUrl = typeof env2.VALKEY_URL === "string" ? env2.VALKEY_URL : null;
117441
- return { envPath, shared, rootRefused: typeof rootRefused === "boolean" ? rootRefused : derived, error, password: { environment: envPassword, file: filePassword }, url: { environment: envUrl, file: fileUrl } };
117497
+ return { envPath, shared, rootRefused: typeof rootRefused === "boolean" ? rootRefused : derived, error, absent: absent === true, password: { environment: envPassword, file: filePassword }, url: { environment: envUrl, file: fileUrl } };
117442
117498
  }
117443
117499
  function valkeyUrlProblem(url) {
117444
117500
  let u;
@@ -117577,6 +117633,11 @@ function scrubValkeyError(err) {
117577
117633
  }
117578
117634
  return err;
117579
117635
  }
117636
+ function refusedDatabaseOf(client) {
117637
+ if (!client?.[DB_REFUSED]) return null;
117638
+ const db = client.condition?.select ?? client.options?.db;
117639
+ return { db: Number.isSafeInteger(db) ? db : null };
117640
+ }
117580
117641
  function isSelectRangeError(err) {
117581
117642
  return Boolean(err) && typeof err === "object" && String(err.command?.name ?? "").toLowerCase() === "select" && /DB index is out of range|invalid DB index/i.test(String(err.message ?? ""));
117582
117643
  }
@@ -117600,10 +117661,12 @@ function onValkeyError(emitter, what2, write = (line2) => process.stderr.write(l
117600
117661
  function givesUp(connection) {
117601
117662
  return connection?.[JUDGE]?.failFast === true;
117602
117663
  }
117603
- function parseConnection(url, { failFast = false, servername = null, context = null, judge: judge2 = null, withoutPassword = false, freshContext = false } = {}) {
117664
+ function parseConnection(url, { failFast = false, servername = null, context = null, judge: judge2 = null, withoutPassword = false, freshContext = false, commandTimeoutMs = null } = {}) {
117604
117665
  const u = new URL(url);
117605
117666
  const dbProblem = valkeyUrlProblem(url);
117606
117667
  if (dbProblem) throw valkeyRefusal(dbProblem);
117668
+ const bounded12 = Number.isSafeInteger(commandTimeoutMs) && commandTimeoutMs > 0;
117669
+ if (bounded12 && !failFast) throw new TypeError("parseConnection: a command bound (commandTimeoutMs) is for a failFast client only, which has no offline queue to send a command that timed out from later");
117607
117670
  const where = context ?? defaultValkeyContext();
117608
117671
  const { password } = valkeyPasswordFor(url, where, { withoutPassword });
117609
117672
  return {
@@ -117612,7 +117675,9 @@ function parseConnection(url, { failFast = false, servername = null, context = n
117612
117675
  // reports. `context` is where the client stands (`valkeyClientContext`), the process's own when not given.
117613
117676
  Connector: JudgedConnector,
117614
117677
  // `judge` is a test seam only: the function asked for the address (judgedEndpoint when not given).
117615
- [JUDGE]: { url, context: where, ...judge2 ? { judge: judge2 } : {}, ...freshContext && !context ? { freshContext: true } : {}, ...failFast ? { failFast: true } : {} },
117678
+ // `freshPassword` (issue #845): a client that judges by the context as it is at each connect sends the password that
117679
+ // context gives then, unless it was built to send none (`withoutPassword`, a probe of what another account sees).
117680
+ [JUDGE]: { url, context: where, ...judge2 ? { judge: judge2 } : {}, ...freshContext && !context ? { freshContext: true, ...withoutPassword ? {} : { freshPassword: true } } : {}, ...failFast ? { failFast: true } : {}, ...bounded12 ? { commandBound: true } : {} },
117616
117681
  // WHATWG URL keeps an IPv6 literal's brackets in `hostname` (`[::1]`), and net.connect then looks up the name
117617
117682
  // "[::1]", which never resolves, so a `redis://[::1]:6379` VALKEY_URL never connected (issue #464, measured on
117618
117683
  // Fedora 44: ETIMEDOUT). The address is the part inside them.
@@ -117634,7 +117699,10 @@ function parseConnection(url, { failFast = false, servername = null, context = n
117634
117699
  // don't buffer commands while disconnected -- error now
117635
117700
  retryStrategy: (attempts) => attempts > 2 ? null : 200
117636
117701
  // give up after ~2 tries
117637
- } : {}
117702
+ } : {},
117703
+ // ioredis' resend of what was on a connection that died is off with the bound: the caller was told such a command
117704
+ // failed, and a resend on the next connection would make it run after that.
117705
+ ...bounded12 ? { commandTimeout: commandTimeoutMs, autoResendUnfulfilledCommands: false } : {}
117638
117706
  };
117639
117707
  }
117640
117708
  function makeRedisClient(url, { servername = null, context = null, failFast = false, lazyConnect = false, judge: judge2 = null, withoutPassword = false, disconnectTimeoutMs = null, noOfflineQueue = false, commandTimeoutMs = null, freshContext = false } = {}) {
@@ -117642,6 +117710,30 @@ function makeRedisClient(url, { servername = null, context = null, failFast = fa
117642
117710
  const options = { ...parseConnection(url, { failFast, servername, context, judge: judge2, withoutPassword, freshContext }), ...noOfflineQueue ? { enableOfflineQueue: false, maxRetriesPerRequest: 1 } : {}, ...lazyConnect ? { lazyConnect: true } : {}, ...Number.isSafeInteger(disconnectTimeoutMs) && disconnectTimeoutMs >= 0 ? { disconnectTimeout: disconnectTimeoutMs } : {}, ...bounded12 ? { commandTimeout: commandTimeoutMs, autoResendUnfulfilledCommands: false } : {} };
117643
117711
  return bounded12 ? new BoundedRedis(options) : new Redis(options);
117644
117712
  }
117713
+ function sameValkeyServer(a, b) {
117714
+ const x2 = valkeyTarget(a);
117715
+ const y = valkeyTarget(b);
117716
+ return !x2.error && !y.error && x2.host === y.host && x2.port === y.port;
117717
+ }
117718
+ function freshPasswordVerdict(judge2, context, held) {
117719
+ if (!context || context.error || context.absent === true) return { take: false, declined: null };
117720
+ if ((context.envPath ?? null) !== (judge2?.context?.envPath ?? null)) return { take: false, declined: null };
117721
+ const { password, from } = valkeyPasswordFor(judge2.url, context);
117722
+ const fromFile2 = from !== "VALKEY_URL" && from !== "the environment";
117723
+ if (fromFile2 && !sameValkeyServer(context.url?.file ?? DEFAULT_VALKEY_URL, judge2.url)) return { take: false, declined: (password ?? null) !== (held ?? null) };
117724
+ return { take: true, password: password ?? null };
117725
+ }
117726
+ function passwordDeclinedOf(client) {
117727
+ const connector = client?.connector;
117728
+ if (connector?.[PASSWORD_DECLINED] !== true) return false;
117729
+ const judge2 = connector.options?.[JUDGE];
117730
+ if (judge2?.freshPassword !== true) return false;
117731
+ try {
117732
+ return freshPasswordVerdict(judge2, defaultValkeyContext(), connector.options.password).declined === true;
117733
+ } catch {
117734
+ return false;
117735
+ }
117736
+ }
117645
117737
  function isJudgedConnection(connection) {
117646
117738
  const options = connection?.options ?? connection;
117647
117739
  return options?.Connector === JudgedConnector && Boolean(options?.[JUDGE]);
@@ -117672,7 +117764,7 @@ async function claimValkeyOwner(url, folder2, { context = null, waitMs = 15e3, m
117672
117764
  await sleep5(500);
117673
117765
  }
117674
117766
  }
117675
- var SCRUBS, DB_REFUSED, SELECT_HOOK, VALKEY_ERROR_QUIET_MS, VALKEY_ERRORS_HELD, saidAt, BoundedRedis, JUDGE, JudgedConnector;
117767
+ var SCRUBS, DB_REFUSED, SELECT_HOOK, VALKEY_ERROR_QUIET_MS, VALKEY_ERRORS_HELD, saidAt, COMMAND_BOUND_HOOK, BoundedRedis, JUDGE, FRESH_PASSWORD, PASSWORD_DECLINED, JudgedConnector, FRESH_PASSWORD_HOOK;
117676
117768
  var init_connection = __esm({
117677
117769
  "../worker/src/connection.mjs"() {
117678
117770
  init_valkey_auth();
@@ -117724,6 +117816,23 @@ var init_connection = __esm({
117724
117816
  VALKEY_ERROR_QUIET_MS = 6e4;
117725
117817
  VALKEY_ERRORS_HELD = 256;
117726
117818
  saidAt = /* @__PURE__ */ new Map();
117819
+ COMMAND_BOUND_HOOK = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-command-bound");
117820
+ if (!Redis.prototype[COMMAND_BOUND_HOOK]) {
117821
+ const sendCommand = Redis.prototype.sendCommand;
117822
+ Redis.prototype.sendCommand = function sendCommandWithBound(command3, stream11) {
117823
+ const answer = sendCommand.call(this, command3, stream11);
117824
+ if (this.options?.[JUDGE]?.commandBound !== true) return answer;
117825
+ command3._commandTimeoutTimer?.unref?.();
117826
+ if (this.commandQueue?.peekBack?.()?.command !== command3) return answer;
117827
+ answer?.catch?.((err) => {
117828
+ if (!err || typeof err !== "object" || err.name === "ReplyError" || err.valkeyRefused) return;
117829
+ if (err.message === "Command timed out") err.code ??= "ETIMEDOUT";
117830
+ err.valkeyMayStillRun = true;
117831
+ });
117832
+ return answer;
117833
+ };
117834
+ Redis.prototype[COMMAND_BOUND_HOOK] = true;
117835
+ }
117727
117836
  BoundedRedis = class extends Redis {
117728
117837
  sendCommand(command3, stream11) {
117729
117838
  const answer = super.sendCommand(command3, stream11);
@@ -117744,6 +117853,8 @@ var init_connection = __esm({
117744
117853
  }
117745
117854
  };
117746
117855
  JUDGE = "piDispatchValkeyJudge";
117856
+ FRESH_PASSWORD = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-fresh-password");
117857
+ PASSWORD_DECLINED = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-password-declined");
117747
117858
  JudgedConnector = class _JudgedConnector extends AbstractConnector {
117748
117859
  constructor(options) {
117749
117860
  super(options.disconnectTimeout);
@@ -117753,6 +117864,55 @@ var init_connection = __esm({
117753
117864
  check() {
117754
117865
  return !this[DB_REFUSED];
117755
117866
  }
117867
+ /**
117868
+ * Issue #845: a `freshPassword` client's password, as `valkeyPasswordFor` reads it from the context as it is now, put
117869
+ * where ioredis reads it for the connect about to start (the hook below). Here and not in `connect()`: by the time
117870
+ * ioredis asks the connector for a stream it has already built that connect's AUTH from `options.password`. The options
117871
+ * are the client's own object (ioredis hands its `options` to the Connector it constructs), so this is what the client
117872
+ * sends. A password from VALKEY_URL's userinfo comes back as it is, so it stays fixed. The last password is kept by a
117873
+ * context that:
117874
+ * - throws, or names an `error` (a `.env` that cannot be read, or a line in it the service's loader reads
117875
+ * differently): it cannot say which password the file holds, and dropping the one that worked would refuse a client
117876
+ * that is fine;
117877
+ * - finds no file (`absent`): a `.env` gone, for good or for the moment an editor replaces it, is not a file that
117878
+ * sets no password, so it does not drop one either (the console says to put the file back before a restart);
117879
+ * - would answer from the `.env` while that file does not name the Valkey this client dials: a password the file
117880
+ * gives (or the lack of one) belongs to the Valkey the file's VALKEY_URL names, the default one when it names
117881
+ * none, and a held client never moves to another Valkey. Review of PR #878, measured with two Valkeys: one
117882
+ * deployment moving to another Valkey changes both lines together, a symlinked deployment folder repointed reads
117883
+ * another deployment's file through the same path, and a shell that exports VALKEY_URL keeps the client on its URL
117884
+ * whatever the file says; in each the new Valkey's password went to the old one, which refused it, and the pages
117885
+ * lost the old Valkey too. The check is against the URL the client dials, not the context's own choice of URL
117886
+ * (`valkeyUrlFor`, which prefers this shell's and so never moved), and by the server each names (`sameValkeyServer`:
117887
+ * `valkeyTarget`'s host and port, so a scheme alias, a database path or a trailing `/` is the same Valkey, while
117888
+ * `localhost` and `127.0.0.1` stay two names, as `valkeyEndpointTag` keeps them). A rotation keeps the file's URL,
117889
+ * so it is taken, and so is a password a bad line hid at start once the line is fixed, when the file names the
117890
+ * client's Valkey. A password declined here that is not the one the client holds is recorded (`PASSWORD_DECLINED`,
117891
+ * `passwordDeclinedOf`), so the console names VALKEY_PASSWORD as needing a restart rather than going on refused in
117892
+ * silence (a shell that exports `redis://localhost:6440` over a file's `redis://127.0.0.1:6440`, say);
117893
+ * - is read from another `.env` path than the one the client was built with: the pi extension's context follows the
117894
+ * deployment pointer, and that alone says the file is another deployment's.
117895
+ * A password from this process's environment, or from VALKEY_URL's own userinfo, is not the file's, and is taken as it
117896
+ * comes. A context that passes all of these and gives no password drops it, as a restart would.
117897
+ */
117898
+ [FRESH_PASSWORD]() {
117899
+ const judge2 = this.options?.[JUDGE];
117900
+ if (judge2?.freshPassword !== true) return;
117901
+ let context;
117902
+ try {
117903
+ context = defaultValkeyContext();
117904
+ } catch {
117905
+ return;
117906
+ }
117907
+ const verdict2 = freshPasswordVerdict(judge2, context, this.options.password);
117908
+ if (!verdict2.take) {
117909
+ if (verdict2.declined !== null) this[PASSWORD_DECLINED] = verdict2.declined;
117910
+ return;
117911
+ }
117912
+ this[PASSWORD_DECLINED] = false;
117913
+ if (verdict2.password) this.options.password = verdict2.password;
117914
+ else delete this.options.password;
117915
+ }
117756
117916
  connect() {
117757
117917
  const { options } = this;
117758
117918
  if (this[DB_REFUSED]) return Promise.reject(this[DB_REFUSED]);
@@ -117784,6 +117944,18 @@ var init_connection = __esm({
117784
117944
  }
117785
117945
  };
117786
117946
  JudgedConnector.dial = { net: netConnect2, tls: tlsConnect };
117947
+ FRESH_PASSWORD_HOOK = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-fresh-password-hook");
117948
+ if (!Redis.prototype[FRESH_PASSWORD_HOOK]) {
117949
+ const connect = Redis.prototype.connect;
117950
+ Redis.prototype.connect = function connectWithFreshPassword(...args) {
117951
+ try {
117952
+ this.connector?.[FRESH_PASSWORD]?.();
117953
+ } catch {
117954
+ }
117955
+ return connect.apply(this, args);
117956
+ };
117957
+ Redis.prototype[FRESH_PASSWORD_HOOK] = true;
117958
+ }
117787
117959
  }
117788
117960
  });
117789
117961
 
@@ -118877,6 +119049,10 @@ var init_redact_secrets = __esm({
118877
119049
  NON_SECRET_ENV_KEYS = /* @__PURE__ */ new Set([
118878
119050
  // Issue #693: a memory-derived concurrency count, never a credential; keep it in the structural venue env.
118879
119051
  "PI_CODEMODE_MAX_WORKERS",
119052
+ // Issue #865: the job's size, two integers, never a secret. Listed, or OpenShell would put each in the job's credential
119053
+ // provider and the agent would read a placeholder where a tool's worker count needs the number.
119054
+ "PI_JOB_SIZE_CPUS",
119055
+ "PI_JOB_SIZE_MEMORY_MIB",
118880
119056
  "PI_PROVIDER",
118881
119057
  "PI_MODEL",
118882
119058
  "PI_FORK_ENTRY",
@@ -121675,7 +121851,7 @@ function hostsAgree(rows3) {
121675
121851
  const [first, ...others] = rows3.map(loadsFingerprint);
121676
121852
  return others.every((other) => other === first);
121677
121853
  }
121678
- var HOST_LOADS_CAPS, HOST_LOADS_MAX_BYTES, FIT_ORDER, HOST_LOADS_PARTS, HOST_LOADS_MORE, HOST_LOADS_LISTS, LOADS_EXTENSION_KINDS, LOADS_TRANSPORTS, LOADS_FLOW_TIERS, LOADS_REPO_STATES, isObject4, LOADS_IGNORE_FILES;
121854
+ var HOST_LOADS_CAPS, HOST_LOADS_MAX_BYTES, FIT_ORDER, HOST_LOADS_PARTS, HOST_LOADS_MORE, HOST_LOADS_LISTS, LOADS_EXTENSION_KINDS, LOADS_TRANSPORTS, LOADS_FLOW_TIERS, LOADS_REPO_STATES, isObject4, FLOW_INPUT_FIELDS, FLOW_BASIS_HEX, FLOW_BASIS_RE, LOADS_IGNORE_FILES;
121679
121855
  var init_host_loads = __esm({
121680
121856
  "../worker/src/host-loads.mjs"() {
121681
121857
  init_doctor_results();
@@ -121695,6 +121871,9 @@ var init_host_loads = __esm({
121695
121871
  LOADS_FLOW_TIERS = Object.freeze(["repository", "trigger", "overlay", "package", "none", "unknown"]);
121696
121872
  LOADS_REPO_STATES = Object.freeze(["present", "absent", "unknown"]);
121697
121873
  isObject4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
121874
+ FLOW_INPUT_FIELDS = Object.freeze(["flow", "folder", "skillsDir", "packages", "excludeExtensions"]);
121875
+ FLOW_BASIS_HEX = 8;
121876
+ FLOW_BASIS_RE = new RegExp(`^[0-9a-f]{${FLOW_BASIS_HEX}}$`);
121698
121877
  LOADS_IGNORE_FILES = Object.freeze([".gitignore", ".ignore", ".fdignore"]);
121699
121878
  }
121700
121879
  });
@@ -121888,7 +122067,7 @@ var init_mcp_preflight = __esm({
121888
122067
  "mcp-list-invalid": "the list this job carries is not one the triggers file's loader writes (a service in this deployment may be at another version)",
121889
122068
  "mcp-config-invalid": "this deployment's list of MCP servers (mcp.json in the overlay) cannot be used",
121890
122069
  "mcp-server-unknown": "this deployment does not offer that server",
121891
- "mcp-secret-reserved": "that server would be handed a credential this deployment keeps for the job itself",
122070
+ "mcp-secret-reserved": "that server would be handed a variable this deployment keeps for the job itself",
121892
122071
  "mcp-secret-unmapped": "a credential that server needs is not passed by this trigger",
121893
122072
  "mcp-excluded": "this trigger's run.excludeTools removes every way the job could reach that server",
121894
122073
  "mcp-egress-unreachable": "the egress policy does not let a job reach that server",
@@ -122143,7 +122322,7 @@ var init_reason_catalog = __esm({
122143
122322
  ["mcp-config-invalid", "Refused: the list of MCP servers could not be used: the overlay `mcp.json` before the job started, or the list the worker wrote for the job inside its container. Not run.", "Fix the overlay `mcp.json` (`pi-dispatch doctor` checks it), and run a worker and a job image of the same release."],
122144
122323
  ["mcp-server-unknown", mcpRefusalComment("mcp-server-unknown", null), "Declare the server in the overlay `mcp.json`, or fix the name in `run.mcp`."],
122145
122324
  ["mcp-secret-unmapped", mcpRefusalComment("mcp-secret-unmapped", null), "Pass the credential the server needs in the trigger's `run.secrets`."],
122146
- ["mcp-secret-reserved", mcpRefusalComment("mcp-secret-reserved", null), "Give the server's credential a variable of its own."],
122325
+ ["mcp-secret-reserved", mcpRefusalComment("mcp-secret-reserved", null), "Give the server a variable of its own, not one the job keeps."],
122147
122326
  ["mcp-excluded", mcpRefusalComment("mcp-excluded", null), "Remove the server from `run.mcp`, or keep a tool that reaches it out of `run.excludeTools`."],
122148
122327
  ["mcp-egress-unreachable", mcpRefusalComment("mcp-egress-unreachable", null), "Add the server's host to the egress allowlist, or drop the server from `run.mcp`."],
122149
122328
  ["mcp-package-runner", mcpRefusalComment("mcp-package-runner", null), "Allow the package registry in the egress allowlist, or install the server in the job image."],
@@ -123831,11 +124010,11 @@ function readDeploymentEnv({ dir, fs: fs9, uid = process.geteuid?.(), names: nam
123831
124010
  }
123832
124011
  }
123833
124012
  function deploymentServiceEnv({ env: env2, dir, keys: keys3, platform = process.platform, fs: fs9, uid = process.geteuid?.() }) {
123834
- const { path: path6, bytes, unreadable: unreadable3, untrusted } = readDeploymentEnv({ dir, fs: fs9, uid });
124013
+ const { path: path6, bytes, absent, unreadable: unreadable3, untrusted } = readDeploymentEnv({ dir, fs: fs9, uid });
123835
124014
  const raw = untrusted ? null : bytes;
123836
124015
  const file = raw === null ? null : serviceEnvFileOf(raw, path6, serviceEnvLoader(platform));
123837
124016
  const shellRunsLine = file?.loader === "shell" && file.hazard !== null && file.hazard.shape !== "shell-crlf-comment" ? file.hazard.line : null;
123838
- return { ...resolveServiceEnv({ env: env2, file, keys: keys3 }), path: path6, unreadable: unreadable3, untrusted, hazard: file?.hazard ?? null, shellRunsLine };
124017
+ return { ...resolveServiceEnv({ env: env2, file, keys: keys3 }), path: path6, absent, unreadable: unreadable3, untrusted, hazard: file?.hazard ?? null, shellRunsLine };
123839
124018
  }
123840
124019
  var init_service_env = __esm({
123841
124020
  "../worker/src/service-env.mjs"() {
@@ -127157,6 +127336,7 @@ __export(read_model_exports, {
127157
127336
  liveAbsentCause: () => liveAbsentCause,
127158
127337
  liveJobsOfRun: () => liveJobsOfRun,
127159
127338
  loaderRefuses: () => loaderRefuses,
127339
+ loaderTriggers: () => loaderTriggers,
127160
127340
  makeLocalRunIndex: () => makeLocalRunIndex,
127161
127341
  mapSchedulers: () => mapSchedulers,
127162
127342
  mergedDollarProblem: () => mergedDollarProblem,
@@ -127303,7 +127483,7 @@ import { execFileSync as execFileSync3 } from "node:child_process";
127303
127483
  import { homedir as homedir3 } from "node:os";
127304
127484
  function resolvePaths(env2 = process.env) {
127305
127485
  return {
127306
- valkeyUrl: env2.VALKEY_URL ?? "redis://127.0.0.1:6379",
127486
+ valkeyUrl: env2.VALKEY_URL ?? DEFAULT_VALKEY_URL,
127307
127487
  logsDir: logsDirPath(env2),
127308
127488
  settingsFile: settingsFilePath(env2),
127309
127489
  // Cwd defaults match what `pi-dispatch init` scaffolds (and, since issue #80, what the receiver
@@ -127463,7 +127643,7 @@ function panelValkeyContext({ env: env2 = process.env, pointerDir = null, owned
127463
127643
  if (pointerDir === null) return valkeyContextFromKeys({ env: own2, platform });
127464
127644
  const res = deploymentServiceEnv({ env: {}, dir: pointerDir, keys: VALKEY_CONTEXT_KEYS, platform, fs: fs9, uid });
127465
127645
  const error = res.untrusted ? `${res.path} ${res.untrusted}` : res.unreadable ? `${res.path} could not be read (${res.unreadable})` : res.hazardSkipped.length > 0 ? `${res.path} line ${res.hazard.line} ${res.hazard.what}` : res.unread.length > 0 ? `${res.path} assigns ${res.unread.map((u) => `${u.key} (line ${u.line})`).join(", ")} in a form the service's loader may read differently` : null;
127466
- return valkeyContextFromKeys({ env: own2, envPath: res.path, fileKeys: res.fromFile, error, platform });
127646
+ return valkeyContextFromKeys({ env: own2, envPath: res.path, fileKeys: res.fromFile, error, absent: res.absent === true, platform });
127467
127647
  }
127468
127648
  function installPanelValkeyContext({ env: env2 = process.env } = {}) {
127469
127649
  useValkeyContext(() => {
@@ -128873,11 +129053,13 @@ function readTriggers({ triggersPath, fs: fs9 = nodeFs11 }) {
128873
129053
  return { triggers: triggers2, count: entries.length, ...bundles !== null && { secretBundles: bundles }, ...loaderRefuses(text4, triggersPath) && { doesNotLoad: true } };
128874
129054
  }
128875
129055
  function loaderRefuses(text4, path6) {
129056
+ return loaderTriggers(text4, path6) === null;
129057
+ }
129058
+ function loaderTriggers(text4, path6) {
128876
129059
  try {
128877
- parseTriggers(text4, path6);
128878
- return false;
129060
+ return parseTriggers(text4, path6);
128879
129061
  } catch {
128880
- return true;
129062
+ return null;
128881
129063
  }
128882
129064
  }
128883
129065
  function configLocationsOf({ paths = {}, deploymentDir = null, home = homedir3() } = {}) {
@@ -130617,6 +130799,21 @@ function createFleetReader(paths, {
130617
130799
  async sandboxEnd(request2) {
130618
130800
  return requestSandboxEndFn({ redis, ...request2, now: nowFn });
130619
130801
  },
130802
+ /**
130803
+ * Issue #845: the database this reader's held client was refused (`refusedDatabaseOf`: `{ db }`, or null when it holds
130804
+ * no refusal). The queues are rebuilt and try again; the held client never does, so its reads stay refused until the
130805
+ * process is restarted, and the console says that rather than only that Valkey could not be read. Reads no Valkey.
130806
+ */
130807
+ refusedDatabase() {
130808
+ return refusedDatabaseOf(redis);
130809
+ },
130810
+ /**
130811
+ * Issue #845, review of PR #878: whether this reader's held client last declined a VALKEY_PASSWORD the `.env` now
130812
+ * gives, because the file does not name the Valkey it dials as it dials it (`passwordDeclinedOf`). Reads no Valkey.
130813
+ */
130814
+ passwordDeclined() {
130815
+ return passwordDeclinedOf(redis);
130816
+ },
130620
130817
  async dispose() {
130621
130818
  disposed = true;
130622
130819
  for (const q of sandboxQueues.values()) {
@@ -150381,9 +150578,15 @@ function buildSandboxRunArgs({ venue = DEFAULT_BACKEND, image, name: name2, work
150381
150578
  TMOUT: idleSeconds > 0 ? String(idleSeconds) : void 0,
150382
150579
  // Beside `--user` only (issue #341). Not a credential either.
150383
150580
  HOME: user !== null ? home : void 0,
150581
+ // Issue #865: the size this shell is built at (`size`: the manifest's, the run's own or, for a prepared
150582
+ // sandbox, its trigger's), by the job env's own function (`jobSizeEnv`), so a flow command copied in here
150583
+ // (`jest --maxWorkers=$PI_JOB_SIZE_CPUS`) sizes its pool as a job of that size would instead of expanding to
150584
+ // nothing. A run retained before this release never had the variables; its shell gets them for its recorded
150585
+ // size all the same. Two integers, not a credential.
150586
+ ...jobSizeEnv(size),
150384
150587
  // Still NO CREDENTIALS, and that clause is untouched: a proxy URL is not a credential, and
150385
150588
  // buildContainerEnv is still not reused here. The env is two variables about the terminal, HOME
150386
- // beside `--user`, and, when a policy is armed, four about the network.
150589
+ // beside `--user`, two about the run's size and, when a policy is armed, four about the network.
150387
150590
  ...proxyEnv
150388
150591
  },
150389
150592
  // Ahead of the env and the mounts, and well ahead of the image, which the builder keeps as
@@ -153105,7 +153308,7 @@ function sizingLines(sizing, id7, iw, styler) {
153105
153308
  if (verdicts.length > 0) push(wrapColumns(verdicts.join("; "), sub2, styler), tone);
153106
153309
  if (typeof s2.call === "string") out.push(...callLines(cellOf(s2.call), iw, styler, indent.length));
153107
153310
  if (over) push(wrapColumns(`no live host admits it (${cellOf(s2.words?.refusal ?? "")}): a job of it would wait for a host that never comes`, sub2, styler), "error");
153108
- for (const fact of [s2.words?.memoryFact, s2.words?.cpuFact]) if (typeof fact === "string" && fact !== "") push(wrapColumns(cellOf(fact), sub2, styler), "dim");
153311
+ for (const fact of [s2.words?.memoryFact, s2.words?.cpuFact, s2.words?.memoryCeiling]) if (typeof fact === "string" && fact !== "") push(wrapColumns(cellOf(fact), sub2, styler), "dim");
153109
153312
  return out;
153110
153313
  }
153111
153314
  function projectRows(snapshot, info = null) {
@@ -161534,18 +161737,28 @@ function agreeRow2(row2, trigger2) {
161534
161737
  skills: row2.skills ?? [],
161535
161738
  mcp: (row2.mcp ?? []).filter((server) => named2.includes(server.name) && server.enabled === true),
161536
161739
  packages: packagesLoaded(row2, trigger2),
161537
- flows: (row2.flows ?? []).filter((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow)
161740
+ // Its answer for the file as it is, without the basis: two hosts that answer for it hold the same one, and a host on a release before
161741
+ // #869 holds none, which would otherwise be a difference no job meets.
161742
+ flows: [flowRowFor(row2, trigger2)?.row].filter(Boolean).map((found) => ({ trigger: found.trigger, flow: found.flow, tier: found.tier, package: found.package, repo: found.repo }))
161538
161743
  };
161539
161744
  }
161540
- function flowAnswer(row2, trigger2) {
161541
- const found = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow);
161542
- if (!found) return null;
161745
+ function flowRowFor(row2, trigger2) {
161746
+ if (typeof trigger2.flow !== "string") return null;
161747
+ const here = (row2.flows ?? []).filter((entry3) => entry3.trigger === trigger2.index);
161748
+ const found = here.find((entry3) => entry3.flow === trigger2.flow);
161749
+ if (!found) return here.some((entry3) => typeof entry3.basis === "string" && entry3.basis !== trigger2.flowBasis) ? { stale: true } : null;
161750
+ if (typeof found.basis === "string") return found.basis === trigger2.flowBasis ? { row: found } : { stale: true };
161543
161751
  if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
161544
161752
  if (found.tier === "package" && trigger2.packages === false) return null;
161753
+ return { row: found };
161754
+ }
161755
+ function flowAnswer(row2, trigger2) {
161756
+ const found = flowRowFor(row2, trigger2)?.row;
161757
+ if (!found) return null;
161545
161758
  return found.tier === "package" ? `from ${found.package ?? "a staged package"}` : FLOW_TIER_WORDS[found.tier] ?? null;
161546
161759
  }
161547
161760
  function lacksFlow(row2, trigger2) {
161548
- const found = trigger2.flow === null ? null : (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow);
161761
+ const found = flowRowFor(row2, trigger2)?.row;
161549
161762
  return found?.tier === "none" && found.repo === "absent";
161550
161763
  }
161551
161764
  function hostPills(rows3, trigger2, liveList, fileVersion) {
@@ -161638,8 +161851,13 @@ function flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }) {
161638
161851
  const choices = offered.filter((entry3) => !seen.has(entry3.name) && seen.add(entry3.name)).slice(0, FLOW_CHOICES_MAX);
161639
161852
  const groups = /* @__PURE__ */ new Map();
161640
161853
  const silent = [];
161854
+ const stale = [];
161641
161855
  for (const row2 of rows3) {
161642
161856
  if (trigger2.flow === null || row2.state !== "current") continue;
161857
+ if (flowRowFor(row2, trigger2)?.stale === true) {
161858
+ stale.push(row2.name);
161859
+ continue;
161860
+ }
161643
161861
  const word3 = flowAnswer(row2, trigger2);
161644
161862
  if (word3 === null) silent.push(row2.name);
161645
161863
  else groups.set(word3, [...groups.get(word3) ?? [], row2.name]);
@@ -161648,7 +161866,11 @@ function flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }) {
161648
161866
  const lacking = rows3.filter((row2) => row2.state === "current" && lacksFlow(row2, trigger2)).map((row2) => row2.name);
161649
161867
  const said = [
161650
161868
  ...[...groups].map(([word3, names2]) => `${listed(names2)} ${names2.length === 1 ? "finds" : "find"} ${trigger2.flow} ${word3}`),
161651
- silent.length > 0 ? `${listed(silent)} ${silent.length === 1 ? "has" : "have"} no answer for it yet` : null,
161869
+ // Issue #869: an answer judged before a change to what the flow is found by. One sentence for one host or many, so it reads alike,
161870
+ // and none promises a later answer: a spent one-shot, a file the loaders refuse or a host whose own triggers file differs gets none.
161871
+ stale.length > 0 ? `${listed(stale)} ${TRIGGER_GETS_WORDS.flowStale}` : null,
161872
+ // No "yet": a flow that fails the skill charset, or a spent one-shot, never gets one.
161873
+ silent.length > 0 ? `${listed(silent)} ${silent.length === 1 ? "has" : "have"} no answer for it` : null,
161652
161874
  unknown3.length > 0 ? `${listed(unknown3)} ${unknown3.length === 1 ? "has" : "have"} no answer to read` : null
161653
161875
  ].filter(Boolean);
161654
161876
  return {
@@ -161846,9 +162068,11 @@ function triggerGetsOf({ trigger: trigger2, records = [], historyUnread = false,
161846
162068
  function freshFlowWords(row2, trigger2) {
161847
162069
  if (trigger2.flow === null) return SANDBOX_FRESH_WORDS.noFlow;
161848
162070
  const who = `a job of trigger ${trigger2.index + 1} on this host`;
162071
+ const at2 = flowRowFor(row2, trigger2);
162072
+ if (at2?.stale === true) return `Whether ${who} would find ${trigger2.flow} is unknown: ${SANDBOX_FRESH_WORDS.flowStale}.`;
161849
162073
  const word3 = flowAnswer(row2, trigger2);
161850
162074
  if (word3 === null) return `Whether ${who} would find ${trigger2.flow} is unknown: its last doctor run has no answer for it.`;
161851
- const tier = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow)?.tier;
162075
+ const tier = at2.row.tier;
161852
162076
  if (tier === "none" || tier === "unknown") return `Whether ${who} would find ${trigger2.flow} is unknown: doctor found it ${word3}, and the repository, which is on no host, may hold it.`;
161853
162077
  if (tier === "repository") return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}.`;
161854
162078
  return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}, unless the repository holds it at its base commit, which comes first.`;
@@ -161880,7 +162104,7 @@ function freshMcpWords(row2, named2) {
161880
162104
  ].filter(Boolean);
161881
162105
  return `${said.join("; ")}.`;
161882
162106
  }
161883
- function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, mcp = [], withholdsPackages = false, excludes = [], image = null }) {
162107
+ function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, flowBasis = null, mcp = [], withholdsPackages = false, excludes = [], image = null }) {
161884
162108
  const rows3 = [[`On ${host}`, sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages, excludes, image, mcp })]];
161885
162109
  const row2 = hostLoadsRow(hostLoads, host);
161886
162110
  if (row2.state === "unknown") return rows3;
@@ -161891,7 +162115,7 @@ function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, mcp = []
161891
162115
  if (listedNames.length > 0) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [`${listed(listedNames)} unknown here: its last doctor run is old.`]]);
161892
162116
  return rows3;
161893
162117
  }
161894
- rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [freshFlowWords(row2, { index: triggerIndex, flow: named2, excludeExtensions: excludes, packages: withholdsPackages ? false : void 0 })]]);
162118
+ rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [freshFlowWords(row2, { index: triggerIndex, flow: named2, excludeExtensions: excludes, packages: withholdsPackages ? false : void 0, flowBasis })]]);
161895
162119
  const mcpWords = freshMcpWords(row2, mcp);
161896
162120
  if (mcpWords !== null) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [mcpWords]]);
161897
162121
  return rows3;
@@ -161948,6 +162172,7 @@ var init_trigger_gets_view = __esm({
161948
162172
  viewer: "An operator changes these on this page, with one preview and one Confirm.",
161949
162173
  noTool: "No tool reaches this action; dispatch_trigger_edit keeps today\u2019s scope.",
161950
162174
  flowPicker: "It leaves out what is in a skills folder, or in a repository not on this host, and any other name can be typed.",
162175
+ flowStale: "answered for another version of the triggers file, so it is unknown there",
161951
162176
  reloadDrops: "A Confirm elsewhere on this page reloads it, and drops any change not yet written in What its jobs get.",
161952
162177
  othersUnread: "This page read the triggers file, and its other reads failed: the run history, the hosts and the jobs in line are unknown.",
161953
162178
  pillOrder: "differs in the order or kind of what it loads",
@@ -161981,6 +162206,7 @@ var init_trigger_gets_view = __esm({
161981
162206
  alsoNaming = (list5, name2) => list5.filter((other) => other !== name2 && (overlayEntryNamed(other, name2) || overlayEntryNamed(name2, other)));
161982
162207
  SANDBOX_FRESH_WORDS = Object.freeze({
161983
162208
  noFlow: "This trigger names no flow.",
162209
+ flowStale: "its last doctor run answered for another version of the triggers file",
161984
162210
  flowLabel: "Flow",
161985
162211
  mcpLabel: "MCP servers"
161986
162212
  });
@@ -162026,7 +162252,7 @@ function sandboxNew(v) {
162026
162252
  const plan = data5?.plan;
162027
162253
  const source2 = data5?.fromRun ? html`<p class="sx-p">Preparing from <a class="link" href="${`/run?${new URLSearchParams({ id: data5.fromRun })}`}">${data5.fromRun}</a>. The ref starts at HEAD because the record holds no created pull request. ${data5.sourceRun?.historicalIndex !== null && data5.sourceRun?.historicalIndex !== void 0 ? `Its historical trigger row was ${data5.sourceRun.historicalIndex + 1}; that does not prove the current row's identity.` : "Its original trigger identity is unavailable."} Choose the current trigger explicitly.${String(data5.sourceRun?.backend).startsWith("openshell") ? " Its original OpenShell venue cannot prepare a sandbox; deliberately choose a compatible local or podman trigger." : ""}</p>` : null;
162028
162254
  const form = html`${source2}<form method="get" action="/runs/sandboxes" data-live-hold>${data5?.fromRun ? html`<input type="hidden" name="fromRun" value="${data5.fromRun}">` : null}<div class="sx-form">${fields3}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
162029
- const fresh = plan ? html`${facts(sandboxFreshRows({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, flow: data5?.flow ?? null, mcp: data5?.mcp ?? [], withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }).map(([label3, parts2]) => [label3, wordsHtml(parts2)]), { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
162255
+ const fresh = plan ? html`${facts(sandboxFreshRows({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, flow: data5?.flow ?? null, flowBasis: data5?.flowBasis ?? null, mcp: data5?.mcp ?? [], withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }).map(([label3, parts2]) => [label3, wordsHtml(parts2)]), { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
162030
162256
  const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts5(plan)}${fresh}<p class="sx-p">${plan.pin ? `Pinned for ${plan.pinDays} days` : `Kept for ${plan.retentionHours} hours`}, on ${plan.host}. The worker clones on that host with a credential for this repository, then drops it. No container starts, no model runs, no dollars are spent and no run slot is taken.</p>${acts2(v) ? actForm({ action: "sandbox-prepare", hidden: [["repo", s2.repo], ["index", s2.index, "int"], ["ref", s2.ref], ["host", s2.host], ["pin", Boolean(s2.pin), "json"]], label: "Preview preparation", kind: ["primary"], icon: "plus" }) : html`<p class="card__sub">This console is read-only. Prepare from the host's terminal.</p>`}${cmd(["pi-dispatch", "sandbox", "--new", s2.repo, "--ref", s2.ref, "--as", String(s2.index), ...s2.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s2.index + 1} is index ${s2.index}.</p>` : html`<p class="sx-p" role="status">${data5?.refusal ?? "Choose a repository, its trigger, a ref and a host, then review their settings."}</p>`;
162031
162257
  return card3("Without a run", html`${form}${preparation}<noscript><p class="card__sub">${SANDBOX_WORDS.noScript}</p></noscript>`, "a fresh clone with a trigger's image and a job's isolation");
162032
162258
  }
@@ -163516,7 +163742,7 @@ function projectSizing2(v) {
163516
163742
  const s2 = p.sizing;
163517
163743
  const dim = (label3, d, words) => html`<p class="proj-verdict"><b>${label3}: ${d?.suggested ? `suggest ${label3 === "Memory" ? gibText(d.suggested) : formatCpus(d.suggested) + " CPUs"}` : d?.reason === "not-enough-runs" ? "not enough runs" : d?.reason ?? "unknown"}.</b> ${(label3 === "Memory" ? memoryWords(words) : words) ?? "No suggestion could be read."}</p>`;
163518
163744
  const action2 = s2?.action;
163519
- return card6("Job size", html`${projectSizingChart(p)}${s2 ? html`${dim("Memory", s2.memory, s2.words?.memory)}${dim("CPUs", s2.cpu, s2.words?.cpu)}${[memoryWords(s2.words?.memoryHeld), s2.words?.memoryFact, s2.words?.cpuFact, memoryWords(s2.words?.refusal)].filter(Boolean).map(note2)}` : note2("No size suggestion could be read.")}${p.sizingUnreadable ? note2("The measurements or scoped limits could not be read completely.") : null}${note2("Measured inside the jobs, so advisory.")}${s2?.call && canAct(v) ? html`<p class="proj-sub">The exact call</p><div class="cmd"><code>${s2.call}</code><button class="btn btn--sm" type="button" data-copy hidden>Copy</button></div>${action2 ? note2(action2.row === null ? `Adds a scoped-limits row for project:${p.id}.` : `Applies to scoped-limits row ${action2.row}, project:${p.id}.`) : null}${canAct(v) && action2 ? actForm3(action2.action, { hidden: [...action2.index === void 0 ? [] : [["index", action2.index, "int"]], ["limit", JSON.stringify(action2.limit), "json"]], label: "Preview this size" }) : null}` : null}`, "peak memory per run, last 30 days");
163745
+ return card6("Job size", html`${projectSizingChart(p)}${s2 ? html`${dim("Memory", s2.memory, s2.words?.memory)}${dim("CPUs", s2.cpu, s2.words?.cpu)}${[memoryWords(s2.words?.memoryHeld), s2.words?.memoryFact, s2.words?.cpuFact, memoryWords(s2.words?.refusal), s2.words?.memoryCeiling].filter(Boolean).map(note2)}` : note2("No size suggestion could be read.")}${p.sizingUnreadable ? note2("The measurements or scoped limits could not be read completely.") : null}${note2("Measured inside the jobs, so advisory.")}${s2?.call && canAct(v) ? html`<p class="proj-sub">The exact call</p><div class="cmd"><code>${s2.call}</code><button class="btn btn--sm" type="button" data-copy hidden>Copy</button></div>${action2 ? note2(action2.row === null ? `Adds a scoped-limits row for project:${p.id}.` : `Applies to scoped-limits row ${action2.row}, project:${p.id}.`) : null}${canAct(v) && action2 ? actForm3(action2.action, { hidden: [...action2.index === void 0 ? [] : [["index", action2.index, "int"]], ["limit", JSON.stringify(action2.limit), "json"]], label: "Preview this size" }) : null}` : null}`, "peak memory per run, last 30 days");
163520
163746
  }
163521
163747
  function projectRecent(v) {
163522
163748
  if (!ready(v)) return null;
@@ -172285,6 +172511,14 @@ var init_watch = __esm({
172285
172511
  }
172286
172512
  });
172287
172513
 
172514
+ // src/console/restart-notice.mjs
172515
+ var init_restart_notice = __esm({
172516
+ "src/console/restart-notice.mjs"() {
172517
+ init_kit();
172518
+ init_html();
172519
+ }
172520
+ });
172521
+
172288
172522
  // src/console/session-ends.mjs
172289
172523
  function streamEndOf(v) {
172290
172524
  return typeof v === "string" && STREAM_ENDS.includes(v) ? v : null;
@@ -172351,6 +172585,7 @@ var init_shell = __esm({
172351
172585
  init_html();
172352
172586
  init_kit();
172353
172587
  init_pages();
172588
+ init_restart_notice();
172354
172589
  init_pulse_icons();
172355
172590
  init_signin();
172356
172591
  init_theme();
@@ -178182,8 +178417,8 @@ var init_setup_wizard = __esm({
178182
178417
  init_connection();
178183
178418
  init_service_env();
178184
178419
  init_index();
178185
- RUNTIME_VERSION = "5.2.0";
178186
- RECEIVER_VERSION = "5.2.0";
178420
+ RUNTIME_VERSION = "5.2.1";
178421
+ RECEIVER_VERSION = "5.2.1";
178187
178422
  RUNTIME_PKG = "@edgehero/pi-dispatch";
178188
178423
  RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
178189
178424
  NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
@@ -180063,7 +180298,7 @@ The project's runs could not be read (${sizing.unreachable}).`;
180063
180298
  if (dim.held === "largest") return `no larger size (${dim.reason}: ${words})`;
180064
180299
  return dim.reason === "not-enough-runs" ? notEnoughRunsText(dim, shown3 === formatCpus ? cpusText : shown3) : "fits";
180065
180300
  };
180066
- const facts7 = [s2.words.memoryFact, s2.words.cpuFact].filter(Boolean);
180301
+ const facts7 = [s2.words.memoryFact, s2.words.cpuFact, s2.words.memoryCeiling].filter(Boolean);
180067
180302
  return `
180068
180303
  Project ${id7}'s runs (the last ${SUGGEST_WINDOW_DAYS} days, measured inside the jobs, so advisory): size now ${formatMemory(m2.current)}, ${cpusText(c.current)}; ${peaks}. They suggest memory ${said(m2, s2.words.memory, formatMemory)}, CPUs ${said(c, s2.words.cpu, formatCpus)}.${facts7.length > 0 ? ` ${facts7.map((f3) => `${f3[0].toUpperCase()}${f3.slice(1)}.`).join(" ")}` : ""}`;
180069
180304
  }