@edgehero/pi-dispatch-admin 5.1.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
  });
@@ -15037,8 +15068,9 @@ function pauseWindowsVersionRefusal(doc, { supported = PAUSE_WINDOWS_VERSION } =
15037
15068
  function heldPauseWindows(text4, path6) {
15038
15069
  if ((text4 ?? "").trim() === "") return { doc: null, windows: [] };
15039
15070
  const { value, problem } = heldDocument(text4, "windows");
15040
- const refused3 = problem ?? (findDuplicateKey(text4) ? "has a key written twice" : null);
15041
- if (refused3) return { doc: value ?? null, problem: `the pause-windows file ${path6} ${refused3}, so its windows cannot be kept` };
15071
+ const doubled = value !== void 0 && findDuplicateKey(text4) !== null;
15072
+ const refused3 = doubled ? "has a key written twice" : problem ?? null;
15073
+ if (refused3) return { doc: doubled ? null : value ?? null, problem: `the pause-windows file ${path6} ${refused3}, so its windows cannot be kept` };
15042
15074
  return { doc: value, windows: Array.isArray(value.windows) ? value.windows : [] };
15043
15075
  }
15044
15076
  function pauseWindowsText(doc, windows, { table: table3 = PAUSE_WINDOW_FIELD_VERSIONS, valueTable = PAUSE_WINDOW_VALUE_VERSIONS } = {}) {
@@ -15089,6 +15121,10 @@ function parsePauseWindows(text4, path6, { supported = PAUSE_WINDOWS_VERSION, ta
15089
15121
  } catch (error) {
15090
15122
  throw configError4(`pause-windows file is not valid JSON: ${path6} (${error.message})`);
15091
15123
  }
15124
+ const duplicate = findDuplicateKey(text4);
15125
+ if (duplicate) {
15126
+ throw configError4(`pause-windows file has a duplicate key ${JSON.stringify(duplicate.key)} at ${duplicate.at}: JSON keeps the LAST value, so the file a person reads and the file that applies are not the same: ${path6}`);
15127
+ }
15092
15128
  const problem = pauseWindowsVersionRefusal(parsed, { supported });
15093
15129
  if (problem) {
15094
15130
  const error = configError4(`${problem.message}: ${path6}`);
@@ -39837,7 +39873,7 @@ function setMember(members, key2, value) {
39837
39873
  members[key2] = value;
39838
39874
  }
39839
39875
  }
39840
- 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;
39841
39877
  var init_streaming_json = __esm({
39842
39878
  "../node_modules/@earendil-works/pi-ai/dist/utils/streaming-json.js"() {
39843
39879
  init_json_parse();
@@ -40030,7 +40066,7 @@ var init_streaming_json = __esm({
40030
40066
  /** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */
40031
40067
  #continueEscape(open2, char) {
40032
40068
  if (open2.escape === "\\") {
40033
- const decoded2 = SIMPLE_ESCAPES2[char];
40069
+ const decoded2 = SIMPLE_ESCAPES[char];
40034
40070
  if (decoded2 !== void 0) {
40035
40071
  open2.escape = "";
40036
40072
  this.#appendString(open2, decoded2);
@@ -40234,7 +40270,7 @@ var init_streaming_json = __esm({
40234
40270
  return this.#arguments;
40235
40271
  }
40236
40272
  };
40237
- SIMPLE_ESCAPES2 = {
40273
+ SIMPLE_ESCAPES = {
40238
40274
  '"': '"',
40239
40275
  "\\": "\\",
40240
40276
  "/": "/",
@@ -114901,10 +114937,12 @@ function withdrawCancel({ redis, jobId, interventionOrigin = "human" }) {
114901
114937
  Promise.resolve().then(() => redis.del(cancelRequestKeyOf(jobId, interventionOrigin))).catch(() => {
114902
114938
  });
114903
114939
  }
114904
- async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep: sleep5 = (ms3) => new Promise((r2) => setTimeout(r2, ms3)) }) {
114940
+ async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep: sleep5 = (ms3) => new Promise((r2) => setTimeout(r2, ms3)), now = Date.now, onWritten = null }) {
114905
114941
  const requestKey = cancelRequestKeyOf(jobId, interventionOrigin);
114906
114942
  const ackKey = interventionOrigin === "agent" ? agentCancelAckKey(jobId) : cancelAckKey(jobId);
114907
114943
  await redis.set(requestKey, interventionOrigin === "agent" ? "agent-cancel" : "operator-cancel", "PX", CANCEL_REQ_TTL_MS);
114944
+ onWritten?.();
114945
+ const deadline = now() + ackTimeoutMs;
114908
114946
  for (let waited = 0; ; waited += pollMs) {
114909
114947
  let host;
114910
114948
  try {
@@ -114914,7 +114952,7 @@ async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTi
114914
114952
  throw err;
114915
114953
  }
114916
114954
  if (host !== null && host !== void 0) return { ack: host };
114917
- if (waited + pollMs > ackTimeoutMs) break;
114955
+ if (waited + pollMs > ackTimeoutMs || now() >= deadline) break;
114918
114956
  await sleep5(pollMs);
114919
114957
  }
114920
114958
  try {
@@ -114931,31 +114969,44 @@ function ranBefore(job) {
114931
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`;
114932
114970
  }
114933
114971
  async function holdInPlace(redis, jobId) {
114934
- if (typeof redis?.hexists !== "function") return false;
114972
+ if (typeof redis?.hexists !== "function") return null;
114935
114973
  try {
114936
114974
  return await redis.hexists(jobKey(jobId), "since") === 1;
114937
114975
  } catch {
114938
- return false;
114976
+ return null;
114939
114977
  }
114940
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
+ }
114941
114985
  async function removeHeldJob({ redis, queue, jobId, timeoutMs = 2e3, dropDefer = false, touchStore = void 0, touchOutbox = null, now = Date.now, interventionOrigin = "human" }) {
114942
114986
  let cleared = false;
114987
+ let step = null;
114943
114988
  try {
114944
114989
  const hash2 = await redis.hgetall(jobKey(jobId));
114945
114990
  if (!hash2 || !hash2.since) return { invalid: `job ${jobId} is not waiting on a condition` };
114946
114991
  const job = await queue.getJob(jobId);
114947
114992
  if (!job) return { invalid: `job ${jobId} is no longer in the queue` };
114948
- 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) };
114949
114996
  if (state5 !== "delayed" && state5 !== "waiting" && state5 !== "prioritized") {
114950
114997
  return { invalid: `job ${jobId} is ${state5 ?? "in an unknown state"}, not waiting -- it has already left the hold` };
114951
114998
  }
114952
114999
  cleared = true;
115000
+ step = "hold";
114953
115001
  try {
114954
115002
  await redis.del(jobKey(jobId));
114955
115003
  } catch (err) {
114956
- 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;
114957
115007
  throw err;
114958
115008
  }
115009
+ step = null;
114959
115010
  await redis.srem(HELD_SET, jobId).catch(() => {
114960
115011
  });
114961
115012
  if (hash2.dedupId) {
@@ -114963,24 +115014,33 @@ async function removeHeldJob({ redis, queue, jobId, timeoutMs = 2e3, dropDefer =
114963
115014
  if (holder === jobId) await redis.del(leaseKey(hash2.dedupId));
114964
115015
  }
114965
115016
  const store = touchStore === void 0 ? typeof redis?.eval === "function" ? makeTouchStore({ redis, now }) : null : touchStore;
114966
- 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";
114967
115019
  await job.remove();
114968
115020
  return { ok: true };
114969
- }, 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;
114970
115024
  await Promise.all([dropDefer ? dropDeferRecord(redis, jobId, timeoutMs) : null, dropAttemptOf(redis, queue, jobId, timeoutMs)]);
114971
115025
  return { ok: true, jobId, attemptsMade: Number.isInteger(job.attemptsMade) && job.attemptsMade > 0 ? job.attemptsMade : 0, ...removal2.touchWarning ? { touchWarning: removal2.touchWarning } : {} };
114972
115026
  } catch (err) {
114973
115027
  const invalid = err?.message ?? String(err);
114974
- 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 } : {} };
114975
115030
  }
114976
115031
  }
114977
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" }) {
114978
- 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
+ }
114979
115039
  if (hash2?.since) {
114980
115040
  const stop3 = await guarded(guard, job);
114981
115041
  if (stop3 !== null) return stop3;
114982
115042
  const res = await removeHeldJob({ redis, queue, jobId, timeoutMs, dropDefer: true, touchStore, touchOutbox, now, interventionOrigin });
114983
- 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 } : {} };
114984
115044
  return { ok: true, held: true, jobId: res.jobId, attemptsMade: res.attemptsMade, ...res.touchWarning ? { touchWarning: res.touchWarning } : {} };
114985
115045
  }
114986
115046
  if (!job) return { notQueued: true, state: "unknown" };
@@ -115020,7 +115080,7 @@ async function dropAttemptOf(redis, queue, jobId, timeoutMs) {
115020
115080
  function dropDeferRecord(redis, jobId, timeoutMs) {
115021
115081
  return dropKey(redis, deferKey(jobId), timeoutMs);
115022
115082
  }
115023
- 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;
115024
115084
  var init_cancel_state = __esm({
115025
115085
  "../worker/src/cancel-state.mjs"() {
115026
115086
  init_touch_state();
@@ -115037,6 +115097,7 @@ var init_cancel_state = __esm({
115037
115097
  agentCancelReqKey = (jobId) => `cancel:agent:req:${jobId}`;
115038
115098
  agentCancelAckKey = (jobId) => `cancel:agent:ack:${jobId}`;
115039
115099
  cancelRequestKeyOf = (jobId, interventionOrigin) => interventionOrigin === "agent" ? agentCancelReqKey(jobId) : cancelReqKey(jobId);
115100
+ noAnswerOf = (err) => noAnswer(err) ? { noAnswer: err?.message ?? String(err) } : {};
115040
115101
  QUEUED_STATES2 = Object.freeze(["delayed", "waiting", "prioritized", "paused"]);
115041
115102
  }
115042
115103
  });
@@ -117398,10 +117459,12 @@ function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), pla
117398
117459
  const envPath = join8(cwd, ".env");
117399
117460
  let content = null;
117400
117461
  let error = null;
117462
+ let absent = false;
117401
117463
  try {
117402
117464
  content = readEnv3(envPath);
117403
117465
  } catch (err) {
117404
- 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})`;
117405
117468
  }
117406
117469
  const fileKeys = {};
117407
117470
  if (content !== null) {
@@ -117415,9 +117478,9 @@ function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), pla
117415
117478
  if (keys3.error) error ??= keys3.error;
117416
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];
117417
117480
  }
117418
- return valkeyContextFromKeys({ env: env2, envPath, fileKeys, error, platform, rootRefused });
117481
+ return valkeyContextFromKeys({ env: env2, envPath, fileKeys, error, absent, platform, rootRefused });
117419
117482
  }
117420
- 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 } = {}) {
117421
117484
  const fileVenue = typeof fileKeys.PI_BACKENDS === "string" ? { PI_BACKENDS: fileKeys.PI_BACKENDS } : {};
117422
117485
  const shared = valkeySharedOn(fileKeys[VALKEY_SHARED_KEY]);
117423
117486
  const filePassword = fileKeys[VALKEY_PASSWORD_KEY] || null;
@@ -117431,7 +117494,7 @@ function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKe
117431
117494
  const engine = typeof env2.PI_OPENSHELL_ENGINE === "string" ? env2.PI_OPENSHELL_ENGINE : fileKeys.PI_OPENSHELL_ENGINE;
117432
117495
  const derived = platform === "linux" && (venues.podmanUsed && !venues.localUsed || openshellSetupOf(venues, { PI_OPENSHELL_ENGINE: engine }).valkeyEngine === "podman");
117433
117496
  const envUrl = typeof env2.VALKEY_URL === "string" ? env2.VALKEY_URL : null;
117434
- 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 } };
117435
117498
  }
117436
117499
  function valkeyUrlProblem(url) {
117437
117500
  let u;
@@ -117570,6 +117633,11 @@ function scrubValkeyError(err) {
117570
117633
  }
117571
117634
  return err;
117572
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
+ }
117573
117641
  function isSelectRangeError(err) {
117574
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 ?? ""));
117575
117643
  }
@@ -117593,10 +117661,12 @@ function onValkeyError(emitter, what2, write = (line2) => process.stderr.write(l
117593
117661
  function givesUp(connection) {
117594
117662
  return connection?.[JUDGE]?.failFast === true;
117595
117663
  }
117596
- 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 } = {}) {
117597
117665
  const u = new URL(url);
117598
117666
  const dbProblem = valkeyUrlProblem(url);
117599
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");
117600
117670
  const where = context ?? defaultValkeyContext();
117601
117671
  const { password } = valkeyPasswordFor(url, where, { withoutPassword });
117602
117672
  return {
@@ -117605,7 +117675,9 @@ function parseConnection(url, { failFast = false, servername = null, context = n
117605
117675
  // reports. `context` is where the client stands (`valkeyClientContext`), the process's own when not given.
117606
117676
  Connector: JudgedConnector,
117607
117677
  // `judge` is a test seam only: the function asked for the address (judgedEndpoint when not given).
117608
- [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 } : {} },
117609
117681
  // WHATWG URL keeps an IPv6 literal's brackets in `hostname` (`[::1]`), and net.connect then looks up the name
117610
117682
  // "[::1]", which never resolves, so a `redis://[::1]:6379` VALKEY_URL never connected (issue #464, measured on
117611
117683
  // Fedora 44: ETIMEDOUT). The address is the part inside them.
@@ -117627,7 +117699,10 @@ function parseConnection(url, { failFast = false, servername = null, context = n
117627
117699
  // don't buffer commands while disconnected -- error now
117628
117700
  retryStrategy: (attempts) => attempts > 2 ? null : 200
117629
117701
  // give up after ~2 tries
117630
- } : {}
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 } : {}
117631
117706
  };
117632
117707
  }
117633
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 } = {}) {
@@ -117635,6 +117710,30 @@ function makeRedisClient(url, { servername = null, context = null, failFast = fa
117635
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 } : {} };
117636
117711
  return bounded12 ? new BoundedRedis(options) : new Redis(options);
117637
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
+ }
117638
117737
  function isJudgedConnection(connection) {
117639
117738
  const options = connection?.options ?? connection;
117640
117739
  return options?.Connector === JudgedConnector && Boolean(options?.[JUDGE]);
@@ -117665,7 +117764,7 @@ async function claimValkeyOwner(url, folder2, { context = null, waitMs = 15e3, m
117665
117764
  await sleep5(500);
117666
117765
  }
117667
117766
  }
117668
- 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;
117669
117768
  var init_connection = __esm({
117670
117769
  "../worker/src/connection.mjs"() {
117671
117770
  init_valkey_auth();
@@ -117717,6 +117816,23 @@ var init_connection = __esm({
117717
117816
  VALKEY_ERROR_QUIET_MS = 6e4;
117718
117817
  VALKEY_ERRORS_HELD = 256;
117719
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
+ }
117720
117836
  BoundedRedis = class extends Redis {
117721
117837
  sendCommand(command3, stream11) {
117722
117838
  const answer = super.sendCommand(command3, stream11);
@@ -117737,6 +117853,8 @@ var init_connection = __esm({
117737
117853
  }
117738
117854
  };
117739
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");
117740
117858
  JudgedConnector = class _JudgedConnector extends AbstractConnector {
117741
117859
  constructor(options) {
117742
117860
  super(options.disconnectTimeout);
@@ -117746,6 +117864,55 @@ var init_connection = __esm({
117746
117864
  check() {
117747
117865
  return !this[DB_REFUSED];
117748
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
+ }
117749
117916
  connect() {
117750
117917
  const { options } = this;
117751
117918
  if (this[DB_REFUSED]) return Promise.reject(this[DB_REFUSED]);
@@ -117777,6 +117944,18 @@ var init_connection = __esm({
117777
117944
  }
117778
117945
  };
117779
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
+ }
117780
117959
  }
117781
117960
  });
117782
117961
 
@@ -118870,6 +119049,10 @@ var init_redact_secrets = __esm({
118870
119049
  NON_SECRET_ENV_KEYS = /* @__PURE__ */ new Set([
118871
119050
  // Issue #693: a memory-derived concurrency count, never a credential; keep it in the structural venue env.
118872
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",
118873
119056
  "PI_PROVIDER",
118874
119057
  "PI_MODEL",
118875
119058
  "PI_FORK_ENTRY",
@@ -121668,7 +121851,7 @@ function hostsAgree(rows3) {
121668
121851
  const [first, ...others] = rows3.map(loadsFingerprint);
121669
121852
  return others.every((other) => other === first);
121670
121853
  }
121671
- 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;
121672
121855
  var init_host_loads = __esm({
121673
121856
  "../worker/src/host-loads.mjs"() {
121674
121857
  init_doctor_results();
@@ -121688,6 +121871,9 @@ var init_host_loads = __esm({
121688
121871
  LOADS_FLOW_TIERS = Object.freeze(["repository", "trigger", "overlay", "package", "none", "unknown"]);
121689
121872
  LOADS_REPO_STATES = Object.freeze(["present", "absent", "unknown"]);
121690
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}}$`);
121691
121877
  LOADS_IGNORE_FILES = Object.freeze([".gitignore", ".ignore", ".fdignore"]);
121692
121878
  }
121693
121879
  });
@@ -121881,7 +122067,7 @@ var init_mcp_preflight = __esm({
121881
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)",
121882
122068
  "mcp-config-invalid": "this deployment's list of MCP servers (mcp.json in the overlay) cannot be used",
121883
122069
  "mcp-server-unknown": "this deployment does not offer that server",
121884
- "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",
121885
122071
  "mcp-secret-unmapped": "a credential that server needs is not passed by this trigger",
121886
122072
  "mcp-excluded": "this trigger's run.excludeTools removes every way the job could reach that server",
121887
122073
  "mcp-egress-unreachable": "the egress policy does not let a job reach that server",
@@ -122136,7 +122322,7 @@ var init_reason_catalog = __esm({
122136
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."],
122137
122323
  ["mcp-server-unknown", mcpRefusalComment("mcp-server-unknown", null), "Declare the server in the overlay `mcp.json`, or fix the name in `run.mcp`."],
122138
122324
  ["mcp-secret-unmapped", mcpRefusalComment("mcp-secret-unmapped", null), "Pass the credential the server needs in the trigger's `run.secrets`."],
122139
- ["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."],
122140
122326
  ["mcp-excluded", mcpRefusalComment("mcp-excluded", null), "Remove the server from `run.mcp`, or keep a tool that reaches it out of `run.excludeTools`."],
122141
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`."],
122142
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."],
@@ -122801,7 +122987,7 @@ function triggerLastRun(records, hostLoads = null) {
122801
122987
  ...sourcesOf(loaded, "package").filter((row2) => row2.toolNames?.length > 0).map((row2) => ({ kind: "package", from: row2.name ?? "a staged package", names: [...row2.toolNames] })),
122802
122988
  ...(overlay?.namedTools ?? []).map((row2) => ({ kind: "overlay", from: row2.name, names: [...row2.tools] }))
122803
122989
  ];
122804
- const sources = [...sourcesOf(loaded, "package"), ...(overlay?.names ?? []).map((name2) => ({ name: name2, version: null, tools: overlay.namedTools?.find((row2) => row2.name === name2)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
122990
+ const sources = [...sourcesOf(loaded, "package").map((row2) => ({ ...row2, kind: "package" })), ...(overlay?.names ?? []).map((name2) => ({ name: name2, version: null, kind: "overlay", tools: overlay.namedTools?.find((row2) => row2.name === name2)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
122805
122991
  return {
122806
122992
  newest,
122807
122993
  quoted: { id: runIdOf2(record3), host: facts7.host, record: record3, rows: Object.fromEntries(loadedRows(record3, { past: true }).map((row2) => [row2.label, row2])), problems, sources, tools, notes: [...cutWords(loaded), ...wentOn(record3) && loaded.at !== "ready" ? [LOADED_WORDS.notedEarly] : []] }
@@ -122829,12 +123015,12 @@ function hostLoadsRow(hostLoads, name2) {
122829
123015
  const row2 = Array.isArray(hostLoads?.hosts) ? hostLoads.hosts.find((entry3) => entry3?.name === name2) : null;
122830
123016
  return row2 ?? { name: name2, state: "unknown", why: hostLoads?.unreachable ? "unreachable" : "absent" };
122831
123017
  }
122832
- function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null }) {
123018
+ function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null, mcp = null }) {
122833
123019
  const row2 = hostLoadsRow(hostLoads, host);
122834
123020
  const first = noOverlay(hostPiFor(row2, image));
122835
123021
  const unknown3 = (why) => [`${first} What a job of trigger ${triggerIndex + 1} would load on this host is unknown: ${why}.`];
122836
123022
  if (row2.state === "unknown") return unknown3(row2.why === "unreachable" ? "the hosts\u2019 doctor runs could not be read" : row2.why === "unreadable" ? "its last doctor run could not be read" : "it has published no doctor run");
122837
- const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed));
123023
+ const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed) && (part !== "mcp" || !Array.isArray(mcp) || mcp.length > 0));
122838
123024
  if (unread2.length > 0) return unknown3(`its last doctor run could not read ${listed(unread2.map((part) => UNREAD_PART_WORDS[part] ?? "part of what it publishes"))}`);
122839
123025
  const packages = withholdsPackages ? [] : (row2.packages ?? []).filter((entry3) => !excludes.includes(entry3.name)).map((entry3) => `${entry3.name} ${entry3.version}`);
122840
123026
  const extensions = row2.overlayExtensionsAllowed ? (row2.extensions ?? []).map((entry3) => entry3.name).filter((name2) => !excludes.some((excluded) => overlayEntryNamed(excluded, name2))) : [];
@@ -123824,11 +124010,11 @@ function readDeploymentEnv({ dir, fs: fs9, uid = process.geteuid?.(), names: nam
123824
124010
  }
123825
124011
  }
123826
124012
  function deploymentServiceEnv({ env: env2, dir, keys: keys3, platform = process.platform, fs: fs9, uid = process.geteuid?.() }) {
123827
- 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 });
123828
124014
  const raw = untrusted ? null : bytes;
123829
124015
  const file = raw === null ? null : serviceEnvFileOf(raw, path6, serviceEnvLoader(platform));
123830
124016
  const shellRunsLine = file?.loader === "shell" && file.hazard !== null && file.hazard.shape !== "shell-crlf-comment" ? file.hazard.line : null;
123831
- 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 };
123832
124018
  }
123833
124019
  var init_service_env = __esm({
123834
124020
  "../worker/src/service-env.mjs"() {
@@ -125087,6 +125273,8 @@ async function enqueueTriggerFire(plan, options = {}) {
125087
125273
  const canary = options.packageCanary ?? null;
125088
125274
  const id7 = (canary ? manualCanaryJobId : manualTriggerJobId)({ triggerId: current.schedulerId, now: instant7 });
125089
125275
  redis = redisFn(url);
125276
+ redis.on?.("error", () => {
125277
+ });
125090
125278
  if (await redis.set(`trigger-fire:${current.queue}:${id7}`, "1", "EX", 120, "NX") !== "OK") return { refusal: "This cron trigger was already fired this minute. Nothing was queued.", deduplicated: true };
125091
125279
  queue = queueFn(parseConnectionFn(url, { failFast: true }), { name: current.queue });
125092
125280
  const schedule = canary ? { ...current.schedule, data: withJobDataVersion({ ...current.schedule.data, packageCanary: canary }) } : current.schedule;
@@ -127148,6 +127336,7 @@ __export(read_model_exports, {
127148
127336
  liveAbsentCause: () => liveAbsentCause,
127149
127337
  liveJobsOfRun: () => liveJobsOfRun,
127150
127338
  loaderRefuses: () => loaderRefuses,
127339
+ loaderTriggers: () => loaderTriggers,
127151
127340
  makeLocalRunIndex: () => makeLocalRunIndex,
127152
127341
  mapSchedulers: () => mapSchedulers,
127153
127342
  mergedDollarProblem: () => mergedDollarProblem,
@@ -127294,7 +127483,7 @@ import { execFileSync as execFileSync3 } from "node:child_process";
127294
127483
  import { homedir as homedir3 } from "node:os";
127295
127484
  function resolvePaths(env2 = process.env) {
127296
127485
  return {
127297
- valkeyUrl: env2.VALKEY_URL ?? "redis://127.0.0.1:6379",
127486
+ valkeyUrl: env2.VALKEY_URL ?? DEFAULT_VALKEY_URL,
127298
127487
  logsDir: logsDirPath(env2),
127299
127488
  settingsFile: settingsFilePath(env2),
127300
127489
  // Cwd defaults match what `pi-dispatch init` scaffolds (and, since issue #80, what the receiver
@@ -127454,7 +127643,7 @@ function panelValkeyContext({ env: env2 = process.env, pointerDir = null, owned
127454
127643
  if (pointerDir === null) return valkeyContextFromKeys({ env: own2, platform });
127455
127644
  const res = deploymentServiceEnv({ env: {}, dir: pointerDir, keys: VALKEY_CONTEXT_KEYS, platform, fs: fs9, uid });
127456
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;
127457
- 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 });
127458
127647
  }
127459
127648
  function installPanelValkeyContext({ env: env2 = process.env } = {}) {
127460
127649
  useValkeyContext(() => {
@@ -127581,6 +127770,8 @@ async function checkDispatchRun({
127581
127770
  let redis;
127582
127771
  try {
127583
127772
  redis = redisFn(valkeyUrl);
127773
+ redis.on?.("error", () => {
127774
+ });
127584
127775
  const key2 = hourKey(now());
127585
127776
  const count7 = Number(consumeRate ? await redis.incr(key2) : await redis.get(key2) ?? 0);
127586
127777
  if (consumeRate && count7 === 1) await redis.expire(key2, DISPATCH_RUN_TTL_SECONDS);
@@ -128454,6 +128645,8 @@ async function readHosts({ url, redisFn = makeRedisClient, timeoutMs = 2500, now
128454
128645
  try {
128455
128646
  parseConnection(url, { failFast: true });
128456
128647
  redis = redisFn(url);
128648
+ redis.on?.("error", () => {
128649
+ });
128457
128650
  const res = await withTimeout2(readLiveHosts(redis, { now, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) }), timeoutMs, { unreachable: "timed out reaching the registry" });
128458
128651
  if (res.unreachable) return { unreachable: res.unreachable };
128459
128652
  return { hosts: res.hosts };
@@ -128477,6 +128670,8 @@ async function readHeldJobs({ url, limit: limit3 = 20, redisFn = makeRedisClient
128477
128670
  try {
128478
128671
  parseConnection(url, { failFast: true });
128479
128672
  redis = redisFn(url);
128673
+ redis.on?.("error", () => {
128674
+ });
128480
128675
  const settled = (async () => {
128481
128676
  const ids = await redis.smembers(HELD_SET);
128482
128677
  const hydrated = await Promise.all(
@@ -128545,6 +128740,8 @@ async function cancelHeldJob({ url, jobId, redisFn = makeRedisClient, queueFn =
128545
128740
  try {
128546
128741
  parseConnection(url, { failFast: true });
128547
128742
  redis = redisFn(url);
128743
+ redis.on?.("error", () => {
128744
+ });
128548
128745
  settled = (async () => {
128549
128746
  const { names: names2 } = await readFleetQueues({ url, redisFn, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) });
128550
128747
  let owner2 = null;
@@ -128677,6 +128874,8 @@ async function readBudget({ url, redisFn = makeRedisClient, timeoutMs = 2500, no
128677
128874
  try {
128678
128875
  parseConnection(url, { failFast: true });
128679
128876
  redis = redisFn(url);
128877
+ redis.on?.("error", () => {
128878
+ });
128680
128879
  const settled = Promise.all([redis.get(dayKey(now)), redis.get(weekKey(now)), redis.get(monthKey(now)), redis.get(tokenDayKey(now))]).then(
128681
128880
  ([day2, week, month, tokens]) => ({ day: Number(day2 ?? 0), week: Number(week ?? 0), month: Number(month ?? 0), tokensToday: Number(tokens ?? 0) }),
128682
128881
  (err) => ({ unreachable: err?.message ?? String(err) })
@@ -128854,11 +129053,13 @@ function readTriggers({ triggersPath, fs: fs9 = nodeFs11 }) {
128854
129053
  return { triggers: triggers2, count: entries.length, ...bundles !== null && { secretBundles: bundles }, ...loaderRefuses(text4, triggersPath) && { doesNotLoad: true } };
128855
129054
  }
128856
129055
  function loaderRefuses(text4, path6) {
129056
+ return loaderTriggers(text4, path6) === null;
129057
+ }
129058
+ function loaderTriggers(text4, path6) {
128857
129059
  try {
128858
- parseTriggers(text4, path6);
128859
- return false;
129060
+ return parseTriggers(text4, path6);
128860
129061
  } catch {
128861
- return true;
129062
+ return null;
128862
129063
  }
128863
129064
  }
128864
129065
  function configLocationsOf({ paths = {}, deploymentDir = null, home = homedir3() } = {}) {
@@ -130598,6 +130799,21 @@ function createFleetReader(paths, {
130598
130799
  async sandboxEnd(request2) {
130599
130800
  return requestSandboxEndFn({ redis, ...request2, now: nowFn });
130600
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
+ },
130601
130817
  async dispose() {
130602
130818
  disposed = true;
130603
130819
  for (const q of sandboxQueues.values()) {
@@ -138104,7 +138320,10 @@ async function readConnections({ deploymentDir, env: env2 = {}, paths = {}, snap
138104
138320
  if (redis) return redis;
138105
138321
  if (owned) return owned;
138106
138322
  if (injectedRead && typeof paths.valkeyUrl !== "string") return null;
138107
- return owned = redisFn(paths.valkeyUrl);
138323
+ owned = redisFn(paths.valkeyUrl);
138324
+ owned.on?.("error", () => {
138325
+ });
138326
+ return owned;
138108
138327
  };
138109
138328
  const endpointFleetDeclared = endpointFleetDeclaration(file, environmentUnknown);
138110
138329
  let endpointStatus = { state: "unknown", hosts: [], more: 0, partial: true };
@@ -150359,9 +150578,15 @@ function buildSandboxRunArgs({ venue = DEFAULT_BACKEND, image, name: name2, work
150359
150578
  TMOUT: idleSeconds > 0 ? String(idleSeconds) : void 0,
150360
150579
  // Beside `--user` only (issue #341). Not a credential either.
150361
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),
150362
150587
  // Still NO CREDENTIALS, and that clause is untouched: a proxy URL is not a credential, and
150363
150588
  // buildContainerEnv is still not reused here. The env is two variables about the terminal, HOME
150364
- // 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.
150365
150590
  ...proxyEnv
150366
150591
  },
150367
150592
  // Ahead of the env and the mounts, and well ahead of the image, which the builder keeps as
@@ -153083,7 +153308,7 @@ function sizingLines(sizing, id7, iw, styler) {
153083
153308
  if (verdicts.length > 0) push(wrapColumns(verdicts.join("; "), sub2, styler), tone);
153084
153309
  if (typeof s2.call === "string") out.push(...callLines(cellOf(s2.call), iw, styler, indent.length));
153085
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");
153086
- 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");
153087
153312
  return out;
153088
153313
  }
153089
153314
  function projectRows(snapshot, info = null) {
@@ -161477,362 +161702,6 @@ var init_models_view = __esm({
161477
161702
  }
161478
161703
  });
161479
161704
 
161480
- // src/console/panels/sandboxes.mjs
161481
- function state2(v) {
161482
- const s2 = report(v)?.request;
161483
- if (!s2) return null;
161484
- const success = s2.state === "ready";
161485
- return banner({ status: success ? "ok" : ["refused", "failed"].includes(s2.state) ? "attention" : "running", title: success ? `Sandbox ${s2.result?.id ?? s2.id} is ready` : `Sandbox request ${s2.state}`, body: html`<span class="code">${s2.id}</span> on ${s2.host ?? "its host"}. ${s2.message ?? s2.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s2.result ? links(v, { ...s2.result, id: s2.result.id, host: s2.result.host ?? s2.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s2.id, host: s2.host ?? selected3(v).host ?? "" })}`, label: "Refresh request" }) });
161486
- }
161487
- function sandboxFromRun(v) {
161488
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161489
- const all = rows(v);
161490
- const id7 = v.query?.id;
161491
- const picked = all.find((r2) => rowId2(r2) === id7 && (!v.query?.host || r2.host === v.query.host));
161492
- const pickerHost = v.query?.host ?? selected3(v).host ?? all[0]?.host ?? "";
161493
- const picker = html`<form method="get" action="/runs/sandboxes" data-live-hold class="sx-picker">${field2({ label: "Workspace host", id: "sb-workspace-host", control: select({ id: "sb-workspace-host", name: "host", value: pickerHost, options: [{ value: "", label: "Choose a host" }, ...(report(v)?.hosts ?? []).map((h2) => ({ value: h2.name, label: h2.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id7 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r2) => !pickerHost || r2.host === pickerHost).map((r2) => ({ value: rowId2(r2), label: `${rowId2(r2)} on ${r2.host}${r2.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
161494
- if (!report(v)) return card3("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
161495
- if (!picked) return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<p class="card__sub">${all.length ? "Choose a kept workspace, or open Sandboxes from a run page." : SANDBOX_WORDS.empty}</p>`, "the image, workspace and isolation the run kept");
161496
- const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
161497
- const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
161498
- const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId2(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
161499
- const what2 = facts([["Image", picked.image ?? "As recorded in its kept manifest"], ["/workspace", workspace], ["/job", "Read only, with the retained job inputs"], ["pi and what it loads", wordsHtml(loads)], ["Processes", "None until you start them"], ["Credentials", "None, on purpose"], ["Network", "The host's current job egress policy"]], { cols: 1 });
161500
- return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<div class="sx-run">${tile("ok", { icon: "box" })}<div><p class="sx-run__t"><span class="code">${rowId2(picked)}</span>${picked.project ? ptag(picked.project) : null}${picked.prepared ? chip("Without a run") : null}</p><p class="sx-run__s">${picked.host} ${vtag(picked.v ?? picked.backend)}. ${retention(picked, v.now)}</p></div></div><h3 class="sx-h3">What opens</h3>${what2}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.sandboxFoot}</span></p><h3 class="sx-h3">How it opens</h3>${canOpen(picked) ? html`<div class="sx-ch sx-ch--on"><div class="sx-ch__h">${icon("terminal", { size: "sm" })}<b>In your terminal</b><span class="sx-rec sandbox-opener">recommended</span></div><p class="sx-ch__s">Your terminal opens with the command typed in and waits for Enter. The link holds no secret. On a phone, copy the command.</p>${links(v, picked)}</div><div class="sx-foot"><p>${icon("info", { size: "sm" })}<span>The shell uses a job's isolation and network. A published host port is refused while egress is on. Pinning uses this host's configured pin window.</span></p>${pinForm(v, picked)}</div>` : html`<p>This venue has no sandbox shell. Its workspace cannot be reopened here.</p>`}`, "the same image, workspace and isolation as the run");
161501
- }
161502
- function sandboxNew(v) {
161503
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161504
- const s2 = selected3(v), data5 = report(v), options = data5?.options ?? [];
161505
- const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
161506
- const triggers2 = options;
161507
- const hostOptions = (data5?.hosts ?? []).filter((h2) => !h2.unreadable && !h2.stale && h2.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h2.name))).map((h2) => ({ value: h2.name, label: `${h2.name}, ${h2.settings.defaultBackend ?? "venue not published"}` }));
161508
- const fields3 = [
161509
- field2({ label: "Repository", id: "sb-repo", control: input({ id: "sb-repo", name: "repo", value: s2.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
161510
- field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s2.index === void 0 ? "" : String(s2.index), options: [{ value: "", label: "Choose a trigger" }, ...triggers2.map((o) => ({ value: String(o.index), label: o.label }))] }), hint: "Its image, size and egress are shown below, rather than chosen here." }),
161511
- field2({ label: "Start from", id: "sb-ref", control: input({ id: "sb-ref", name: "ref", value: s2.ref ?? "HEAD", code: true, required: true, autocomplete: "off", spellcheck: false }), hint: "HEAD means the default branch. Enter a branch, tag, full commit SHA, or pr:42 for a pull request." }),
161512
- field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s2.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
161513
- field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s2.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
161514
- ];
161515
- const plan = data5?.plan;
161516
- 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;
161517
- 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>`;
161518
- const fresh = plan ? html`${facts([[`On ${plan.host}`, wordsHtml(sandboxFreshLine({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }))]], { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
161519
- 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>`;
161520
- 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");
161521
- }
161522
- function sandboxOpen(v) {
161523
- if (!allowed3(v)) return null;
161524
- const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s2) => !v.query?.gatewaySelect || s2.host === v.query.host);
161525
- const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
161526
- const notices = (data5?.notices ?? []).filter((n3) => !v.query?.gatewaySelect || n3.host === v.query.host).map((n3) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n3.host}, ${n3.id}: ${{ "network-kept": "The sandbox network was kept because cleanup could not be confirmed. Inspect this host\u2019s runtime and network before removing either.", "network-cleanup-failed": "Sandbox network cleanup failed. Inspect this host\u2019s runtime and egress network before retrying cleanup.", "runtime-unreadable": "The runtime could not be read. An expired shell registration does not prove the container stopped. Inspect this host before reopening or cleaning it." }[n3.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n3.host })}`}">Inspect host</a>.</p>`);
161527
- if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card3("Open now", html`<p role="status">The browser gateway is not enabled for ${v.query.host}, so there is no shell to choose here. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>`);
161528
- return card3("Open now", html`${v.query?.gatewaySelect ? html`<p class="sx-p">Choose an already-open shell on ${v.query.host}. Each browser opening needs your current operator token. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>` : null}${notices}${data5?.noticesUnavailable ? html`<p role="status">Some sandbox cleanup notices could not be read. Inspect the host if a terminal ended unexpectedly.</p>` : null}${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? html`<p role="status">The registered shell list is incomplete: at least ${data5.sessionsMore ?? 0} more and ${data5.sessionsUnreadable ?? 0} unreadable registrations.</p>` : null}${data5?.sessionsUnavailable ? html`<p role="status">Open shells could not be read. Reload to try again.</p>` : sessions.length ? html`<ul class="sx-opens">${sessions.map((s2) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s2.id}</span>${vtag(s2.venue)}</p><p class="sx-open__s">${s2.host}, opened by ${s2.by ?? "the terminal operator"}. Open since ${stamp(s2.since)}.</p>${sandboxDriverLine(v, s2)}<div class="sx-open__a">${links(v, s2, "attach", true)}${sandboxBrowserLink(v, s2)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s2.host], ["id", s2.id], ["nonce", s2.nonce], ["containerId", s2.containerId]], label: "Preview End", kind: ["sm", "danger"], icon: "cancel" }) : null}</div></div></li>`)}</ul>` : html`<p>${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? "No readable shell is in this bounded view. The incomplete registrations above prevent an exact empty count." : SANDBOX_WORDS.noShells}</p>`}<p class="sx-small">${icon("info", { size: "sm" })}<span>One shell per workspace at a time. End asks the terminal that opened it to close the shell and keeps the workspace. What you type is not recorded.</span></p>`, null, badge(incomplete ? "attention" : "running", `${sessions.length} ${incomplete ? "shown" : "open"}`));
161529
- }
161530
- function sandboxKept(v) {
161531
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161532
- const data5 = report(v), all = rows(v);
161533
- return card3("Kept workspaces", html`${data5?.more > 0 ? html`<p role="status">At least ${data5.more} more hosts have inventory beyond this bounded view. Inspect the fleet before treating this list as complete.</p>` : null}${!data5 ? html`<p role="status">${SANDBOX_WORDS.unavailable}</p>` : html`${(data5.hosts ?? []).filter((h2) => h2.stale || h2.unreadable).map((h2) => html`<p class="sx-p" role="status">${h2.name}: ${h2.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r2) => html`<li class="sx-kept${canOpen(r2) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId2(r2), host: r2.host })}`}">${rowId2(r2)}</a>${r2.project ? ptag(r2.project) : null}${r2.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r2.host} ${vtag(r2.v ?? r2.backend)}. ${retention(r2, v.now)}${!canOpen(r2) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r2) ? html`<div class="sx-kept__actions">${links(v, r2, "open", true)}${pinForm(v, r2)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h2) => h2.more > 0).map((h2) => html`<p class="sx-p">${h2.name} has at least ${h2.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
161534
- }
161535
- function sandboxBrowser(v) {
161536
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161537
- const gateway = v.data?.browserGateways;
161538
- const count7 = gateway?.enabledHosts?.length ?? 0;
161539
- return card3("In this browser", html`<p class="sx-p">${SANDBOX_WORDS.browser}</p>${gateway?.problem ? html`<p class="sx-p" role="status">${gateway.problem}</p>` : html`<p class="sx-p">${count7 ? `${count7} ${count7 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count7 ? html`<span class="sandbox-opener"> Choose Open in browser beside a registered shell in Open now.</span>` : null}</p>`}<p class="sx-p">Each opening needs your current operator token again. Only one browser window attaches at a time. Closing it keeps the original sandbox; what you type is not recorded.</p><p class="sx-p">First open a kept or prepared workspace in your own terminal. The browser never starts a container.</p><p class="sx-p">Browser terminals require a window wider than 760 pixels. On a phone, copy the command for your own terminal.</p>${v.readOnly ? html`<p class="sx-p">This console is read-only, so browser attachment is unavailable.</p>` : null}`, "optional, separate gateway", chip(count7 ? "Enabled per host" : "Off by default", { status: count7 ? "ok" : "idle", icon: "shield" }));
161540
- }
161541
- var SANDBOX_WORDS, report, allowed3, acts2, rows, card3, stamp, rowId2, canOpen, selected3, facts5, links, retention, pinForm, SANDBOX_PANELS;
161542
- var init_sandboxes = __esm({
161543
- "src/console/panels/sandboxes.mjs"() {
161544
- init_html();
161545
- init_kit();
161546
- init_forges();
161547
- init_sandbox_links();
161548
- init_common();
161549
- init_loaded_words();
161550
- init_loaded_parts();
161551
- SANDBOX_WORDS = Object.freeze({
161552
- unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
161553
- empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
161554
- noShells: "No live sandbox shell was registered. A shell whose terminal stopped answering disappears when its short lease expires. An expired registration is not proof the container stopped; inspect its host if the terminal ended unexpectedly.",
161555
- browser: "A separate gateway can attach to an already-open sandbox. It is off by default and enabled by hand per host. Open in Terminal and copy-command support remain available.",
161556
- noScript: "Preparing, pinning, ending a shell and saving terminal targets require JavaScript for Preview and Confirm. Commands remain selectable and the settings form works without JavaScript."
161557
- });
161558
- report = (v) => v.data?.sandboxes;
161559
- allowed3 = (v) => v.role === "operator";
161560
- acts2 = (v) => allowed3(v) && !v.readOnly;
161561
- rows = (v) => (report(v)?.hosts ?? []).flatMap((h2) => (h2.rows ?? []).map((r2) => ({ ...r2, host: h2.name, hostStale: h2.stale, retentionHours: h2.retentionHours })));
161562
- card3 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
161563
- stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
161564
- rowId2 = (r2) => r2.id ?? r2.jobId;
161565
- canOpen = (r2) => !String(rowId2(r2)).startsWith("sha256:") && r2.canOpen !== false && ["local", "podman"].includes(r2.v ?? r2.backend);
161566
- selected3 = (v) => report(v)?.selected ?? {};
161567
- facts5 = (plan) => facts([
161568
- ["Image", plan.image ?? "not published"],
161569
- ["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
161570
- ["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
161571
- ["Credentials", "None in the shell"]
161572
- ]);
161573
- links = (v, r2, verb = "open", compact2 = false) => sandboxLinks({ id: rowId2(r2), host: r2.host, verb, compact: compact2, config: v.data?.sandboxHosts });
161574
- retention = (r2, now) => html`${r2.pin ? "Pinned" : "Kept"}${Number.isFinite(r2.until) ? html` until ${stamp(r2.until)}${r2.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r2.until - now)} left`}` : ": deadline not published"}`;
161575
- pinForm = (v, r2) => acts2(v) && canOpen(r2) ? actForm({ action: "sandbox-pin", hidden: [["host", r2.host], ["id", rowId2(r2)]], label: "Preview pin", icon: "clock" }) : null;
161576
- SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
161577
- }
161578
- });
161579
-
161580
- // src/zero-touch-view.mjs
161581
- function breakdown(row2, repository = false) {
161582
- const out = { project: text3(row2?.project ?? row2?.id, 64), scopeId: scope(row2?.scopeId), name: text3(row2?.name ?? row2?.target ?? row2?.project ?? row2?.id) ?? "Unassigned", target: text3(row2?.target), runs: count5(row2?.runs), touched: count5(row2?.touched), rate: number3(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number3(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number3(row2?.streakDays), zeroTouch: row2?.zeroTouch === true && row2?.waitingPartial !== true, waitingNow: row2?.waitingPartial === true ? null : count5(row2?.waitingNow ?? row2?.waiting), waitingPartial: row2?.waitingPartial === true, topCause: CAUSES.includes(row2?.topCause) ? row2.topCause : null, causes: causes(row2?.causes), touchedRunIds: runIds(row2?.touchedRunIds) };
161583
- if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r2) => breakdown(r2, true)) : [];
161584
- return out;
161585
- }
161586
- function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
161587
- const observed = raw?.coverage ?? raw?.window;
161588
- const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number3(observed.fromMs) !== null && observed.fromMs >= (number3(raw?.nowMs) ?? nowMs2);
161589
- if (!raw || typeof raw !== "object" || raw.state === "unavailable" || unobserved2) return { state: "unavailable", nowMs: nowMs2, windowDays, coverage: { partial: true, reasons: [] }, totals: {}, projects: [], causes: causes([]), trend: [], groups: [], fixes: [], fixesPartial: true, touchedRunIds: [], waitingRuns: [], ungroupedRunIds: [], grouping: { enabled: false, pending: 0, unassigned: 0, state: "disabled", sources: [] } };
161590
- const totals = raw.totals ?? raw.summary ?? {};
161591
- const coverage2 = raw.coverage ?? raw.window ?? {};
161592
- const out = {
161593
- state: "ready",
161594
- nowMs: number3(raw.nowMs) ?? nowMs2,
161595
- windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
161596
- coverage: { requestedFromMs: number3(coverage2.requestedFromMs), fromMs: number3(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
161597
- totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number3(totals.rate), previousRate: number3(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number3(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number3(totals.medianWaitMs) ?? (number3(totals.medianWaitHours) === null ? null : totals.medianWaitHours * 36e5), pending: totals.waitingPartial === true ? null : count5(totals.pending ?? totals.waitingNow), draftAnswers: count5(totals.draftAnswers), deltaPoints: Number.isFinite(totals.deltaPoints) ? totals.deltaPoints : null },
161598
- projects: Array.isArray(raw.projects) ? raw.projects.map((r2) => breakdown(r2)) : [],
161599
- causes: causes(raw.causes),
161600
- trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r2) => ({ atMs: number3(r2?.atMs ?? r2?.at), rate: number3(r2?.rate), runs: count5(r2?.runs), touched: count5(r2?.touched), partial: r2?.partial === true })) : [],
161601
- waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r2) => ({ runId: id5(r2?.runId ?? r2?.jobId), parkedAt: number3(r2?.parkedAt) })).filter((r2) => r2.runId && r2.parkedAt !== null) : [],
161602
- touchedRunIds: runIds(raw.touchedRunIds),
161603
- ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
161604
- groups: [],
161605
- fixes: [],
161606
- fixesPartial: raw.fixesPartial === true,
161607
- grouping: { enabled: raw.grouping?.enabled === true, partial: raw.grouping?.partial === true, pending: count5(raw.grouping?.pending), unassigned: count5(raw.grouping?.unassigned), state: ["ready", "ok", "disabled", "pending", "partial", "failed", "configuration-unavailable", "unavailable"].includes(raw.grouping?.state) ? raw.grouping.state : "disabled", sources: role === "operator" && Array.isArray(raw.grouping?.sources ?? raw.groupingStatuses) ? (raw.grouping?.sources ?? raw.groupingStatuses).map((r2) => ({ runId: id5(r2?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r2?.status) ? r2.status : "source-unavailable" })).filter((r2) => r2.runId) : [] }
161608
- };
161609
- out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f3) => ({ id: id5(f3?.id), at: number3(f3?.at), undoneAt: number3(f3?.undoneAt), kind: ["instructions", "config"].includes(f3?.kind) ? f3.kind : "config", groupId: id5(f3?.groupId), reason: typeof f3?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f3.reason) ? f3.reason : null, triggerIndex: Number.isSafeInteger(f3?.triggerIndex) && f3.triggerIndex >= 0 ? f3.triggerIndex : null, beforeCount: number3(f3?.beforeCount), afterCount: number3(f3?.afterCount), spanMs: number3(f3?.spanMs), partial: f3?.partial !== false, subsequentRuns: count5(f3?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f3?.status) ? f3.status : "observing" })).filter((f3) => f3.id) : [];
161610
- if (role === "operator") out.groups = Array.isArray(raw.groups) ? raw.groups.map((g) => ({ id: id5(g?.id), name: text3(g?.name, 120), scopeId: scope(g?.scopeId), repo: text3(g?.repo), project: text3(g?.project, 64), count: count5(g?.count), runIds: runIds(g?.runIds), status: ["open", "fixed", "unassigned"].includes(g?.status) ? g.status : "open", fixedAt: number3(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
161611
- return out;
161612
- }
161613
- var CAUSES, id5, number3, count5, text3, scope, causes, runIds;
161614
- var init_zero_touch_view = __esm({
161615
- "src/zero-touch-view.mjs"() {
161616
- CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
161617
- id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
161618
- number3 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
161619
- count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
161620
- text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
161621
- scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
161622
- causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
161623
- runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
161624
- }
161625
- });
161626
-
161627
- // src/console/panels/zero-touch.mjs
161628
- function delta(n3, previousRate = null) {
161629
- if (!Number.isFinite(n3)) return html`<span class="zt-tr">Previous interval not covered</span>`;
161630
- return html`<span class="${`zt-tr${n3 < 0 ? " tone-ok" : n3 > 0 ? " tone-attention" : ""}`}">${icon(n3 < 0 ? "chev" : n3 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n3 === 0 ? "no change" : `${n3 < 0 ? "down" : "up"} ${amount3(Math.abs(n3))} points`}</b>${previousRate === null ? null : html`<small>from ${pct2(previousRate)}</small>`}</span>`;
161631
- }
161632
- function coverage(m2) {
161633
- return html`<p class="zt-coverage" role="status">${m2.coverage.partial ? html`<b>Partial ${m2.windowDays}-day window.</b> Covered from ${date(m2.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m2.windowDays}-day window.`}${m2.coverage.reasons.length ? html` ${m2.coverage.reasons.join("; ")}.` : null}</p>`;
161634
- }
161635
- function spark(m2) {
161636
- const rows3 = m2.trend;
161637
- if (!rows3.some((r2) => Number.isFinite(r2.rate) && !r2.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
161638
- const top = Math.max(32, ...rows3.map((r2) => r2.rate ?? 0));
161639
- const x2 = (i2) => rows3.length === 1 ? 200 : i2 * 200 / (rows3.length - 1);
161640
- const y = (n3) => 56 - n3 / top * 56;
161641
- const segments = [];
161642
- let points = [];
161643
- for (const [i2, row2] of rows3.entries()) {
161644
- if (row2.rate === null || row2.partial) {
161645
- if (points.length) segments.push(points);
161646
- points = [];
161647
- } else points.push(`${svgNum(x2(i2), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
161648
- }
161649
- if (points.length) segments.push(points);
161650
- const last = rows3[rows3.length - 1];
161651
- const label3 = `Rolling ${m2.windowDays}-day touch rate at weekly intervals: ${rows3.map((r2) => r2.partial || r2.rate === null ? "not covered" : pct2(r2.rate)).join(", ")}. Goal under 5 percent.`;
161652
- return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label3}"><line class="zt-spark__goal" x1="0" x2="200" y1="${svgNum(y(5), 0, 56)}" y2="${svgNum(y(5), 0, 56)}"/>${segments.map((p) => html`<polyline class="zt-spark__l" points="${p.join(" ")}"/>`)}${last?.rate !== null && !last?.partial ? html`<circle class="zt-spark__d" cx="200" cy="${svgNum(y(last.rate), 0, 56)}" r="3.5"/>` : null}</svg><p class="zt-spark__ax"><span>${date(rows3[0]?.atMs)}</span><span>goal, dashed</span><span>now</span></p><p class="sr">${label3}</p>`;
161653
- }
161654
- function longest(m2) {
161655
- return [...m2.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
161656
- }
161657
- function waitingHistory(m2) {
161658
- return m2.totals.waitingPartial ? html`<p class="zt-note" role="status">Waiting history is incomplete. An older park predates retained answer history; its current state and wait time are not known.</p>` : null;
161659
- }
161660
- function compact(m2) {
161661
- const best = longest(m2);
161662
- return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h, median ${span(m2.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m2.totals.draftAnswers} ${m2.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m2.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m2.totals.waitingPartial ? "Not known" : `${m2.totals.pending} ${m2.totals.pending === 1 ? "run" : "runs"}`}</b>${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</li></ul>${waitingHistory(m2)}`;
161663
- }
161664
- function zeroTouchRate(view) {
161665
- const m2 = model5(view);
161666
- return card({ title: `Touch rate, ${m2.windowDays} days`, body: html`${fail5(m2)}${m2.state === "ready" ? html`${kpi(m2.totals.runs ? pct2(m2.totals.rate) : "No runs", `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`)}${spark(m2)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m2.totals.deltaPoints, m2.totals.previousRate)}</p>${coverage(m2)}<p class="zt-kps">${m2.totals.stopped} stopped before they ran, outside the rate.${m2.totals.stoppedUnattributed > 0 ? html` ${m2.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m2)}</div>` : null}` });
161667
- }
161668
- function zeroTouchStreak(view) {
161669
- const m2 = model5(view), best = longest(m2);
161670
- return card({ title: "Zero-touch streak", body: html`${fail5(m2)}${kpi(best ? `${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history", best ? `${best.name}, the longest now` : "30 days of observed activity establish a zero-touch project")}<p class="zt-kp">${m2.projects.filter((p) => p !== best).sort((a, b) => (b.streakDays ?? -1) - (a.streakDays ?? -1)).map((p) => p.waitingNow ? chip(`${p.name} waiting now`, { status: "parked", icon: "parked" }) : html`<span class="zt-kt">${p.name} ${p.streakDays === null ? "not known" : `${amount3(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
161671
- }
161672
- function zeroTouchWait(view) {
161673
- const m2 = model5(view);
161674
- if (m2.state !== "ready") return card({ title: "Waiting for a person", body: fail5(m2) });
161675
- return card({ title: "Waiting for a person", body: html`${fail5(m2)}${kpi(m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h`, `runs waited on a person in ${m2.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m2.totals.medianWaitMs)}</b></p>${waitingHistory(m2)}<p class="zt-kp">${chip(m2.totals.waitingPartial ? "Pending count not known" : `${m2.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</p>` });
161676
- }
161677
- function zeroTouchHandled(view) {
161678
- const m2 = model5(view);
161679
- return card({ title: "Answered from a draft", body: m2.state !== "ready" ? fail5(m2) : html`${kpi(m2.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m2.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
161680
- }
161681
- function meter3(row2) {
161682
- if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
161683
- const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
161684
- const word3 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
161685
- return html`<span class="zt-m"><svg class="${`pbar tone-${tone}`}" height="10" role="meter" aria-valuenow="${svgNum(row2.rate, 0, 100)}" aria-valuemin="0" aria-valuemax="100" aria-label="${`${row2.name}: ${pct2(row2.rate)} of runs needed a person, goal under 5 percent; meter scale 40 percent`}"><rect class="pbar__bed" width="100%" height="10" rx="5"/><rect class="pbar__fill" width="${`${svgNum(row2.rate / 40 * 100, 0, 100)}%`}" height="10" rx="5"/><line class="zt-goal" x1="12.5%" x2="12.5%" y1="-4" y2="14"/></svg><span class="zt-mv"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span></span>`;
161686
- }
161687
- function rowContent(row2, sub2 = false) {
161688
- const c = causesByKey.get(row2.topCause);
161689
- return html`<span class="zt-chev">${sub2 ? null : icon("chev-r", { size: "sm" })}</span><span class="zt-name"><b>${row2.name}</b><small>${sub2 ? `${row2.runs} runs` : row2.repositories.map((r2) => r2.name).join(" and ")}</small></span>${meter3(row2)}${delta(row2.deltaPoints, row2.previousRate)}<span class="zt-n"><b>${row2.touched}</b> of ${row2.runs} runs</span><span class="${`zt-st${row2.waitingNow ? " tone-parked" : ""}`}">${icon(row2.waitingNow ? "parked" : "flag", { size: "sm" })}${row2.waitingPartial ? "waiting history incomplete" : row2.waitingNow ? "waiting now" : row2.zeroTouch ? "zero touch, 30+ days" : row2.streakDays === null ? "not enough history" : `${amount3(row2.streakDays)} ${row2.streakDays === 1 ? "day" : "days"}`}</span><span class="zt-c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : "No touches"}</span>`;
161690
- }
161691
- function phoneProjects(m2, view) {
161692
- const item2 = (row2, repository = false) => {
161693
- const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
161694
- const word3 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
161695
- const query = new URLSearchParams({ days: String(m2.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
161696
- return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct2(row2.rate)}, open ${repository ? "repository" : "project"}`}"${repository ? html` data-repository` : null}><span class="ztp-p__n"><b>${row2.name}</b><small>${row2.touched} of ${row2.runs} runs</small></span><span class="ztp-p__r"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span><span class="ztp-p__c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : html`<span>No touches</span>`}${delta(row2.deltaPoints, row2.previousRate)}</span><span class="ztp-p__go">${icon("chev-r", { size: "sm" })}</span></a></li>`;
161697
- };
161698
- return html`<ul class="ztp-ps zt-phone-projects">${m2.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r2) => item2(r2, true)) : null}`)}</ul>`;
161699
- }
161700
- function zeroTouchProjects(view) {
161701
- const m2 = model5(view);
161702
- if (m2.state !== "ready") return card({ title: "By project", body: fail5(m2) });
161703
- const head = html`<div class="zt-row zt-row--head" aria-hidden="true"><span></span><span>Project</span><span>Touch rate, goal tick at 5%</span><span>Against the previous interval</span><span>Needed a person</span><span>Since the last touch</span><span>Most often</span></div>`;
161704
- const rows3 = m2.projects.map((p) => html`<li><details class="zt-p"${view?.query?.project === p.project ? html` open` : null}><summary class="zt-row" aria-label="${`${p.name}: ${pct2(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r2) => html`<li class="zt-row zt-row--sub">${rowContent(r2, true)}</li>`)}</ul>${acts3(view) && p.touchedRunIds.length ? html`<p class="zt-foot"><a class="link" href="${`/runs?${new URLSearchParams({ project: p.project ?? "", touched: "1", days: String(m2.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
161705
- return card({ title: "By project", sub: `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`, flush: true, body: html`${fail5(m2)}${m2.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m2, view)}<p class="zt-foot">${icon("info", { size: "sm" })}<span>A row opens to its repositories or folders. A run counts once, however many interventions it received.</span></p>` : html`<p class="zt-note">${ZERO_TOUCH_WORDS.empty}</p>`}` });
161706
- }
161707
- function causeCounts(values) {
161708
- return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
161709
- }
161710
- function causeBar(values, max, name2) {
161711
- const rows3 = causeCounts(values);
161712
- let at2 = 0;
161713
- max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
161714
- const all = rows3.filter((r2) => r2.count > 0);
161715
- const label3 = `${name2}: ${all.map((r2) => `${r2.label} ${r2.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
161716
- return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label3}">${all.map((r2) => {
161717
- const x2 = at2 / Math.max(1, max) * 500, w = r2.count / Math.max(1, max) * 500;
161718
- at2 += r2.count;
161719
- return html`<g class="${`tone-${r2.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x2 / 5, 0, 100)}%`}" y="0" width="${`${svgNum(w / 5, 0, 100)}%`}" height="28" rx="5"/>${w >= 22 ? html`<text class="zt-cause-label" x="${`${svgNum((x2 + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r2.count}</text>` : null}</g>`;
161720
- })}</svg>`;
161721
- }
161722
- function zeroTouchCauses(view) {
161723
- const m2 = model5(view), max = Math.max(1, ...m2.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
161724
- if (m2.state !== "ready") return card({ title: "What needed a person", body: fail5(m2) });
161725
- const bars = m2.projects.map((p) => html`<li class="zt-cb"><span class="zt-cb__n"><b>${p.name}</b><small>${p.touched} of ${p.runs}</small></span>${causeBar(p.causes, max, p.name)}<p class="zt-cb__t">${causeCounts(p.causes).filter((c) => c.count).map((c) => `${c.short} ${c.count}`).join(" \xB7 ") || "No touches"}</p></li>`);
161726
- const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
161727
- return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m2.windowDays} days, by project</span><span class="zt-phone-label">${m2.windowDays} days, ${scopeLabel}</span>`, body: html`${fail5(m2)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m2.causes, Math.max(1, m2.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m2.causes).map((c) => html`<li class="zt-why">${tile(c.tone === "running" ? "running" : c.tone === "idle" ? "idle" : c.tone, { icon: c.icon, sm: true })}<span><b>${c.label}</b><small>${c.detail}</small></span><span class="zt-why__n">${c.count}</span></li>`)}<li class="zt-why zt-why--sum"><span></span><span><b>Runs that needed a person</b></span><span class="zt-why__n">${m2.totals.touched}</span></li></ul><p class="zt-merge">${icon("pr", { size: "sm" })}<span>Merging is never a touch. <a class="link" href="#zt-counting">How touches are counted</a></span></p><details class="zt-counting" id="zt-counting"><summary>How touches are counted</summary><p class="zt-kps">The rate counts workload runs with a retained record that ended in the covered interval. Each run counts once. Bars assign it to its first successful human intervention; later interventions remain in history. Jobs stopped before starting are separate. Reads, sandbox opens, automatic starts and internal grouping jobs do not enter the touch count.</p></details>` });
161728
- }
161729
- function guardedForm(action2, { hidden: hidden2 = [], fields: fields3 = null, label: label3, kind = ["sm"] } = {}) {
161730
- return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name2, value, type]) => html`<input type="hidden" name="${name2}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields3}${btn({ type: "submit", kind, label: label3 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
161731
- }
161732
- function nameForm(g, index2) {
161733
- const key2 = `zt-name-${index2}`;
161734
- return html`<details class="zt-editor"><summary class="link">${g.name ? "Rename this group" : "Name this group"}</summary>${guardedForm("zero-touch-name", { hidden: [["groupId", g.id]], fields: field2({ label: "Group name", id: key2, control: input({ id: key2, name: "name", value: g.name, required: true }), hint: "Write your own name, up to 120 characters. Question text is read only on its run page." }), label: "Preview name" })}</details>`;
161735
- }
161736
- function instructionForm(g, index2) {
161737
- if (!g.runIds[0] || g.triggerIndex === null) return html`<p class="zt-kps">Add to instructions is unavailable: a retained run and a current non-cron trigger are required.</p>`;
161738
- const key2 = `zt-sentence-${index2}`;
161739
- return html`<details class="zt-editor"><summary class="link">Add to instructions</summary>${guardedForm("zero-touch-instruction", { hidden: [["runId", g.runIds[0]], ["groupId", g.id], ["index", g.triggerIndex, "int"]], fields: field2({ label: "Sentence to add", id: key2, control: textarea({ id: key2, name: "sentence", required: true, rows: 3, maxlength: 2e3 }), hint: "Preview the exact diff, then Confirm. The trigger's existing instructions count toward the 2,000-character limit." }), label: `Preview trigger ${g.triggerIndex + 1}`, kind: ["sm", "primary"] })}</details>`;
161740
- }
161741
- function zeroTouchQuestions(view) {
161742
- if (view?.role !== "operator") return collapsed({ icon: "parked", title: "Questions it keeps asking", body: "are for operators. Rates, causes, streaks and fixes are shown here." });
161743
- const m2 = model5(view);
161744
- if (m2.state !== "ready") return card({ title: "Questions it keeps asking", body: fail5(m2) });
161745
- const rows3 = m2.groups.map((g, index2) => html`<li class="zt-q"><div class="zt-q__h"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><div><p class="${`zt-q__t${g.name ? "" : " zt-q__t--un"}`}">${g.name ?? `${g.count} similar questions, not named yet`}</p><p class="zt-q__s"><span class="code">${g.repo ?? g.project ?? "Repository not known"}</span>${g.status === "fixed" && g.fixedAt !== null ? html` · fixed ${date(g.fixedAt)} UTC` : null}${g.status === "fixed" && m2.fixes.some((f3) => f3.groupId === g.id && f3.afterCount === 0 && f3.subsequentRuns > 0 && !f3.partial && f3.undoneAt === null) ? html` · none since` : null}</p></div><span class="zt-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span></div><p class="zt-said">${icon("runs", { size: "sm" })}<span>Asked in ${g.runIds.slice(0, 8).map((id7, at2) => html`${at2 ? ", " : ""}${link(id7)}`)}${g.runIds.length > 8 ? ` and ${g.runIds.length - 8} more` : null}.</span></p>${acts3(view) ? html`<div class="zt-q__act">${nameForm(g, index2)}${g.status !== "fixed" ? instructionForm(g, index2) : html`<a class="link" href="#p-zt-fixes">See the fix</a>`}</div>` : null}</li>`);
161746
- const phoneRows = m2.groups.map((g, index2) => html`<li class="ztp-q"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><span class="ztp-q__t"><b>${g.runIds[0] ? html`<a class="zt-group-link" href="${`/run?id=${encodeURIComponent(g.runIds[0])}`}">${g.name ?? `${g.count} similar, not named yet`}</a>` : g.name ?? `${g.count} similar, not named yet`}</b><small>${g.repo ?? g.project ?? "Repository not known"}</small></span><span class="ztp-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span><span class="ztp-q__a">${acts3(view) ? g.status === "fixed" ? html`<a class="link" href="#p-zt-fixes">See the fix</a>` : g.name ? instructionForm(g, `phone-${index2}`) : nameForm(g, `phone-${index2}`) : null}</span></li>`);
161747
- const sources = m2.grouping.sources.length ? m2.grouping.sources : m2.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
161748
- return card({ title: "Questions it keeps asking", sub: "operator only", body: html`<p class="zt-intro">Grouped by meaning. You name each group; read the questions on their run pages.</p>${fail5(m2)}${m2.grouping.partial ? html`<p class="zt-note" role="status">Question grouping is partial. Missing or unclassified sources are not evidence that a question stopped recurring.</p>` : null}${["unavailable", "configuration-unavailable"].includes(m2.grouping.state) ? html`<p class="zt-note" role="status">${m2.grouping.state === "configuration-unavailable" ? "Grouping configuration could not be read. Check the reviewed triggers file before starting a grouping job." : "Question groups could not be read. Reload to retry; missing assignments are not an empty group list."}</p>` : null}${rows3.length ? html`<ol class="zt-qs zt-desktop-questions">${rows3}</ol><ol class="ztp-qs zt-phone-questions">${phoneRows}</ol>` : html`<p class="zt-kps">${["unavailable", "configuration-unavailable"].includes(m2.grouping?.state) ? "Grouping coverage is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noGroups}</p>`}${sources.length ? html`<ul class="zt-ungrouped">${sources.slice(0, 20).map((source2) => html`<li data-group-status="${source2.status}">${link(source2.runId)}<span>${ZERO_TOUCH_SOURCE_WORDS[source2.status]}</span>${acts3(view) && ["unassigned", "stale", "refused", "failed"].includes(source2.status) ? guardedForm("zero-touch-group", { hidden: [["runId", source2.runId]], label: "Group question", kind: ["sm", "paid"] }) : null}</li>`)}</ul>` : null}<p class="zt-foot zt-foot--in">${icon("info", { size: "sm" })}<span>${m2.grouping.enabled ? `${plural5(m2.grouping.pending, "grouping job")} pending; ${plural5(m2.grouping.unassigned, "question")} unassigned. Expired or unavailable context stays ungrouped.` : "Private capture and a capped classifier must be enabled in the reviewed zeroTouch configuration before questions can be grouped."}</span></p>` });
161749
- }
161750
- function fixTone(f3) {
161751
- if (f3.partial || f3.subsequentRuns === 0 || f3.undoneAt !== null || f3.status === "observing") return "idle";
161752
- return f3.status === "improved" ? "ok" : f3.status === "worse" ? "failed" : "idle";
161753
- }
161754
- function fixObservation(f3) {
161755
- if (f3.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
161756
- if (f3.partial) return "Partial observation: effectiveness is not established.";
161757
- if (f3.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
161758
- return f3.status === "improved" ? "Fewer occurrences over equal observed spans." : f3.status === "worse" ? "More occurrences over equal observed spans." : f3.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
161759
- }
161760
- function beforeAfter(label3, value, max, tone = "attention") {
161761
- return html`<p class="zt-ba"><span>${label3}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label3}: ${value === null ? "not known" : value}`}"><rect class="pbar__bed" width="100%" height="8" rx="4"/>${value ? html`<rect class="pbar__fill" width="${`${svgNum(value / max * 100, 0, 100)}%`}" height="8" rx="4"/>` : null}</svg><b>${value ?? "?"}</b></p>`;
161762
- }
161763
- function zeroTouchFixes(view) {
161764
- const m2 = model5(view);
161765
- const evidence = html`${m2.fixesPartial ? html`<p class="zt-note" role="status">Fix history is incomplete. Reload to retry; missing changes are not evidence that no fix was made.</p>` : null}${m2.fixes.length ? html`<ol class="zt-fxs">${m2.fixes.map((f3) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f3.at)} UTC${f3.undoneAt === null ? null : html` · Reverted ${date(f3.undoneAt)} UTC`}</p><p class="zt-fx__w">${f3.kind === "instructions" ? `Trigger ${f3.triggerIndex === null ? "not known" : f3.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f3.reason ? `${f3.reason} refusals` : "Questions in the targeted group"}, equal ${span(f3.spanMs)} spans before and after; ${f3.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f3.beforeCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1))}${beforeAfter("After", f3.afterCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1), fixTone(f3))}<p class="zt-kps">${fixObservation(f3)}</p>${acts3(view) && f3.kind === "instructions" && f3.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f3.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m2.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
161766
- const brief = m2.fixes.map((f3) => `${f3.reason ?? "grouped questions"}: ${f3.beforeCount ?? "?"} to ${f3.afterCount ?? "?"}`).join("; ");
161767
- return card({ title: "Fixes that worked", sub: "what changed, and what it did", body: html`<div class="zt-desktop-fixes">${evidence}</div><details class="zt-phone-fixes"><summary>${icon("done", { size: "sm" })}<span><b>${m2.fixesPartial ? "Fix history incomplete" : `${m2.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
161768
- }
161769
- var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model5, acts3, pct2, amount3, date, span, link, fail5, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
161770
- var init_zero_touch = __esm({
161771
- "src/console/panels/zero-touch.mjs"() {
161772
- init_common();
161773
- init_html();
161774
- init_kit();
161775
- init_zero_touch_view();
161776
- ZERO_TOUCH_WORDS = Object.freeze({ unavailable: "Zero touch could not be read. Reload to try again; missing history is never counted as zero touches.", empty: "No workload runs ended in the covered interval.", drafts: "Sent by a person, edited or as drafted. Each run still counts as a touch.", draftsPartial: "Partial history; this counts retained runs only.", viewer: "Questions it keeps asking are for operators. Rates, causes, streaks and fixes are shown here.", noGroups: "No question groups yet. Group retained questions with a capped decision job; name each group yourself.", noFixes: "No cause-linked fixes recorded yet. Start a change from a refusal or a question group to measure it here." });
161777
- ZERO_TOUCH_CAUSES = Object.freeze([
161778
- { cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
161779
- { cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
161780
- { cause: "rerun", label: "Failed or refused, then re-run", short: "Re-run", icon: "failed", tone: "failed", detail: "A person started a failed or refused run again." },
161781
- { cause: "config-fix", label: "Refused until config was fixed", short: "Config fixed", icon: "refused", tone: "refused", detail: "A confirmed configuration change explicitly targeted the refusal." },
161782
- { cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
161783
- { cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
161784
- ]);
161785
- ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
161786
- unassigned: "Not grouped yet.",
161787
- pending: "A grouping job is pending.",
161788
- assigned: "Assigned to a question group.",
161789
- "source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
161790
- "source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
161791
- "source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
161792
- "candidates-expired": "A group's representative context expired. Review the groups before trying again.",
161793
- "group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
161794
- stale: "The group revision changed during classification. Preview a fresh grouping job.",
161795
- refused: "The grouping job was refused. Open its run to see the policy reason.",
161796
- failed: "The grouping job failed. Open its run and review the failure before trying again."
161797
- });
161798
- causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
161799
- model5 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
161800
- acts3 = (v) => v?.role === "operator" && !v?.readOnly;
161801
- pct2 = (n3) => Number.isFinite(n3) ? `${Math.round(n3)}%` : "Not known";
161802
- amount3 = (n3) => Number.isFinite(n3) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n3) : "Not known";
161803
- date = (n3) => Number.isFinite(n3) && n3 >= 0 && n3 <= 864e13 ? new Date(n3).toISOString().slice(0, 10) : "not known";
161804
- span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount3(ms3 / 36e5)}h` : `${amount3(ms3 / 6e4)}m`;
161805
- link = (id7) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id7)}`}">${id7}</a>`;
161806
- fail5 = (m2) => m2.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
161807
- ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
161808
- ZERO_TOUCH_PANELS = Object.freeze([["p-zt-rate", zeroTouchRate], ["p-zt-streak", zeroTouchStreak], ["p-zt-wait", zeroTouchWait], ["p-zt-handled", zeroTouchHandled], ["p-zt-projects", zeroTouchProjects], ["p-zt-causes", zeroTouchCauses], ["p-zt-questions", zeroTouchQuestions], ["p-zt-fixes", zeroTouchFixes]]);
161809
- }
161810
- });
161811
-
161812
- // src/trigger-policy.mjs
161813
- function triggerWhoMayFire(trigger2) {
161814
- const on = trigger2.on ?? trigger2;
161815
- const forge = trigger2.run?.kind ?? trigger2.forge;
161816
- const kind = on.type;
161817
- if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
161818
- if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
161819
- if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
161820
- if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
161821
- if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
161822
- if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
161823
- const actions = Array.isArray(on.action) ? on.action : [];
161824
- const gates = [];
161825
- if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
161826
- if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
161827
- if (actions.includes("review_submitted")) gates.push("The reviewer's association must be OWNER, MEMBER or COLLABORATOR, regardless of the pull request author's association");
161828
- if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
161829
- return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
161830
- }
161831
- var init_trigger_policy = __esm({
161832
- "src/trigger-policy.mjs"() {
161833
- }
161834
- });
161835
-
161836
161705
  // src/trigger-gets-view.mjs
161837
161706
  function cannotSay(row2, trigger2, fileVersion) {
161838
161707
  if (fileVersion > 1) {
@@ -161868,17 +161737,28 @@ function agreeRow2(row2, trigger2) {
161868
161737
  skills: row2.skills ?? [],
161869
161738
  mcp: (row2.mcp ?? []).filter((server) => named2.includes(server.name) && server.enabled === true),
161870
161739
  packages: packagesLoaded(row2, trigger2),
161871
- 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 }))
161872
161743
  };
161873
161744
  }
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 };
161751
+ if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
161752
+ if (found.tier === "package" && trigger2.packages === false) return null;
161753
+ return { row: found };
161754
+ }
161874
161755
  function flowAnswer(row2, trigger2) {
161875
- const found = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow);
161756
+ const found = flowRowFor(row2, trigger2)?.row;
161876
161757
  if (!found) return null;
161877
- if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
161878
161758
  return found.tier === "package" ? `from ${found.package ?? "a staged package"}` : FLOW_TIER_WORDS[found.tier] ?? null;
161879
161759
  }
161880
161760
  function lacksFlow(row2, trigger2) {
161881
- 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;
161882
161762
  return found?.tier === "none" && found.repo === "absent";
161883
161763
  }
161884
161764
  function hostPills(rows3, trigger2, liveList, fileVersion) {
@@ -161971,8 +161851,13 @@ function flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }) {
161971
161851
  const choices = offered.filter((entry3) => !seen.has(entry3.name) && seen.add(entry3.name)).slice(0, FLOW_CHOICES_MAX);
161972
161852
  const groups = /* @__PURE__ */ new Map();
161973
161853
  const silent = [];
161854
+ const stale = [];
161974
161855
  for (const row2 of rows3) {
161975
161856
  if (trigger2.flow === null || row2.state !== "current") continue;
161857
+ if (flowRowFor(row2, trigger2)?.stale === true) {
161858
+ stale.push(row2.name);
161859
+ continue;
161860
+ }
161976
161861
  const word3 = flowAnswer(row2, trigger2);
161977
161862
  if (word3 === null) silent.push(row2.name);
161978
161863
  else groups.set(word3, [...groups.get(word3) ?? [], row2.name]);
@@ -161981,7 +161866,11 @@ function flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }) {
161981
161866
  const lacking = rows3.filter((row2) => row2.state === "current" && lacksFlow(row2, trigger2)).map((row2) => row2.name);
161982
161867
  const said = [
161983
161868
  ...[...groups].map(([word3, names2]) => `${listed(names2)} ${names2.length === 1 ? "finds" : "find"} ${trigger2.flow} ${word3}`),
161984
- 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,
161985
161874
  unknown3.length > 0 ? `${listed(unknown3)} ${unknown3.length === 1 ? "has" : "have"} no answer to read` : null
161986
161875
  ].filter(Boolean);
161987
161876
  return {
@@ -162014,7 +161903,7 @@ function overlayStem(segment) {
162014
161903
  for (let end = 1; end < segment.length; end += 1) if (overlayEntryNamed(segment.slice(0, end), segment)) return segment.slice(0, end);
162015
161904
  return segment;
162016
161905
  }
162017
- function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
161906
+ function extensionsRow2({ trigger: trigger2, rows: rows3, last, records = [], historyUnread = false, hostLoads = null }) {
162018
161907
  const known2 = rows3.filter((row2) => row2.state === "current");
162019
161908
  const inFile = trigger2.excludeExtensions ?? [];
162020
161909
  const withheld = trigger2.packages === false;
@@ -162030,10 +161919,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
162030
161919
  for (const value of naming.length > 0 ? naming : [overlayStem(segment)]) groupOf(value).segments.add(segment);
162031
161920
  }
162032
161921
  for (const name2 of inFile) groupOf(name2);
162033
- const brought = (name2, version = null) => {
162034
- const row2 = (last?.sources ?? []).find((source2) => source2.name === name2 && (version === null || source2.version === version));
162035
- const words = broughtWords(row2);
162036
- return words === null ? null : `${words}, as ${last.id} loaded it`;
161922
+ const said = (Array.isArray(records) ? records : []).map((record3) => triggerLastRun([record3], hostLoads).quoted).filter(Boolean);
161923
+ const brought = (name2, { kind, version = null }) => {
161924
+ for (const run3 of [last, ...said].filter(Boolean)) {
161925
+ const row2 = (run3.sources ?? []).find((source2) => source2.kind === kind && source2.name === name2 && (version === null || source2.version === version));
161926
+ if (row2) return `${broughtWords(row2) ?? "no tool, skill or prompt template"}, as ${run3.id} loaded it`;
161927
+ }
161928
+ return null;
162037
161929
  };
162038
161930
  const items = [...groups.values()].map(({ value, versions: versions2, segments }) => {
162039
161931
  const on = !inFile.includes(value);
@@ -162045,12 +161937,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
162045
161937
  const off = known2.filter((row2) => holdsEntry(row2) && row2.overlayExtensionsAllowed !== true).map((row2) => row2.name);
162046
161938
  const hostsOn = (version) => known2.filter((row2) => (row2.packages ?? []).some((entry3) => entry3.name === value && entry3.version === version)).map((row2) => row2.name);
162047
161939
  const stagedWhere = staged.length === 0 ? null : whereWords(rows3, "packages", holdsPackage, "staged", staged.length === 1 ? null : staged.map((version) => `${version} on ${listed(hostsOn(version))}`).join(", "));
161940
+ const fromEntries = () => entries.map((segment) => brought(segment, { kind: "overlay" })).find((words) => words !== null) ?? null;
162048
161941
  const quoted = (() => {
162049
- if (!on || withheld && entries.length === 0) return null;
162050
- if (staged.length === 0) return brought(entries[0]);
162051
- const running = staged.find((version) => brought(value, version) !== null);
162052
- if (running === void 0) return entries.length > 0 ? brought(entries[0]) : null;
162053
- return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, running)}`;
161942
+ if (withheld && entries.length === 0) return null;
161943
+ if (staged.length === 0 || withheld) return fromEntries();
161944
+ const running = staged.find((version) => brought(value, { kind: "package", version }) !== null);
161945
+ if (running === void 0) return fromEntries();
161946
+ return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, { kind: "package", version: running })}`;
162054
161947
  })();
162055
161948
  const also = inFile.includes(value) ? alsoNaming(inFile, value) : [];
162056
161949
  const named2 = staged.length > 0 ? `${value} ${listed(staged)}` : entries.length > 0 && also.length === 0 ? entries.join(", ") : value;
@@ -162060,7 +161953,9 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
162060
161953
  entries.length > 0 ? whereWords(rows3, "extensions", holdsEntry, "in the overlay") : null,
162061
161954
  off.length > 0 ? `its overlay extensions off on ${listed(off)}` : null,
162062
161955
  also.length > 0 ? `the file also names ${listed(also)}` : null,
162063
- quoted
161956
+ quoted,
161957
+ // Not said where the history could not be read: the card says so once, and a failed read is not "no run".
161958
+ quoted === null && !historyUnread && !(withheld && entries.length === 0) && (staged.length > 0 || entries.length > 0) ? staged.length > 0 && !withheld ? TRIGGER_GETS_WORDS.bringsUnknown : TRIGGER_GETS_WORDS.overlayBringsUnknown : null
162064
161959
  ].filter(Boolean).join(" \xB7 ");
162065
161960
  return { value, name: named2, on, moot, sub: sub2, backOn: also.length > 0 ? `${value} off the list` : `${value} back on` };
162066
161961
  });
@@ -162163,14 +162058,69 @@ function triggerGetsOf({ trigger: trigger2, records = [], historyUnread = false,
162163
162058
  last: { Model: under("Model"), Skills: under("Skills"), Extensions: under("Extensions"), MCP: under("MCP"), Tools: under("Tools"), pi: under("pi") },
162164
162059
  model: modelRow2({ trigger: trigger2, defaults: defaults4, modelChoices }),
162165
162060
  flow: { ...flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }), ...watch ? { doctor: null, lacking: null } : {} },
162166
- extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted }),
162061
+ extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted, records, historyUnread, hostLoads }),
162167
162062
  mcp: mcpRow2({ trigger: trigger2, rows: rows3, overlayServers }),
162168
162063
  tools: toolsRow2({ trigger: trigger2, last: quoted }),
162169
162064
  image: imageRow({ trigger: trigger2, rows: rows3 }),
162170
162065
  secrets: { profile: trigger2.secretsProfile ?? null, count: trigger2.secrets ?? 0, optedOut: trigger2.secretBundle === false }
162171
162066
  };
162172
162067
  }
162173
- var TRIGGER_GETS_WORDS, code4, run2, time2, unique, overlayServersOf, packagesLoaded, overlayLoaded, serverDefinition, FLOW_TIER_WORDS, MODEL_CHOICES_MAX, PROVIDER_CHOICES_MAX, FLOW_CHOICES_MAX, listsWhole, plural7, alsoNaming;
162068
+ function freshFlowWords(row2, trigger2) {
162069
+ if (trigger2.flow === null) return SANDBOX_FRESH_WORDS.noFlow;
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}.`;
162073
+ const word3 = flowAnswer(row2, trigger2);
162074
+ if (word3 === null) return `Whether ${who} would find ${trigger2.flow} is unknown: its last doctor run has no answer for it.`;
162075
+ const tier = at2.row.tier;
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.`;
162077
+ if (tier === "repository") return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}.`;
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.`;
162079
+ }
162080
+ function freshMcpWords(row2, named2) {
162081
+ if (!Array.isArray(named2) || named2.length === 0) return null;
162082
+ const on = [];
162083
+ const off = [];
162084
+ const lacking = [];
162085
+ const unknown3 = [];
162086
+ let forge = false;
162087
+ for (const name2 of named2) {
162088
+ if (name2 === FORGE_MCP_SERVER_NAME) {
162089
+ forge = true;
162090
+ continue;
162091
+ }
162092
+ const server = (row2.mcp ?? []).find((entry3) => entry3.name === name2);
162093
+ if (server) (server.enabled === true ? on : off).push(name2);
162094
+ else (freshListsWhole(row2, "mcp") ? lacking : unknown3).push(name2);
162095
+ }
162096
+ const refused3 = [off.length > 0 ? `${listed(off)} switched off here` : null, lacking.length > 0 ? `${listed(lacking)} not on this host` : null].filter(Boolean);
162097
+ const said = [
162098
+ on.length > 0 ? `${listed(on)} on this host` : null,
162099
+ // The reason is not named: a host with no mcp.json publishes the same empty list, and its jobs are refused as
162100
+ // mcp-config-invalid rather than mcp-server-unknown (`serverRefused`). Either way, before it spends.
162101
+ refused3.length > 0 ? `${refused3.join(" and ")}, so a job there is refused before it spends` : null,
162102
+ unknown3.length > 0 ? `${listed(unknown3)} unknown here` : null,
162103
+ forge ? `${FORGE_MCP_SERVER_NAME}: ${TRIGGER_GETS_WORDS.forgeServer}` : null
162104
+ ].filter(Boolean);
162105
+ return `${said.join("; ")}.`;
162106
+ }
162107
+ function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, flowBasis = null, mcp = [], withholdsPackages = false, excludes = [], image = null }) {
162108
+ const rows3 = [[`On ${host}`, sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages, excludes, image, mcp })]];
162109
+ const row2 = hostLoadsRow(hostLoads, host);
162110
+ if (row2.state === "unknown") return rows3;
162111
+ const named2 = typeof flow === "string" && flow !== "" ? flow : null;
162112
+ if (row2.state !== "current") {
162113
+ rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [named2 === null ? SANDBOX_FRESH_WORDS.noFlow : `Whether a job of trigger ${triggerIndex + 1} on this host would find ${named2} is unknown: its last doctor run is old.`]]);
162114
+ const listedNames = (mcp ?? []).filter((name2) => typeof name2 === "string");
162115
+ if (listedNames.length > 0) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [`${listed(listedNames)} unknown here: its last doctor run is old.`]]);
162116
+ return rows3;
162117
+ }
162118
+ rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [freshFlowWords(row2, { index: triggerIndex, flow: named2, excludeExtensions: excludes, packages: withholdsPackages ? false : void 0, flowBasis })]]);
162119
+ const mcpWords = freshMcpWords(row2, mcp);
162120
+ if (mcpWords !== null) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [mcpWords]]);
162121
+ return rows3;
162122
+ }
162123
+ var TRIGGER_GETS_WORDS, code4, run2, time2, unique, overlayServersOf, packagesLoaded, overlayLoaded, serverDefinition, FLOW_TIER_WORDS, MODEL_CHOICES_MAX, PROVIDER_CHOICES_MAX, FLOW_CHOICES_MAX, listsWhole, plural7, alsoNaming, SANDBOX_FRESH_WORDS, freshListsWhole;
162174
162124
  var init_trigger_gets_view = __esm({
162175
162125
  "src/trigger-gets-view.mjs"() {
162176
162126
  init_mcp_config();
@@ -162206,6 +162156,8 @@ var init_trigger_gets_view = __esm({
162206
162156
  noExtensions: "The hosts that can say name no staged package or overlay extension.",
162207
162157
  noExtensionsKnown: "No host\u2019s doctor run can say which extensions it has.",
162208
162158
  forgeServer: "the job\u2019s own forge, with its own token; in no overlay",
162159
+ bringsUnknown: "what it brings is known once a run has loaded it",
162160
+ overlayBringsUnknown: "an overlay entry has no manifest, so what it brings is known once a run has loaded it",
162209
162161
  noServers: "The hosts that can say name no MCP server, and this trigger names none.",
162210
162162
  noServersKnown: "No host\u2019s doctor run can say which MCP servers it has, and this trigger names none.",
162211
162163
  serverRefused: "A job on a host without a server it names, or with it off, is refused as mcp-server-unknown, before it spends; a host with no overlay or no mcp.json refuses it as mcp-config-invalid. A key a server uses must reach the job, through run.secrets in the file or a host\u2019s PI_FORWARD_ENV, or the job is refused as mcp-secret-unmapped.",
@@ -162220,6 +162172,7 @@ var init_trigger_gets_view = __esm({
162220
162172
  viewer: "An operator changes these on this page, with one preview and one Confirm.",
162221
162173
  noTool: "No tool reaches this action; dispatch_trigger_edit keeps today\u2019s scope.",
162222
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",
162223
162176
  reloadDrops: "A Confirm elsewhere on this page reloads it, and drops any change not yet written in What its jobs get.",
162224
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.",
162225
162178
  pillOrder: "differs in the order or kind of what it loads",
@@ -162251,6 +162204,370 @@ var init_trigger_gets_view = __esm({
162251
162204
  listsWhole = (row2, part) => row2.state === "current" && !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
162252
162205
  plural7 = (n3, one, many = `${one}s`) => `${n3} ${n3 === 1 ? one : many}`;
162253
162206
  alsoNaming = (list5, name2) => list5.filter((other) => other !== name2 && (overlayEntryNamed(other, name2) || overlayEntryNamed(name2, other)));
162207
+ SANDBOX_FRESH_WORDS = Object.freeze({
162208
+ noFlow: "This trigger names no flow.",
162209
+ flowStale: "its last doctor run answered for another version of the triggers file",
162210
+ flowLabel: "Flow",
162211
+ mcpLabel: "MCP servers"
162212
+ });
162213
+ freshListsWhole = (row2, part) => !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
162214
+ }
162215
+ });
162216
+
162217
+ // src/console/panels/sandboxes.mjs
162218
+ function state2(v) {
162219
+ const s2 = report(v)?.request;
162220
+ if (!s2) return null;
162221
+ const success = s2.state === "ready";
162222
+ return banner({ status: success ? "ok" : ["refused", "failed"].includes(s2.state) ? "attention" : "running", title: success ? `Sandbox ${s2.result?.id ?? s2.id} is ready` : `Sandbox request ${s2.state}`, body: html`<span class="code">${s2.id}</span> on ${s2.host ?? "its host"}. ${s2.message ?? s2.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s2.result ? links(v, { ...s2.result, id: s2.result.id, host: s2.result.host ?? s2.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s2.id, host: s2.host ?? selected3(v).host ?? "" })}`, label: "Refresh request" }) });
162223
+ }
162224
+ function sandboxFromRun(v) {
162225
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162226
+ const all = rows(v);
162227
+ const id7 = v.query?.id;
162228
+ const picked = all.find((r2) => rowId2(r2) === id7 && (!v.query?.host || r2.host === v.query.host));
162229
+ const pickerHost = v.query?.host ?? selected3(v).host ?? all[0]?.host ?? "";
162230
+ const picker = html`<form method="get" action="/runs/sandboxes" data-live-hold class="sx-picker">${field2({ label: "Workspace host", id: "sb-workspace-host", control: select({ id: "sb-workspace-host", name: "host", value: pickerHost, options: [{ value: "", label: "Choose a host" }, ...(report(v)?.hosts ?? []).map((h2) => ({ value: h2.name, label: h2.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id7 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r2) => !pickerHost || r2.host === pickerHost).map((r2) => ({ value: rowId2(r2), label: `${rowId2(r2)} on ${r2.host}${r2.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
162231
+ if (!report(v)) return card3("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
162232
+ if (!picked) return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<p class="card__sub">${all.length ? "Choose a kept workspace, or open Sandboxes from a run page." : SANDBOX_WORDS.empty}</p>`, "the image, workspace and isolation the run kept");
162233
+ const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
162234
+ const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
162235
+ const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId2(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
162236
+ const what2 = facts([["Image", picked.image ?? "As recorded in its kept manifest"], ["/workspace", workspace], ["/job", "Read only, with the retained job inputs"], ["pi and what it loads", wordsHtml(loads)], ["Processes", "None until you start them"], ["Credentials", "None, on purpose"], ["Network", "The host's current job egress policy"]], { cols: 1 });
162237
+ return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<div class="sx-run">${tile("ok", { icon: "box" })}<div><p class="sx-run__t"><span class="code">${rowId2(picked)}</span>${picked.project ? ptag(picked.project) : null}${picked.prepared ? chip("Without a run") : null}</p><p class="sx-run__s">${picked.host} ${vtag(picked.v ?? picked.backend)}. ${retention(picked, v.now)}</p></div></div><h3 class="sx-h3">What opens</h3>${what2}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.sandboxFoot}</span></p><h3 class="sx-h3">How it opens</h3>${canOpen(picked) ? html`<div class="sx-ch sx-ch--on"><div class="sx-ch__h">${icon("terminal", { size: "sm" })}<b>In your terminal</b><span class="sx-rec sandbox-opener">recommended</span></div><p class="sx-ch__s">Your terminal opens with the command typed in and waits for Enter. The link holds no secret. On a phone, copy the command.</p>${links(v, picked)}</div><div class="sx-foot"><p>${icon("info", { size: "sm" })}<span>The shell uses a job's isolation and network. A published host port is refused while egress is on. Pinning uses this host's configured pin window.</span></p>${pinForm(v, picked)}</div>` : html`<p>This venue has no sandbox shell. Its workspace cannot be reopened here.</p>`}`, "the same image, workspace and isolation as the run");
162238
+ }
162239
+ function sandboxNew(v) {
162240
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162241
+ const s2 = selected3(v), data5 = report(v), options = data5?.options ?? [];
162242
+ const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
162243
+ const triggers2 = options;
162244
+ const hostOptions = (data5?.hosts ?? []).filter((h2) => !h2.unreadable && !h2.stale && h2.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h2.name))).map((h2) => ({ value: h2.name, label: `${h2.name}, ${h2.settings.defaultBackend ?? "venue not published"}` }));
162245
+ const fields3 = [
162246
+ field2({ label: "Repository", id: "sb-repo", control: input({ id: "sb-repo", name: "repo", value: s2.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
162247
+ field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s2.index === void 0 ? "" : String(s2.index), options: [{ value: "", label: "Choose a trigger" }, ...triggers2.map((o) => ({ value: String(o.index), label: o.label }))] }), hint: "Its image, size and egress are shown below, rather than chosen here." }),
162248
+ field2({ label: "Start from", id: "sb-ref", control: input({ id: "sb-ref", name: "ref", value: s2.ref ?? "HEAD", code: true, required: true, autocomplete: "off", spellcheck: false }), hint: "HEAD means the default branch. Enter a branch, tag, full commit SHA, or pr:42 for a pull request." }),
162249
+ field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s2.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
162250
+ field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s2.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
162251
+ ];
162252
+ const plan = data5?.plan;
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;
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>`;
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;
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>`;
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");
162258
+ }
162259
+ function sandboxOpen(v) {
162260
+ if (!allowed3(v)) return null;
162261
+ const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s2) => !v.query?.gatewaySelect || s2.host === v.query.host);
162262
+ const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
162263
+ const notices = (data5?.notices ?? []).filter((n3) => !v.query?.gatewaySelect || n3.host === v.query.host).map((n3) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n3.host}, ${n3.id}: ${{ "network-kept": "The sandbox network was kept because cleanup could not be confirmed. Inspect this host\u2019s runtime and network before removing either.", "network-cleanup-failed": "Sandbox network cleanup failed. Inspect this host\u2019s runtime and egress network before retrying cleanup.", "runtime-unreadable": "The runtime could not be read. An expired shell registration does not prove the container stopped. Inspect this host before reopening or cleaning it." }[n3.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n3.host })}`}">Inspect host</a>.</p>`);
162264
+ if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card3("Open now", html`<p role="status">The browser gateway is not enabled for ${v.query.host}, so there is no shell to choose here. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>`);
162265
+ return card3("Open now", html`${v.query?.gatewaySelect ? html`<p class="sx-p">Choose an already-open shell on ${v.query.host}. Each browser opening needs your current operator token. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>` : null}${notices}${data5?.noticesUnavailable ? html`<p role="status">Some sandbox cleanup notices could not be read. Inspect the host if a terminal ended unexpectedly.</p>` : null}${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? html`<p role="status">The registered shell list is incomplete: at least ${data5.sessionsMore ?? 0} more and ${data5.sessionsUnreadable ?? 0} unreadable registrations.</p>` : null}${data5?.sessionsUnavailable ? html`<p role="status">Open shells could not be read. Reload to try again.</p>` : sessions.length ? html`<ul class="sx-opens">${sessions.map((s2) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s2.id}</span>${vtag(s2.venue)}</p><p class="sx-open__s">${s2.host}, opened by ${s2.by ?? "the terminal operator"}. Open since ${stamp(s2.since)}.</p>${sandboxDriverLine(v, s2)}<div class="sx-open__a">${links(v, s2, "attach", true)}${sandboxBrowserLink(v, s2)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s2.host], ["id", s2.id], ["nonce", s2.nonce], ["containerId", s2.containerId]], label: "Preview End", kind: ["sm", "danger"], icon: "cancel" }) : null}</div></div></li>`)}</ul>` : html`<p>${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? "No readable shell is in this bounded view. The incomplete registrations above prevent an exact empty count." : SANDBOX_WORDS.noShells}</p>`}<p class="sx-small">${icon("info", { size: "sm" })}<span>One shell per workspace at a time. End asks the terminal that opened it to close the shell and keeps the workspace. What you type is not recorded.</span></p>`, null, badge(incomplete ? "attention" : "running", `${sessions.length} ${incomplete ? "shown" : "open"}`));
162266
+ }
162267
+ function sandboxKept(v) {
162268
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162269
+ const data5 = report(v), all = rows(v);
162270
+ return card3("Kept workspaces", html`${data5?.more > 0 ? html`<p role="status">At least ${data5.more} more hosts have inventory beyond this bounded view. Inspect the fleet before treating this list as complete.</p>` : null}${!data5 ? html`<p role="status">${SANDBOX_WORDS.unavailable}</p>` : html`${(data5.hosts ?? []).filter((h2) => h2.stale || h2.unreadable).map((h2) => html`<p class="sx-p" role="status">${h2.name}: ${h2.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r2) => html`<li class="sx-kept${canOpen(r2) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId2(r2), host: r2.host })}`}">${rowId2(r2)}</a>${r2.project ? ptag(r2.project) : null}${r2.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r2.host} ${vtag(r2.v ?? r2.backend)}. ${retention(r2, v.now)}${!canOpen(r2) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r2) ? html`<div class="sx-kept__actions">${links(v, r2, "open", true)}${pinForm(v, r2)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h2) => h2.more > 0).map((h2) => html`<p class="sx-p">${h2.name} has at least ${h2.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
162271
+ }
162272
+ function sandboxBrowser(v) {
162273
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162274
+ const gateway = v.data?.browserGateways;
162275
+ const count7 = gateway?.enabledHosts?.length ?? 0;
162276
+ return card3("In this browser", html`<p class="sx-p">${SANDBOX_WORDS.browser}</p>${gateway?.problem ? html`<p class="sx-p" role="status">${gateway.problem}</p>` : html`<p class="sx-p">${count7 ? `${count7} ${count7 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count7 ? html`<span class="sandbox-opener"> Choose Open in browser beside a registered shell in Open now.</span>` : null}</p>`}<p class="sx-p">Each opening needs your current operator token again. Only one browser window attaches at a time. Closing it keeps the original sandbox; what you type is not recorded.</p><p class="sx-p">First open a kept or prepared workspace in your own terminal. The browser never starts a container.</p><p class="sx-p">Browser terminals require a window wider than 760 pixels. On a phone, copy the command for your own terminal.</p>${v.readOnly ? html`<p class="sx-p">This console is read-only, so browser attachment is unavailable.</p>` : null}`, "optional, separate gateway", chip(count7 ? "Enabled per host" : "Off by default", { status: count7 ? "ok" : "idle", icon: "shield" }));
162277
+ }
162278
+ var SANDBOX_WORDS, report, allowed3, acts2, rows, card3, stamp, rowId2, canOpen, selected3, facts5, links, retention, pinForm, SANDBOX_PANELS;
162279
+ var init_sandboxes = __esm({
162280
+ "src/console/panels/sandboxes.mjs"() {
162281
+ init_html();
162282
+ init_kit();
162283
+ init_forges();
162284
+ init_sandbox_links();
162285
+ init_common();
162286
+ init_loaded_words();
162287
+ init_loaded_parts();
162288
+ init_trigger_gets_view();
162289
+ SANDBOX_WORDS = Object.freeze({
162290
+ unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
162291
+ empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
162292
+ noShells: "No live sandbox shell was registered. A shell whose terminal stopped answering disappears when its short lease expires. An expired registration is not proof the container stopped; inspect its host if the terminal ended unexpectedly.",
162293
+ browser: "A separate gateway can attach to an already-open sandbox. It is off by default and enabled by hand per host. Open in Terminal and copy-command support remain available.",
162294
+ noScript: "Preparing, pinning, ending a shell and saving terminal targets require JavaScript for Preview and Confirm. Commands remain selectable and the settings form works without JavaScript."
162295
+ });
162296
+ report = (v) => v.data?.sandboxes;
162297
+ allowed3 = (v) => v.role === "operator";
162298
+ acts2 = (v) => allowed3(v) && !v.readOnly;
162299
+ rows = (v) => (report(v)?.hosts ?? []).flatMap((h2) => (h2.rows ?? []).map((r2) => ({ ...r2, host: h2.name, hostStale: h2.stale, retentionHours: h2.retentionHours })));
162300
+ card3 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
162301
+ stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
162302
+ rowId2 = (r2) => r2.id ?? r2.jobId;
162303
+ canOpen = (r2) => !String(rowId2(r2)).startsWith("sha256:") && r2.canOpen !== false && ["local", "podman"].includes(r2.v ?? r2.backend);
162304
+ selected3 = (v) => report(v)?.selected ?? {};
162305
+ facts5 = (plan) => facts([
162306
+ ["Image", plan.image ?? "not published"],
162307
+ ["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
162308
+ ["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
162309
+ ["Credentials", "None in the shell"]
162310
+ ]);
162311
+ links = (v, r2, verb = "open", compact2 = false) => sandboxLinks({ id: rowId2(r2), host: r2.host, verb, compact: compact2, config: v.data?.sandboxHosts });
162312
+ retention = (r2, now) => html`${r2.pin ? "Pinned" : "Kept"}${Number.isFinite(r2.until) ? html` until ${stamp(r2.until)}${r2.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r2.until - now)} left`}` : ": deadline not published"}`;
162313
+ pinForm = (v, r2) => acts2(v) && canOpen(r2) ? actForm({ action: "sandbox-pin", hidden: [["host", r2.host], ["id", rowId2(r2)]], label: "Preview pin", icon: "clock" }) : null;
162314
+ SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
162315
+ }
162316
+ });
162317
+
162318
+ // src/zero-touch-view.mjs
162319
+ function breakdown(row2, repository = false) {
162320
+ const out = { project: text3(row2?.project ?? row2?.id, 64), scopeId: scope(row2?.scopeId), name: text3(row2?.name ?? row2?.target ?? row2?.project ?? row2?.id) ?? "Unassigned", target: text3(row2?.target), runs: count5(row2?.runs), touched: count5(row2?.touched), rate: number3(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number3(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number3(row2?.streakDays), zeroTouch: row2?.zeroTouch === true && row2?.waitingPartial !== true, waitingNow: row2?.waitingPartial === true ? null : count5(row2?.waitingNow ?? row2?.waiting), waitingPartial: row2?.waitingPartial === true, topCause: CAUSES.includes(row2?.topCause) ? row2.topCause : null, causes: causes(row2?.causes), touchedRunIds: runIds(row2?.touchedRunIds) };
162321
+ if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r2) => breakdown(r2, true)) : [];
162322
+ return out;
162323
+ }
162324
+ function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
162325
+ const observed = raw?.coverage ?? raw?.window;
162326
+ const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number3(observed.fromMs) !== null && observed.fromMs >= (number3(raw?.nowMs) ?? nowMs2);
162327
+ if (!raw || typeof raw !== "object" || raw.state === "unavailable" || unobserved2) return { state: "unavailable", nowMs: nowMs2, windowDays, coverage: { partial: true, reasons: [] }, totals: {}, projects: [], causes: causes([]), trend: [], groups: [], fixes: [], fixesPartial: true, touchedRunIds: [], waitingRuns: [], ungroupedRunIds: [], grouping: { enabled: false, pending: 0, unassigned: 0, state: "disabled", sources: [] } };
162328
+ const totals = raw.totals ?? raw.summary ?? {};
162329
+ const coverage2 = raw.coverage ?? raw.window ?? {};
162330
+ const out = {
162331
+ state: "ready",
162332
+ nowMs: number3(raw.nowMs) ?? nowMs2,
162333
+ windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
162334
+ coverage: { requestedFromMs: number3(coverage2.requestedFromMs), fromMs: number3(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
162335
+ totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number3(totals.rate), previousRate: number3(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number3(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number3(totals.medianWaitMs) ?? (number3(totals.medianWaitHours) === null ? null : totals.medianWaitHours * 36e5), pending: totals.waitingPartial === true ? null : count5(totals.pending ?? totals.waitingNow), draftAnswers: count5(totals.draftAnswers), deltaPoints: Number.isFinite(totals.deltaPoints) ? totals.deltaPoints : null },
162336
+ projects: Array.isArray(raw.projects) ? raw.projects.map((r2) => breakdown(r2)) : [],
162337
+ causes: causes(raw.causes),
162338
+ trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r2) => ({ atMs: number3(r2?.atMs ?? r2?.at), rate: number3(r2?.rate), runs: count5(r2?.runs), touched: count5(r2?.touched), partial: r2?.partial === true })) : [],
162339
+ waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r2) => ({ runId: id5(r2?.runId ?? r2?.jobId), parkedAt: number3(r2?.parkedAt) })).filter((r2) => r2.runId && r2.parkedAt !== null) : [],
162340
+ touchedRunIds: runIds(raw.touchedRunIds),
162341
+ ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
162342
+ groups: [],
162343
+ fixes: [],
162344
+ fixesPartial: raw.fixesPartial === true,
162345
+ grouping: { enabled: raw.grouping?.enabled === true, partial: raw.grouping?.partial === true, pending: count5(raw.grouping?.pending), unassigned: count5(raw.grouping?.unassigned), state: ["ready", "ok", "disabled", "pending", "partial", "failed", "configuration-unavailable", "unavailable"].includes(raw.grouping?.state) ? raw.grouping.state : "disabled", sources: role === "operator" && Array.isArray(raw.grouping?.sources ?? raw.groupingStatuses) ? (raw.grouping?.sources ?? raw.groupingStatuses).map((r2) => ({ runId: id5(r2?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r2?.status) ? r2.status : "source-unavailable" })).filter((r2) => r2.runId) : [] }
162346
+ };
162347
+ out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f3) => ({ id: id5(f3?.id), at: number3(f3?.at), undoneAt: number3(f3?.undoneAt), kind: ["instructions", "config"].includes(f3?.kind) ? f3.kind : "config", groupId: id5(f3?.groupId), reason: typeof f3?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f3.reason) ? f3.reason : null, triggerIndex: Number.isSafeInteger(f3?.triggerIndex) && f3.triggerIndex >= 0 ? f3.triggerIndex : null, beforeCount: number3(f3?.beforeCount), afterCount: number3(f3?.afterCount), spanMs: number3(f3?.spanMs), partial: f3?.partial !== false, subsequentRuns: count5(f3?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f3?.status) ? f3.status : "observing" })).filter((f3) => f3.id) : [];
162348
+ if (role === "operator") out.groups = Array.isArray(raw.groups) ? raw.groups.map((g) => ({ id: id5(g?.id), name: text3(g?.name, 120), scopeId: scope(g?.scopeId), repo: text3(g?.repo), project: text3(g?.project, 64), count: count5(g?.count), runIds: runIds(g?.runIds), status: ["open", "fixed", "unassigned"].includes(g?.status) ? g.status : "open", fixedAt: number3(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
162349
+ return out;
162350
+ }
162351
+ var CAUSES, id5, number3, count5, text3, scope, causes, runIds;
162352
+ var init_zero_touch_view = __esm({
162353
+ "src/zero-touch-view.mjs"() {
162354
+ CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
162355
+ id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
162356
+ number3 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
162357
+ count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
162358
+ text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
162359
+ scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
162360
+ causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
162361
+ runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
162362
+ }
162363
+ });
162364
+
162365
+ // src/console/panels/zero-touch.mjs
162366
+ function delta(n3, previousRate = null) {
162367
+ if (!Number.isFinite(n3)) return html`<span class="zt-tr">Previous interval not covered</span>`;
162368
+ return html`<span class="${`zt-tr${n3 < 0 ? " tone-ok" : n3 > 0 ? " tone-attention" : ""}`}">${icon(n3 < 0 ? "chev" : n3 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n3 === 0 ? "no change" : `${n3 < 0 ? "down" : "up"} ${amount3(Math.abs(n3))} points`}</b>${previousRate === null ? null : html`<small>from ${pct2(previousRate)}</small>`}</span>`;
162369
+ }
162370
+ function coverage(m2) {
162371
+ return html`<p class="zt-coverage" role="status">${m2.coverage.partial ? html`<b>Partial ${m2.windowDays}-day window.</b> Covered from ${date(m2.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m2.windowDays}-day window.`}${m2.coverage.reasons.length ? html` ${m2.coverage.reasons.join("; ")}.` : null}</p>`;
162372
+ }
162373
+ function spark(m2) {
162374
+ const rows3 = m2.trend;
162375
+ if (!rows3.some((r2) => Number.isFinite(r2.rate) && !r2.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
162376
+ const top = Math.max(32, ...rows3.map((r2) => r2.rate ?? 0));
162377
+ const x2 = (i2) => rows3.length === 1 ? 200 : i2 * 200 / (rows3.length - 1);
162378
+ const y = (n3) => 56 - n3 / top * 56;
162379
+ const segments = [];
162380
+ let points = [];
162381
+ for (const [i2, row2] of rows3.entries()) {
162382
+ if (row2.rate === null || row2.partial) {
162383
+ if (points.length) segments.push(points);
162384
+ points = [];
162385
+ } else points.push(`${svgNum(x2(i2), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
162386
+ }
162387
+ if (points.length) segments.push(points);
162388
+ const last = rows3[rows3.length - 1];
162389
+ const label3 = `Rolling ${m2.windowDays}-day touch rate at weekly intervals: ${rows3.map((r2) => r2.partial || r2.rate === null ? "not covered" : pct2(r2.rate)).join(", ")}. Goal under 5 percent.`;
162390
+ return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label3}"><line class="zt-spark__goal" x1="0" x2="200" y1="${svgNum(y(5), 0, 56)}" y2="${svgNum(y(5), 0, 56)}"/>${segments.map((p) => html`<polyline class="zt-spark__l" points="${p.join(" ")}"/>`)}${last?.rate !== null && !last?.partial ? html`<circle class="zt-spark__d" cx="200" cy="${svgNum(y(last.rate), 0, 56)}" r="3.5"/>` : null}</svg><p class="zt-spark__ax"><span>${date(rows3[0]?.atMs)}</span><span>goal, dashed</span><span>now</span></p><p class="sr">${label3}</p>`;
162391
+ }
162392
+ function longest(m2) {
162393
+ return [...m2.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
162394
+ }
162395
+ function waitingHistory(m2) {
162396
+ return m2.totals.waitingPartial ? html`<p class="zt-note" role="status">Waiting history is incomplete. An older park predates retained answer history; its current state and wait time are not known.</p>` : null;
162397
+ }
162398
+ function compact(m2) {
162399
+ const best = longest(m2);
162400
+ return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h, median ${span(m2.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m2.totals.draftAnswers} ${m2.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m2.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m2.totals.waitingPartial ? "Not known" : `${m2.totals.pending} ${m2.totals.pending === 1 ? "run" : "runs"}`}</b>${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</li></ul>${waitingHistory(m2)}`;
162401
+ }
162402
+ function zeroTouchRate(view) {
162403
+ const m2 = model5(view);
162404
+ return card({ title: `Touch rate, ${m2.windowDays} days`, body: html`${fail5(m2)}${m2.state === "ready" ? html`${kpi(m2.totals.runs ? pct2(m2.totals.rate) : "No runs", `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`)}${spark(m2)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m2.totals.deltaPoints, m2.totals.previousRate)}</p>${coverage(m2)}<p class="zt-kps">${m2.totals.stopped} stopped before they ran, outside the rate.${m2.totals.stoppedUnattributed > 0 ? html` ${m2.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m2)}</div>` : null}` });
162405
+ }
162406
+ function zeroTouchStreak(view) {
162407
+ const m2 = model5(view), best = longest(m2);
162408
+ return card({ title: "Zero-touch streak", body: html`${fail5(m2)}${kpi(best ? `${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history", best ? `${best.name}, the longest now` : "30 days of observed activity establish a zero-touch project")}<p class="zt-kp">${m2.projects.filter((p) => p !== best).sort((a, b) => (b.streakDays ?? -1) - (a.streakDays ?? -1)).map((p) => p.waitingNow ? chip(`${p.name} waiting now`, { status: "parked", icon: "parked" }) : html`<span class="zt-kt">${p.name} ${p.streakDays === null ? "not known" : `${amount3(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
162409
+ }
162410
+ function zeroTouchWait(view) {
162411
+ const m2 = model5(view);
162412
+ if (m2.state !== "ready") return card({ title: "Waiting for a person", body: fail5(m2) });
162413
+ return card({ title: "Waiting for a person", body: html`${fail5(m2)}${kpi(m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h`, `runs waited on a person in ${m2.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m2.totals.medianWaitMs)}</b></p>${waitingHistory(m2)}<p class="zt-kp">${chip(m2.totals.waitingPartial ? "Pending count not known" : `${m2.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</p>` });
162414
+ }
162415
+ function zeroTouchHandled(view) {
162416
+ const m2 = model5(view);
162417
+ return card({ title: "Answered from a draft", body: m2.state !== "ready" ? fail5(m2) : html`${kpi(m2.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m2.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
162418
+ }
162419
+ function meter3(row2) {
162420
+ if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
162421
+ const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
162422
+ const word3 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
162423
+ return html`<span class="zt-m"><svg class="${`pbar tone-${tone}`}" height="10" role="meter" aria-valuenow="${svgNum(row2.rate, 0, 100)}" aria-valuemin="0" aria-valuemax="100" aria-label="${`${row2.name}: ${pct2(row2.rate)} of runs needed a person, goal under 5 percent; meter scale 40 percent`}"><rect class="pbar__bed" width="100%" height="10" rx="5"/><rect class="pbar__fill" width="${`${svgNum(row2.rate / 40 * 100, 0, 100)}%`}" height="10" rx="5"/><line class="zt-goal" x1="12.5%" x2="12.5%" y1="-4" y2="14"/></svg><span class="zt-mv"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span></span>`;
162424
+ }
162425
+ function rowContent(row2, sub2 = false) {
162426
+ const c = causesByKey.get(row2.topCause);
162427
+ return html`<span class="zt-chev">${sub2 ? null : icon("chev-r", { size: "sm" })}</span><span class="zt-name"><b>${row2.name}</b><small>${sub2 ? `${row2.runs} runs` : row2.repositories.map((r2) => r2.name).join(" and ")}</small></span>${meter3(row2)}${delta(row2.deltaPoints, row2.previousRate)}<span class="zt-n"><b>${row2.touched}</b> of ${row2.runs} runs</span><span class="${`zt-st${row2.waitingNow ? " tone-parked" : ""}`}">${icon(row2.waitingNow ? "parked" : "flag", { size: "sm" })}${row2.waitingPartial ? "waiting history incomplete" : row2.waitingNow ? "waiting now" : row2.zeroTouch ? "zero touch, 30+ days" : row2.streakDays === null ? "not enough history" : `${amount3(row2.streakDays)} ${row2.streakDays === 1 ? "day" : "days"}`}</span><span class="zt-c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : "No touches"}</span>`;
162428
+ }
162429
+ function phoneProjects(m2, view) {
162430
+ const item2 = (row2, repository = false) => {
162431
+ const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
162432
+ const word3 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
162433
+ const query = new URLSearchParams({ days: String(m2.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
162434
+ return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct2(row2.rate)}, open ${repository ? "repository" : "project"}`}"${repository ? html` data-repository` : null}><span class="ztp-p__n"><b>${row2.name}</b><small>${row2.touched} of ${row2.runs} runs</small></span><span class="ztp-p__r"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span><span class="ztp-p__c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : html`<span>No touches</span>`}${delta(row2.deltaPoints, row2.previousRate)}</span><span class="ztp-p__go">${icon("chev-r", { size: "sm" })}</span></a></li>`;
162435
+ };
162436
+ return html`<ul class="ztp-ps zt-phone-projects">${m2.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r2) => item2(r2, true)) : null}`)}</ul>`;
162437
+ }
162438
+ function zeroTouchProjects(view) {
162439
+ const m2 = model5(view);
162440
+ if (m2.state !== "ready") return card({ title: "By project", body: fail5(m2) });
162441
+ const head = html`<div class="zt-row zt-row--head" aria-hidden="true"><span></span><span>Project</span><span>Touch rate, goal tick at 5%</span><span>Against the previous interval</span><span>Needed a person</span><span>Since the last touch</span><span>Most often</span></div>`;
162442
+ const rows3 = m2.projects.map((p) => html`<li><details class="zt-p"${view?.query?.project === p.project ? html` open` : null}><summary class="zt-row" aria-label="${`${p.name}: ${pct2(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r2) => html`<li class="zt-row zt-row--sub">${rowContent(r2, true)}</li>`)}</ul>${acts3(view) && p.touchedRunIds.length ? html`<p class="zt-foot"><a class="link" href="${`/runs?${new URLSearchParams({ project: p.project ?? "", touched: "1", days: String(m2.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
162443
+ return card({ title: "By project", sub: `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`, flush: true, body: html`${fail5(m2)}${m2.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m2, view)}<p class="zt-foot">${icon("info", { size: "sm" })}<span>A row opens to its repositories or folders. A run counts once, however many interventions it received.</span></p>` : html`<p class="zt-note">${ZERO_TOUCH_WORDS.empty}</p>`}` });
162444
+ }
162445
+ function causeCounts(values) {
162446
+ return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
162447
+ }
162448
+ function causeBar(values, max, name2) {
162449
+ const rows3 = causeCounts(values);
162450
+ let at2 = 0;
162451
+ max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
162452
+ const all = rows3.filter((r2) => r2.count > 0);
162453
+ const label3 = `${name2}: ${all.map((r2) => `${r2.label} ${r2.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
162454
+ return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label3}">${all.map((r2) => {
162455
+ const x2 = at2 / Math.max(1, max) * 500, w = r2.count / Math.max(1, max) * 500;
162456
+ at2 += r2.count;
162457
+ return html`<g class="${`tone-${r2.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x2 / 5, 0, 100)}%`}" y="0" width="${`${svgNum(w / 5, 0, 100)}%`}" height="28" rx="5"/>${w >= 22 ? html`<text class="zt-cause-label" x="${`${svgNum((x2 + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r2.count}</text>` : null}</g>`;
162458
+ })}</svg>`;
162459
+ }
162460
+ function zeroTouchCauses(view) {
162461
+ const m2 = model5(view), max = Math.max(1, ...m2.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
162462
+ if (m2.state !== "ready") return card({ title: "What needed a person", body: fail5(m2) });
162463
+ const bars = m2.projects.map((p) => html`<li class="zt-cb"><span class="zt-cb__n"><b>${p.name}</b><small>${p.touched} of ${p.runs}</small></span>${causeBar(p.causes, max, p.name)}<p class="zt-cb__t">${causeCounts(p.causes).filter((c) => c.count).map((c) => `${c.short} ${c.count}`).join(" \xB7 ") || "No touches"}</p></li>`);
162464
+ const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
162465
+ return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m2.windowDays} days, by project</span><span class="zt-phone-label">${m2.windowDays} days, ${scopeLabel}</span>`, body: html`${fail5(m2)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m2.causes, Math.max(1, m2.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m2.causes).map((c) => html`<li class="zt-why">${tile(c.tone === "running" ? "running" : c.tone === "idle" ? "idle" : c.tone, { icon: c.icon, sm: true })}<span><b>${c.label}</b><small>${c.detail}</small></span><span class="zt-why__n">${c.count}</span></li>`)}<li class="zt-why zt-why--sum"><span></span><span><b>Runs that needed a person</b></span><span class="zt-why__n">${m2.totals.touched}</span></li></ul><p class="zt-merge">${icon("pr", { size: "sm" })}<span>Merging is never a touch. <a class="link" href="#zt-counting">How touches are counted</a></span></p><details class="zt-counting" id="zt-counting"><summary>How touches are counted</summary><p class="zt-kps">The rate counts workload runs with a retained record that ended in the covered interval. Each run counts once. Bars assign it to its first successful human intervention; later interventions remain in history. Jobs stopped before starting are separate. Reads, sandbox opens, automatic starts and internal grouping jobs do not enter the touch count.</p></details>` });
162466
+ }
162467
+ function guardedForm(action2, { hidden: hidden2 = [], fields: fields3 = null, label: label3, kind = ["sm"] } = {}) {
162468
+ return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name2, value, type]) => html`<input type="hidden" name="${name2}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields3}${btn({ type: "submit", kind, label: label3 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
162469
+ }
162470
+ function nameForm(g, index2) {
162471
+ const key2 = `zt-name-${index2}`;
162472
+ return html`<details class="zt-editor"><summary class="link">${g.name ? "Rename this group" : "Name this group"}</summary>${guardedForm("zero-touch-name", { hidden: [["groupId", g.id]], fields: field2({ label: "Group name", id: key2, control: input({ id: key2, name: "name", value: g.name, required: true }), hint: "Write your own name, up to 120 characters. Question text is read only on its run page." }), label: "Preview name" })}</details>`;
162473
+ }
162474
+ function instructionForm(g, index2) {
162475
+ if (!g.runIds[0] || g.triggerIndex === null) return html`<p class="zt-kps">Add to instructions is unavailable: a retained run and a current non-cron trigger are required.</p>`;
162476
+ const key2 = `zt-sentence-${index2}`;
162477
+ return html`<details class="zt-editor"><summary class="link">Add to instructions</summary>${guardedForm("zero-touch-instruction", { hidden: [["runId", g.runIds[0]], ["groupId", g.id], ["index", g.triggerIndex, "int"]], fields: field2({ label: "Sentence to add", id: key2, control: textarea({ id: key2, name: "sentence", required: true, rows: 3, maxlength: 2e3 }), hint: "Preview the exact diff, then Confirm. The trigger's existing instructions count toward the 2,000-character limit." }), label: `Preview trigger ${g.triggerIndex + 1}`, kind: ["sm", "primary"] })}</details>`;
162478
+ }
162479
+ function zeroTouchQuestions(view) {
162480
+ if (view?.role !== "operator") return collapsed({ icon: "parked", title: "Questions it keeps asking", body: "are for operators. Rates, causes, streaks and fixes are shown here." });
162481
+ const m2 = model5(view);
162482
+ if (m2.state !== "ready") return card({ title: "Questions it keeps asking", body: fail5(m2) });
162483
+ const rows3 = m2.groups.map((g, index2) => html`<li class="zt-q"><div class="zt-q__h"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><div><p class="${`zt-q__t${g.name ? "" : " zt-q__t--un"}`}">${g.name ?? `${g.count} similar questions, not named yet`}</p><p class="zt-q__s"><span class="code">${g.repo ?? g.project ?? "Repository not known"}</span>${g.status === "fixed" && g.fixedAt !== null ? html` · fixed ${date(g.fixedAt)} UTC` : null}${g.status === "fixed" && m2.fixes.some((f3) => f3.groupId === g.id && f3.afterCount === 0 && f3.subsequentRuns > 0 && !f3.partial && f3.undoneAt === null) ? html` · none since` : null}</p></div><span class="zt-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span></div><p class="zt-said">${icon("runs", { size: "sm" })}<span>Asked in ${g.runIds.slice(0, 8).map((id7, at2) => html`${at2 ? ", " : ""}${link(id7)}`)}${g.runIds.length > 8 ? ` and ${g.runIds.length - 8} more` : null}.</span></p>${acts3(view) ? html`<div class="zt-q__act">${nameForm(g, index2)}${g.status !== "fixed" ? instructionForm(g, index2) : html`<a class="link" href="#p-zt-fixes">See the fix</a>`}</div>` : null}</li>`);
162484
+ const phoneRows = m2.groups.map((g, index2) => html`<li class="ztp-q"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><span class="ztp-q__t"><b>${g.runIds[0] ? html`<a class="zt-group-link" href="${`/run?id=${encodeURIComponent(g.runIds[0])}`}">${g.name ?? `${g.count} similar, not named yet`}</a>` : g.name ?? `${g.count} similar, not named yet`}</b><small>${g.repo ?? g.project ?? "Repository not known"}</small></span><span class="ztp-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span><span class="ztp-q__a">${acts3(view) ? g.status === "fixed" ? html`<a class="link" href="#p-zt-fixes">See the fix</a>` : g.name ? instructionForm(g, `phone-${index2}`) : nameForm(g, `phone-${index2}`) : null}</span></li>`);
162485
+ const sources = m2.grouping.sources.length ? m2.grouping.sources : m2.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
162486
+ return card({ title: "Questions it keeps asking", sub: "operator only", body: html`<p class="zt-intro">Grouped by meaning. You name each group; read the questions on their run pages.</p>${fail5(m2)}${m2.grouping.partial ? html`<p class="zt-note" role="status">Question grouping is partial. Missing or unclassified sources are not evidence that a question stopped recurring.</p>` : null}${["unavailable", "configuration-unavailable"].includes(m2.grouping.state) ? html`<p class="zt-note" role="status">${m2.grouping.state === "configuration-unavailable" ? "Grouping configuration could not be read. Check the reviewed triggers file before starting a grouping job." : "Question groups could not be read. Reload to retry; missing assignments are not an empty group list."}</p>` : null}${rows3.length ? html`<ol class="zt-qs zt-desktop-questions">${rows3}</ol><ol class="ztp-qs zt-phone-questions">${phoneRows}</ol>` : html`<p class="zt-kps">${["unavailable", "configuration-unavailable"].includes(m2.grouping?.state) ? "Grouping coverage is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noGroups}</p>`}${sources.length ? html`<ul class="zt-ungrouped">${sources.slice(0, 20).map((source2) => html`<li data-group-status="${source2.status}">${link(source2.runId)}<span>${ZERO_TOUCH_SOURCE_WORDS[source2.status]}</span>${acts3(view) && ["unassigned", "stale", "refused", "failed"].includes(source2.status) ? guardedForm("zero-touch-group", { hidden: [["runId", source2.runId]], label: "Group question", kind: ["sm", "paid"] }) : null}</li>`)}</ul>` : null}<p class="zt-foot zt-foot--in">${icon("info", { size: "sm" })}<span>${m2.grouping.enabled ? `${plural5(m2.grouping.pending, "grouping job")} pending; ${plural5(m2.grouping.unassigned, "question")} unassigned. Expired or unavailable context stays ungrouped.` : "Private capture and a capped classifier must be enabled in the reviewed zeroTouch configuration before questions can be grouped."}</span></p>` });
162487
+ }
162488
+ function fixTone(f3) {
162489
+ if (f3.partial || f3.subsequentRuns === 0 || f3.undoneAt !== null || f3.status === "observing") return "idle";
162490
+ return f3.status === "improved" ? "ok" : f3.status === "worse" ? "failed" : "idle";
162491
+ }
162492
+ function fixObservation(f3) {
162493
+ if (f3.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
162494
+ if (f3.partial) return "Partial observation: effectiveness is not established.";
162495
+ if (f3.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
162496
+ return f3.status === "improved" ? "Fewer occurrences over equal observed spans." : f3.status === "worse" ? "More occurrences over equal observed spans." : f3.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
162497
+ }
162498
+ function beforeAfter(label3, value, max, tone = "attention") {
162499
+ return html`<p class="zt-ba"><span>${label3}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label3}: ${value === null ? "not known" : value}`}"><rect class="pbar__bed" width="100%" height="8" rx="4"/>${value ? html`<rect class="pbar__fill" width="${`${svgNum(value / max * 100, 0, 100)}%`}" height="8" rx="4"/>` : null}</svg><b>${value ?? "?"}</b></p>`;
162500
+ }
162501
+ function zeroTouchFixes(view) {
162502
+ const m2 = model5(view);
162503
+ const evidence = html`${m2.fixesPartial ? html`<p class="zt-note" role="status">Fix history is incomplete. Reload to retry; missing changes are not evidence that no fix was made.</p>` : null}${m2.fixes.length ? html`<ol class="zt-fxs">${m2.fixes.map((f3) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f3.at)} UTC${f3.undoneAt === null ? null : html` · Reverted ${date(f3.undoneAt)} UTC`}</p><p class="zt-fx__w">${f3.kind === "instructions" ? `Trigger ${f3.triggerIndex === null ? "not known" : f3.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f3.reason ? `${f3.reason} refusals` : "Questions in the targeted group"}, equal ${span(f3.spanMs)} spans before and after; ${f3.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f3.beforeCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1))}${beforeAfter("After", f3.afterCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1), fixTone(f3))}<p class="zt-kps">${fixObservation(f3)}</p>${acts3(view) && f3.kind === "instructions" && f3.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f3.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m2.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
162504
+ const brief = m2.fixes.map((f3) => `${f3.reason ?? "grouped questions"}: ${f3.beforeCount ?? "?"} to ${f3.afterCount ?? "?"}`).join("; ");
162505
+ return card({ title: "Fixes that worked", sub: "what changed, and what it did", body: html`<div class="zt-desktop-fixes">${evidence}</div><details class="zt-phone-fixes"><summary>${icon("done", { size: "sm" })}<span><b>${m2.fixesPartial ? "Fix history incomplete" : `${m2.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
162506
+ }
162507
+ var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model5, acts3, pct2, amount3, date, span, link, fail5, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
162508
+ var init_zero_touch = __esm({
162509
+ "src/console/panels/zero-touch.mjs"() {
162510
+ init_common();
162511
+ init_html();
162512
+ init_kit();
162513
+ init_zero_touch_view();
162514
+ ZERO_TOUCH_WORDS = Object.freeze({ unavailable: "Zero touch could not be read. Reload to try again; missing history is never counted as zero touches.", empty: "No workload runs ended in the covered interval.", drafts: "Sent by a person, edited or as drafted. Each run still counts as a touch.", draftsPartial: "Partial history; this counts retained runs only.", viewer: "Questions it keeps asking are for operators. Rates, causes, streaks and fixes are shown here.", noGroups: "No question groups yet. Group retained questions with a capped decision job; name each group yourself.", noFixes: "No cause-linked fixes recorded yet. Start a change from a refusal or a question group to measure it here." });
162515
+ ZERO_TOUCH_CAUSES = Object.freeze([
162516
+ { cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
162517
+ { cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
162518
+ { cause: "rerun", label: "Failed or refused, then re-run", short: "Re-run", icon: "failed", tone: "failed", detail: "A person started a failed or refused run again." },
162519
+ { cause: "config-fix", label: "Refused until config was fixed", short: "Config fixed", icon: "refused", tone: "refused", detail: "A confirmed configuration change explicitly targeted the refusal." },
162520
+ { cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
162521
+ { cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
162522
+ ]);
162523
+ ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
162524
+ unassigned: "Not grouped yet.",
162525
+ pending: "A grouping job is pending.",
162526
+ assigned: "Assigned to a question group.",
162527
+ "source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
162528
+ "source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
162529
+ "source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
162530
+ "candidates-expired": "A group's representative context expired. Review the groups before trying again.",
162531
+ "group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
162532
+ stale: "The group revision changed during classification. Preview a fresh grouping job.",
162533
+ refused: "The grouping job was refused. Open its run to see the policy reason.",
162534
+ failed: "The grouping job failed. Open its run and review the failure before trying again."
162535
+ });
162536
+ causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
162537
+ model5 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
162538
+ acts3 = (v) => v?.role === "operator" && !v?.readOnly;
162539
+ pct2 = (n3) => Number.isFinite(n3) ? `${Math.round(n3)}%` : "Not known";
162540
+ amount3 = (n3) => Number.isFinite(n3) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n3) : "Not known";
162541
+ date = (n3) => Number.isFinite(n3) && n3 >= 0 && n3 <= 864e13 ? new Date(n3).toISOString().slice(0, 10) : "not known";
162542
+ span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount3(ms3 / 36e5)}h` : `${amount3(ms3 / 6e4)}m`;
162543
+ link = (id7) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id7)}`}">${id7}</a>`;
162544
+ fail5 = (m2) => m2.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
162545
+ ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
162546
+ ZERO_TOUCH_PANELS = Object.freeze([["p-zt-rate", zeroTouchRate], ["p-zt-streak", zeroTouchStreak], ["p-zt-wait", zeroTouchWait], ["p-zt-handled", zeroTouchHandled], ["p-zt-projects", zeroTouchProjects], ["p-zt-causes", zeroTouchCauses], ["p-zt-questions", zeroTouchQuestions], ["p-zt-fixes", zeroTouchFixes]]);
162547
+ }
162548
+ });
162549
+
162550
+ // src/trigger-policy.mjs
162551
+ function triggerWhoMayFire(trigger2) {
162552
+ const on = trigger2.on ?? trigger2;
162553
+ const forge = trigger2.run?.kind ?? trigger2.forge;
162554
+ const kind = on.type;
162555
+ if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
162556
+ if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
162557
+ if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
162558
+ if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
162559
+ if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
162560
+ if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
162561
+ const actions = Array.isArray(on.action) ? on.action : [];
162562
+ const gates = [];
162563
+ if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
162564
+ if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
162565
+ if (actions.includes("review_submitted")) gates.push("The reviewer's association must be OWNER, MEMBER or COLLABORATOR, regardless of the pull request author's association");
162566
+ if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
162567
+ return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
162568
+ }
162569
+ var init_trigger_policy = __esm({
162570
+ "src/trigger-policy.mjs"() {
162254
162571
  }
162255
162572
  });
162256
162573
 
@@ -163425,7 +163742,7 @@ function projectSizing2(v) {
163425
163742
  const s2 = p.sizing;
163426
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>`;
163427
163744
  const action2 = s2?.action;
163428
- 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");
163429
163746
  }
163430
163747
  function projectRecent(v) {
163431
163748
  if (!ready(v)) return null;
@@ -172194,6 +172511,14 @@ var init_watch = __esm({
172194
172511
  }
172195
172512
  });
172196
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
+
172197
172522
  // src/console/session-ends.mjs
172198
172523
  function streamEndOf(v) {
172199
172524
  return typeof v === "string" && STREAM_ENDS.includes(v) ? v : null;
@@ -172260,6 +172585,7 @@ var init_shell = __esm({
172260
172585
  init_html();
172261
172586
  init_kit();
172262
172587
  init_pages();
172588
+ init_restart_notice();
172263
172589
  init_pulse_icons();
172264
172590
  init_signin();
172265
172591
  init_theme();
@@ -174616,6 +174942,15 @@ import { createHash as createHash28 } from "node:crypto";
174616
174942
  function changesHtml(changes) {
174617
174943
  return (Array.isArray(changes) ? changes : []).map(([key2, before, after], i2) => html`${i2 > 0 ? ", " : null}${key2}: <b>${before}</b> to <b>${after}</b>`);
174618
174944
  }
174945
+ function plainBody(value, depth = 0) {
174946
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
174947
+ if (typeof value === "number") return Number.isFinite(value);
174948
+ if (depth >= BODY_DEPTH_MAX || typeof value !== "object") return false;
174949
+ if (Array.isArray(value)) return value.length <= 256 && value.every((v) => plainBody(v, depth + 1));
174950
+ const keys3 = Object.keys(value);
174951
+ if (keys3.length > BODY_KEYS_MAX) return false;
174952
+ return keys3.every((k) => plainBody(value[k], depth + 1));
174953
+ }
174619
174954
  function str4(body, key2, { required = false, max = 512 } = {}) {
174620
174955
  const v = body?.[key2];
174621
174956
  if (v === void 0 || v === null || v === "") {
@@ -175245,11 +175580,37 @@ function settingDone(out) {
175245
175580
  const sayable = typeof old === "string" && old.trim() !== "" || typeof old === "number" && Number.isFinite(old);
175246
175581
  return sayable ? `Changed ${key2} from ${String(old)} to ${value}` : `Changed ${key2} to ${value}`;
175247
175582
  }
175583
+ function pauseListShapeRefusal(windows) {
175584
+ for (const [i2, w] of windows.entries()) {
175585
+ const at2 = atIndex("pause window", i2);
175586
+ if (w === null || typeof w !== "object" || Array.isArray(w)) return `${at2} is not an object`;
175587
+ for (const [k, value] of Object.entries(w)) {
175588
+ if (!PAUSE_WINDOW_FIELDS.includes(k)) return `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only)`;
175589
+ const plain = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
175590
+ if (!plain) return `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}`;
175591
+ }
175592
+ }
175593
+ return null;
175594
+ }
175248
175595
  function pauseListBack(out) {
175249
175596
  const unread2 = typeof out.beforeText === "string" && !Array.isArray(jsonOf(out.beforeText)?.windows);
175250
175597
  if (unread2) return { undo: twice(UNDO_SENTENCES.tooBig, UNDO_SENTENCES.tooBigAfter), inverse: null };
175598
+ if (!pauseListGoesBack(out)) return { undo: twice(UNDO_SENTENCES.refusedList, UNDO_SENTENCES.refusedListAfter), inverse: null };
175251
175599
  return { undo: "Put the previous pause windows back", inverse: { action: "pause-windows", body: { windows: out.extra.before } } };
175252
175600
  }
175601
+ function pauseListGoesBack(out) {
175602
+ const before = out.extra?.before;
175603
+ if (!Array.isArray(before) || !plainBody({ windows: before }) || pauseListShapeRefusal(before) !== null) return false;
175604
+ if (typeof out.beforeText !== "string") return true;
175605
+ const held = heldPauseWindows(out.beforeText, out.target ?? "pause-windows.json");
175606
+ if (held.problem) return false;
175607
+ try {
175608
+ parsePauseWindows(pauseWindowsText(held.doc, before), out.target ?? "pause-windows.json");
175609
+ return true;
175610
+ } catch {
175611
+ return false;
175612
+ }
175613
+ }
175253
175614
  function triggerEditBack(out) {
175254
175615
  const p = out.plan ?? {};
175255
175616
  const n3 = atIndex("trigger", p.index);
@@ -175344,7 +175705,7 @@ function adoptable(facts7, { name: name2, version }) {
175344
175705
  function actionNamed(name2) {
175345
175706
  return ACTIONS3.find((a) => a.name === name2) ?? null;
175346
175707
  }
175347
- var BULK_MAX2, CANCEL_TIMEOUT_SENTENCE, JOB_ID_RE2, sha2563, digestOf, codeOf4, AGENT_POLICY_TITLES, rerunAuthorityDigest, atIndex, BadRequest, UNCHANGED, reserveTail, SNAPSHOT_TIMEOUT_MS, SNAPSHOT_TIMEOUT_SENTENCE, SWITCH_READ, SNAPSHOT_UNREAD_SENTENCE, SNAPSHOT_LATE, FILE_NEEDS, JOB_NEEDS, FLEET_NEEDS, TRIGGER_FORM, settingBar, pausePath, shownDir, pauseWindowsWhole, writePause, writeTriggersOf, writeSettingsOf, writeLimitsOf, writeProjectsOf, configActions, failedLine2, drawable, CANCEL_QUEUED_LINE, jobActions, SWITCH_QUEUES_NAMED, SWITCH_SHORT_LINES, queueUnreadRefusal, RESUME_WAITING_UNKNOWN, fleetActions, UNDO_SENTENCES, twice, DELETED, resumeManyUndo, listBefore, byHand, say, none, confirmWord, rerunCancel, replacedOne, whereOf2, bulkDone, PAUSE_FIELDS, WRITE_RULES, PAUSE_EDIT, LIMIT_EDIT, RULE_FNS, zeroTouchActions, modelRoleActions, triggerEnvironmentActions, sandboxActions, deviceActions, boardActions, stagedRow, packageActions, CONFIG_FIX_ACTIONS, ACTIONS3;
175708
+ var BULK_MAX2, CANCEL_TIMEOUT_SENTENCE, JOB_ID_RE2, BODY_DEPTH_MAX, BODY_KEYS_MAX, sha2563, digestOf, codeOf4, AGENT_POLICY_TITLES, rerunAuthorityDigest, atIndex, BadRequest, UNCHANGED, reserveTail, SNAPSHOT_TIMEOUT_MS, SNAPSHOT_TIMEOUT_SENTENCE, SWITCH_READ, SNAPSHOT_UNREAD_SENTENCE, SNAPSHOT_LATE, FILE_NEEDS, JOB_NEEDS, FLEET_NEEDS, TRIGGER_FORM, settingBar, pausePath, shownDir, pauseWindowsWhole, writePause, writeTriggersOf, writeSettingsOf, writeLimitsOf, writeProjectsOf, configActions, failedLine2, drawable, CANCEL_QUEUED_LINE, jobActions, SWITCH_QUEUES_NAMED, SWITCH_SHORT_LINES, queueUnreadRefusal, RESUME_WAITING_UNKNOWN, fleetActions, UNDO_SENTENCES, twice, DELETED, resumeManyUndo, listBefore, byHand, say, none, confirmWord, rerunCancel, replacedOne, whereOf2, bulkDone, PAUSE_FIELDS, WRITE_RULES, PAUSE_EDIT, LIMIT_EDIT, RULE_FNS, zeroTouchActions, modelRoleActions, triggerEnvironmentActions, sandboxActions, deviceActions, boardActions, stagedRow, packageActions, CONFIG_FIX_ACTIONS, ACTIONS3;
175348
175709
  var init_actions = __esm({
175349
175710
  "src/console/actions.mjs"() {
175350
175711
  init_package_canary_record();
@@ -175381,6 +175742,8 @@ var init_actions = __esm({
175381
175742
  BULK_MAX2 = 100;
175382
175743
  CANCEL_TIMEOUT_SENTENCE = "no worker acknowledged: the job may have just finished, gone back to the queue (cancel it again to remove it), or its host is unreachable; nothing was changed";
175383
175744
  JOB_ID_RE2 = /^[A-Za-z0-9._:-]{1,200}$/;
175745
+ BODY_DEPTH_MAX = 8;
175746
+ BODY_KEYS_MAX = 64;
175384
175747
  sha2563 = (text4) => createHash28("sha256").update(text4).digest("hex");
175385
175748
  digestOf = (text4) => text4 === null || text4 === void 0 ? null : sha2563(text4);
175386
175749
  codeOf4 = (e2) => typeof e2?.code === "string" && /^[A-Z0-9_]{1,40}$/.test(e2.code) ? e2.code : "error";
@@ -175411,15 +175774,8 @@ var init_actions = __esm({
175411
175774
  needs: [],
175412
175775
  async preview(body, ctx) {
175413
175776
  if (!Array.isArray(body.windows)) return { refusal: 'send {"windows": [...]} with the whole new list of pause windows' };
175414
- for (const [i2, w] of body.windows.entries()) {
175415
- const at2 = atIndex("pause window", i2);
175416
- if (w === null || typeof w !== "object" || Array.isArray(w)) return { refusal: `${at2} is not an object; nothing was changed` };
175417
- for (const [k, value] of Object.entries(w)) {
175418
- if (!PAUSE_WINDOW_FIELDS.includes(k)) return { refusal: `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only); nothing was changed` };
175419
- const plain = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
175420
- if (!plain) return { refusal: `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}; nothing was changed` };
175421
- }
175422
- }
175777
+ const shape2 = pauseListShapeRefusal(body.windows);
175778
+ if (shape2) return { refusal: `${shape2}; nothing was changed` };
175423
175779
  const path6 = ctx.config.pauseWindowsPath;
175424
175780
  const snap = readNow(ctx.fs, path6);
175425
175781
  if (snap.refusal) return snap;
@@ -176097,6 +176453,8 @@ var init_actions = __esm({
176097
176453
  envelopeAfter: "Set it back with the envelope form: the console kept no copy of the old values.",
176098
176454
  tooBig: "Put the previous list back by hand: this preview showed it; the console keeps no copy.",
176099
176455
  tooBigAfter: "Put the previous list back by hand: the console kept no copy of it.",
176456
+ refusedList: "Put the previous list back by hand: the console would refuse it as an Undo (a window it does not take, or more than 256 windows or days); this preview showed it, and the console keeps no copy.",
176457
+ refusedListAfter: "Put the previous list back by hand: the console would refuse it as an Undo (a window it does not take, or more than 256 windows or days), and kept no copy of it.",
176100
176458
  resumeUnread: "Pause the queues that were paused again by hand: the fleet was not read before the resume, so which they were is not known.",
176101
176459
  resumeShort: "Pause the queues that were paused again by hand: not every queue was read before the resume, so which they were is not fully known."
176102
176460
  });
@@ -178059,8 +178417,8 @@ var init_setup_wizard = __esm({
178059
178417
  init_connection();
178060
178418
  init_service_env();
178061
178419
  init_index();
178062
- RUNTIME_VERSION = "5.1.0";
178063
- RECEIVER_VERSION = "5.1.0";
178420
+ RUNTIME_VERSION = "5.2.1";
178421
+ RECEIVER_VERSION = "5.2.1";
178064
178422
  RUNTIME_PKG = "@edgehero/pi-dispatch";
178065
178423
  RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
178066
178424
  NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
@@ -179940,7 +180298,7 @@ The project's runs could not be read (${sizing.unreachable}).`;
179940
180298
  if (dim.held === "largest") return `no larger size (${dim.reason}: ${words})`;
179941
180299
  return dim.reason === "not-enough-runs" ? notEnoughRunsText(dim, shown3 === formatCpus ? cpusText : shown3) : "fits";
179942
180300
  };
179943
- const facts7 = [s2.words.memoryFact, s2.words.cpuFact].filter(Boolean);
180301
+ const facts7 = [s2.words.memoryFact, s2.words.cpuFact, s2.words.memoryCeiling].filter(Boolean);
179944
180302
  return `
179945
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(" ")}` : ""}`;
179946
180304
  }