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