@edgehero/pi-dispatch 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/package.json +1 -1
- package/src/auth-cli.mjs +76 -5
- package/src/cancel-cli.mjs +193 -16
- package/src/cancel-state.mjs +85 -20
- package/src/connection.mjs +214 -6
- package/src/doctor-bundles.mjs +6 -2
- package/src/doctor-model-proposals.mjs +4 -2
- package/src/doctor.mjs +225 -45
- package/src/env-allowlist.mjs +9 -1
- package/src/github-app-setup.mjs +17 -4
- package/src/host-loads.mjs +58 -4
- package/src/job-size.mjs +27 -0
- package/src/json-duplicates.mjs +53 -42
- package/src/mcp-preflight.mjs +2 -2
- package/src/pause-windows.mjs +19 -3
- package/src/reason-catalog.mjs +1 -1
- package/src/redact-secrets.mjs +4 -0
- package/src/reserved-env.mjs +4 -0
- package/src/sandbox-cli.mjs +9 -3
- package/src/sandbox-deployment.mjs +38 -5
- package/src/sandbox-prepare.mjs +16 -2
- package/src/sandbox.mjs +10 -3
- package/src/service-env.mjs +4 -3
- package/src/size-suggest.mjs +118 -8
- package/src/touch-outbox.mjs +5 -0
- package/src/valkey-endpoint.mjs +9 -6
package/src/service-env.mjs
CHANGED
|
@@ -237,7 +237,8 @@ export function readDeploymentEnv({ dir, fs, uid = process.geteuid?.(), names =
|
|
|
237
237
|
/**
|
|
238
238
|
* The `.env` of the deployment in `dir`, read the way doctor reads it (a regular file only, as bytes, one read) and
|
|
239
239
|
* resolved for `keys` (`resolveServiceEnv`). For a caller that has no file of its own in hand (the admin panel).
|
|
240
|
-
* Returns the resolution plus `path`, `
|
|
240
|
+
* Returns the resolution plus `path`, `absent` (true when nothing is at the path: issue #845's console tells a file that
|
|
241
|
+
* went away from one that never was), `unreadable` (a reason, when something is at the path and could not be read) and
|
|
241
242
|
* `untrusted` (`envFileTrust`'s reason); in either case nothing was taken from it. `shellRunsLine` is the line a sourcing shell does
|
|
242
243
|
* not read as an assignment, whatever keys were asked for: it can set any of them without spelling one (`. ./more.env`, `eval`), so
|
|
243
244
|
* `hazardSkipped`, which needs the key spelled, says nothing of it. Only for that loader: systemd ignores such a line and the cmd wrapper
|
|
@@ -245,11 +246,11 @@ export function readDeploymentEnv({ dir, fs, uid = process.geteuid?.(), names =
|
|
|
245
246
|
*/
|
|
246
247
|
export function deploymentServiceEnv({ env, dir, keys, platform = process.platform, fs, uid = process.geteuid?.() }) {
|
|
247
248
|
// Issue #471 (gate rounds 1 and 2): one descriptor, and nothing taken from a file another account can change.
|
|
248
|
-
const { path, bytes, unreadable, untrusted } = readDeploymentEnv({ dir, fs, uid });
|
|
249
|
+
const { path, bytes, absent, unreadable, untrusted } = readDeploymentEnv({ dir, fs, uid });
|
|
249
250
|
const raw = untrusted ? null : bytes;
|
|
250
251
|
const file = raw === null ? null : serviceEnvFileOf(raw, path, serviceEnvLoader(platform));
|
|
251
252
|
const shellRunsLine = file?.loader === "shell" && file.hazard !== null && file.hazard.shape !== "shell-crlf-comment" ? file.hazard.line : null;
|
|
252
|
-
return { ...resolveServiceEnv({ env, file, keys }), path, unreadable, untrusted, hazard: file?.hazard ?? null, shellRunsLine };
|
|
253
|
+
return { ...resolveServiceEnv({ env, file, keys }), path, absent, unreadable, untrusted, hazard: file?.hazard ?? null, shellRunsLine };
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
/**
|
package/src/size-suggest.mjs
CHANGED
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
* 4. otherwise it fits.
|
|
40
40
|
* A FACT (no call) rides along when runs at the limit were also under real memory pressure (`memFullUsec` above 1%
|
|
41
41
|
* of the wall time): information, never an instruction.
|
|
42
|
+
* An `oom-killed` raise also says what CPU count the killed runs' tools counted at least (issue #865, `oomCeiling`,
|
|
43
|
+
* worded as `memoryCeiling`): on a venue whose `--cpus` is the host ceiling (`CEILING_VENUES`), the fewest whole CPUs
|
|
44
|
+
* a run's ceiling gives `os.availableParallelism()` (`seenCpusOf`), from its record's `capacity`, when that is more
|
|
45
|
+
* than its size in whole CPUs (`PI_JOB_SIZE_CPUS`). A tool that starts a worker per CPU it sees starts one per such CPU inside the size's memory,
|
|
46
|
+
* so a smaller pool may be a cheaper fix than a larger size. Information beside the raise, which is the same either
|
|
47
|
+
* way.
|
|
42
48
|
* A size is rounded UP to a step: 256m up to 2g, 512m up to 8g, then 1g.
|
|
43
49
|
* CPUs: fewer than `SUGGEST_MIN_SAMPLES` runs is not enough runs; the p95 cores used (CPU time over wall time) below
|
|
44
50
|
* 0.4x the current cpus lowers to 1.25x that p95, never below 1.25x the window's largest cores used, nor 0.25;
|
|
@@ -59,7 +65,7 @@
|
|
|
59
65
|
* land on the side the rule says, on every host.
|
|
60
66
|
*/
|
|
61
67
|
|
|
62
|
-
import { JOB_CPUS_CEILING_CENTI, JOB_CPUS_FLOOR_CENTI, JOB_MEMORY_CEILING_MIB, JOB_MEMORY_FLOOR_MIB, formatCpus, formatMemory, recordedJobSize } from "./job-size.mjs";
|
|
68
|
+
import { HOST_CPUS_MAX, JOB_CPUS_CEILING_CENTI, JOB_CPUS_FLOOR_CENTI, JOB_MEMORY_CEILING_MIB, JOB_MEMORY_FLOOR_MIB, cpuCeilingCenti, formatCpus, formatMemory, recordedJobSize, wholeCpusOf } from "./job-size.mjs";
|
|
63
69
|
|
|
64
70
|
/** How far back a suggestion reads: 30 days. */
|
|
65
71
|
export const SUGGEST_WINDOW_DAYS = 30;
|
|
@@ -93,6 +99,18 @@ export const MEMORY_HELD = Object.freeze(["cap", "largest", "no-cap"]);
|
|
|
93
99
|
export const MEMORY_CAP_MISSING = Object.freeze(["unread", "none-holds", "off"]);
|
|
94
100
|
/** The facts a suggestion may carry with no call: memory `pressure`, CPU `ceiling`. */
|
|
95
101
|
export const SIZE_FACTS = Object.freeze(["pressure", "ceiling"]);
|
|
102
|
+
/**
|
|
103
|
+
* Issue #865: the venues whose job containers get the host's CPU ceiling as `--cpus`, the same for every job
|
|
104
|
+
* (job-size.mjs `containerSizing`, on the docker and podman builders alike), so a job's tools count at least the ceiling
|
|
105
|
+
* (`seenCpusOf`) and never its size. OpenShell's venues are not here: their `--cpu` is the job's size itself
|
|
106
|
+
* (backend-openshell.mjs `sizeArgs`), so on cgroup v2 a tool there reads the size rounded down from one CPU up, and
|
|
107
|
+
* under one CPU (0.25 to 0.99) the gateway's whole runtime, whose CPU count the record does not hold (its
|
|
108
|
+
* `capacity.cpus` is this host's runtime's, and the gateway may be another machine). That last case is a residual,
|
|
109
|
+
* named in `DES-JOB-SIZE-IN-THE-ENV`, not a clause said on a guess. Restated, since this module imports nothing heavy, and held
|
|
110
|
+
* to backends.mjs by a test: every venue is in this list or in `OPENSHELL_BACKENDS`, so a new venue fails it until
|
|
111
|
+
* someone says which it is.
|
|
112
|
+
*/
|
|
113
|
+
export const CEILING_VENUES = Object.freeze(["local", "podman"]);
|
|
96
114
|
|
|
97
115
|
const MIB = 1024 * 1024;
|
|
98
116
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -117,9 +135,60 @@ function rank(sorted, pct) {
|
|
|
117
135
|
|
|
118
136
|
const isCount = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
119
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Issue #865: the whole CPUs a run's tools read through `os.availableParallelism()`, as its record gives it, or null. The
|
|
140
|
+
* `--cpus` ceiling comes from the record's `capacity` (`cpus`, the runtime's CPU count, and `cpuCenti`, the host's CPU
|
|
141
|
+
* budget, each judged again here) through the worker's own `cpuCeilingCenti`, on a venue in `CEILING_VENUES` only, and
|
|
142
|
+
* becomes a count by `seenCpusOf`. Null for any other venue, a record with no readable `capacity` (every record before
|
|
143
|
+
* issue #599, and every refusal before a slot), a capacity that names neither, and a ceiling under one CPU on a record
|
|
144
|
+
* whose runtime count is not known.
|
|
145
|
+
* The record holds what the host offered at admission, not the argv itself, so this is the ceiling the worker computes
|
|
146
|
+
* from those facts. Where the budget's read of the runtime gave no CPU count, the record holds the worker's own count
|
|
147
|
+
* instead (index.mjs `capacityNow`), which on Docker Desktop is the Mac's and not the VM's, so the value is then an
|
|
148
|
+
* estimate of what the tools read. Accepted, rather than a record field for the argv's own `--cpus`: the clause is
|
|
149
|
+
* information beside a raise that does not depend on it, and that read failing while a job still ran is rare.
|
|
150
|
+
*/
|
|
151
|
+
function seenOf(record) {
|
|
152
|
+
if (!CEILING_VENUES.includes(record.backend)) return null;
|
|
153
|
+
const c = record.capacity;
|
|
154
|
+
if (c === null || typeof c !== "object" || Array.isArray(c)) return null;
|
|
155
|
+
const cpus = Number.isSafeInteger(c.cpus) && c.cpus >= 1 && c.cpus <= HOST_CPUS_MAX ? c.cpus : null;
|
|
156
|
+
const budget = Number.isSafeInteger(c.cpuCenti) && c.cpuCenti >= 1 && c.cpuCenti <= HOST_CPUS_MAX * 100 ? c.cpuCenti : null;
|
|
157
|
+
const ceilingCenti = cpuCeilingCenti(cpus, budget);
|
|
158
|
+
return ceilingCenti === null ? null : seenCpusOf(ceilingCenti, cpus);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Issue #865: the libuv the rule in `seenCpusOf` was read at, the one the job image's Node bundles (the Node
|
|
163
|
+
* image/Dockerfile notes its base resolves to). The next minor release, 1.52.0, changed it
|
|
164
|
+
* (`uv__get_cgroupv2_constrained_cpu` answers 1 for a quota under one CPU, where this release answers 0 and the caller
|
|
165
|
+
* skips it), so the image job compares the built image's `process.versions.uv` and `process.versions.node` with this
|
|
166
|
+
* and with that note (.github/scripts/libuv-pin-check.mjs, in "the job image holds its contract") and fails on any
|
|
167
|
+
* other, saying to read the rule again at the new libuv before moving this.
|
|
168
|
+
*/
|
|
169
|
+
export const SEEN_CPUS_LIBUV = "1.51.0";
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Issue #865: the fewest whole CPUs a tool counts through `os.availableParallelism()` in a container with a `--cpus`
|
|
173
|
+
* ceiling in hundredths, or null, where `runtimeCpus` is the runtime's own count (the container's affinity mask holds all
|
|
174
|
+
* of it, so `nproc` reads it). A LOWER BOUND, since what libuv reads depends on the host's cgroup version, which the
|
|
175
|
+
* record does not hold. Read at libuv `SEEN_CPUS_LIBUV`: on cgroup v2, `uv__get_constrained_cpu` (src/unix/linux.c)
|
|
176
|
+
* sets the quota to `cpu.max`'s limit over its period in INTEGER division, and `uv_available_parallelism`
|
|
177
|
+
* (src/unix/core.c) takes it only `if (quota > 0 && quota < rc)`, where `rc` is the affinity count: a ceiling of 2.5
|
|
178
|
+
* reads as 2 (ROUNDED DOWN, where `PI_JOB_SIZE_CPUS` rounds the size up), and one under one CPU gives a quota of 0,
|
|
179
|
+
* which is skipped, so the tool reads the WHOLE runtime. On cgroup v1, `uv__cgroup1_find_cpu_controller` turns
|
|
180
|
+
* `N:cpu,cpuacct:/path` into a `/sys/fs/cgroup/cpuacct:/path` that does not exist, so the read fails and the tool reads
|
|
181
|
+
* the whole runtime at any ceiling. So a tool counts at least the ceiling rounded down, and under one CPU the whole
|
|
182
|
+
* runtime: null when the record does not say how many that is.
|
|
183
|
+
*/
|
|
184
|
+
function seenCpusOf(ceilingCenti, runtimeCpus) {
|
|
185
|
+
if (ceilingCenti >= 100) return Math.floor(ceilingCenti / 100);
|
|
186
|
+
return Number.isSafeInteger(runtimeCpus) && runtimeCpus >= 1 ? runtimeCpus : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
120
189
|
/**
|
|
121
190
|
* One record as a suggestion reads it, or null when it is not evidence: `{ at, size, memPeak, oom, wallUsec, cpuUsec,
|
|
122
|
-
* throttledUsec, memFullUsec }`. `memPeak` (bytes, clamped to the record's own limit) is null when absent or not a
|
|
191
|
+
* throttledUsec, memFullUsec, seenCpus }`. `memPeak` (bytes, clamped to the record's own limit) is null when absent or not a
|
|
123
192
|
* count; `wallUsec` is null unless the span is readable; `cpuUsec` is null unless it can be read with a wall time (CPU
|
|
124
193
|
* time clamped to the CPU ceiling over the wall), and `throttledUsec` likewise on its own (clamped to the wall): cgroup v2
|
|
125
194
|
* writes `throttled_usec` in `cpu.stat` only where the cpu controller is enabled, so a run without it is still a CPU
|
|
@@ -145,7 +214,7 @@ function evidenceOf(record, project, nowMs) {
|
|
|
145
214
|
// unclamped: it is only ever compared (exactly, in BigInt) with 1% of the wall, and a stall past the wall is above it
|
|
146
215
|
const memFullUsec = wallUsec !== null && isCount(r.memFullUsec) ? r.memFullUsec : null;
|
|
147
216
|
if (memPeak === null && cpu.cpuUsec === null) return null;
|
|
148
|
-
return { at, size, memPeak, oom: record.reason === "oom-killed", wallUsec, ...cpu, memFullUsec };
|
|
217
|
+
return { at, size, memPeak, oom: record.reason === "oom-killed", wallUsec, ...cpu, memFullUsec, seenCpus: seenOf(record) };
|
|
149
218
|
}
|
|
150
219
|
|
|
151
220
|
/** a x b > c x d, exactly. */
|
|
@@ -164,6 +233,25 @@ function heldToCap(wanted, current, cap) {
|
|
|
164
233
|
return { suggested: wanted, held: null };
|
|
165
234
|
}
|
|
166
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Issue #865: what CPU count the killed runs' tools counted at least, against their size, or null: over the
|
|
238
|
+
* `oom-killed` runs that decide a raise (the same ones `ooms` counts), those where the fewest CPUs their tools could
|
|
239
|
+
* count (`seenOf`) is more than their size rounded up, which is what `PI_JOB_SIZE_CPUS` tells them. At or below it nothing can be said:
|
|
240
|
+
* a pool sized from the variable may be the pool the ceiling gave. `{ runs, seenCpus, seenMaxCpus, sizeCenti,
|
|
241
|
+
* sizeVaries }`: how many such runs, the fewest and the most CPUs their tools counted at least (equal when every run
|
|
242
|
+
* gives the same bound), and the smallest CPU size among them and whether their sizes differ. Either can vary alone
|
|
243
|
+
* (one host's ceiling under projects of several sizes, or one size across hosts), and the words say a range for that
|
|
244
|
+
* one only.
|
|
245
|
+
*/
|
|
246
|
+
function oomCeilingOf(relevant) {
|
|
247
|
+
const over = relevant.filter((e) => e.oom && e.seenCpus !== null && e.seenCpus > wholeCpusOf(e.size.cpuCenti));
|
|
248
|
+
if (over.length === 0) return null;
|
|
249
|
+
const seen = over.map((e) => e.seenCpus);
|
|
250
|
+
const sizes = over.map((e) => e.size.cpuCenti);
|
|
251
|
+
const sizeCenti = Math.min(...sizes);
|
|
252
|
+
return { runs: over.length, seenCpus: Math.min(...seen), seenMaxCpus: Math.max(...seen), sizeCenti, sizeVaries: sizes.some((n) => n !== sizeCenti) };
|
|
253
|
+
}
|
|
254
|
+
|
|
167
255
|
function suggestMemory(runs, current, cap) {
|
|
168
256
|
const measured = runs.filter((e) => e.memPeak !== null);
|
|
169
257
|
const relevant = measured.filter((e) => e.size.memMiB >= current);
|
|
@@ -176,7 +264,7 @@ function suggestMemory(runs, current, cap) {
|
|
|
176
264
|
// at the limit (90% of it or more: peak x 10 >= limit x 9) AND stalled for memory more than 1% of the wall.
|
|
177
265
|
const pressured = relevant.filter((e) => !productAbove(e.size.memMiB * MIB, 9, e.memPeak, 10) && e.memFullUsec !== null && productAbove(e.memFullUsec, 100, e.wallUsec, 1)).length;
|
|
178
266
|
// `smaller`: the measured runs below the current size, which a chart draws and this verdict does not count.
|
|
179
|
-
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 };
|
|
267
|
+
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) };
|
|
180
268
|
const fact = pressured > 0 ? "pressure" : null;
|
|
181
269
|
const out = (reason, suggested, more = {}) => ({ current, suggested: suggested === null || suggested === current ? null : suggested, reason, evidence, fact, held: null, wanted: null, ...more });
|
|
182
270
|
if (ooms > 0) {
|
|
@@ -419,12 +507,33 @@ export function notEnoughRunsText(dim, sizeText) {
|
|
|
419
507
|
return `not enough runs at ${sizeText(dim.current)} (${samples}${smaller})`;
|
|
420
508
|
}
|
|
421
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Issue #865: the CPU count the killed runs' tools counted at least, in words, or "" when the evidence carries none
|
|
512
|
+
* (`oomCeilingOf`): `size per-CPU worker pools from PI_JOB_SIZE_CPUS: the killed run's tools counted at least 15 CPUs
|
|
513
|
+
* at a size of 2 CPUs`. "At least", since the count is a lower bound (`seenCpusOf`). A word of its own
|
|
514
|
+
* (`memoryCeiling`), not part of the OOM words: every surface prints it AFTER the suggestion, a held raise and a refusal
|
|
515
|
+
* (doctor ahead of its two facts; the console's card as the last of the suggestion's notes, before the card's advisory
|
|
516
|
+
* note, with the call's block below the notes; the panel and the confirm after their facts), so where a surface cuts a
|
|
517
|
+
* line (doctor publishes a label at most `DOCTOR_LABEL_MAX` long) the facts go first, then this, never the call, a held
|
|
518
|
+
* raise or a refusal. Short for the same cap, and the variable first, so a cut keeps the pointer as long as it can. A
|
|
519
|
+
* pointer, never a call: the raise is the same either way.
|
|
520
|
+
*/
|
|
521
|
+
function oomCeilingWords(me) {
|
|
522
|
+
const o = me?.oomCeiling;
|
|
523
|
+
if (o === null || typeof o !== "object" || !Number.isSafeInteger(o.runs) || o.runs < 1 || !Number.isSafeInteger(o.seenCpus)) return "";
|
|
524
|
+
const ooms = Number.isSafeInteger(me.ooms) && me.ooms >= o.runs ? me.ooms : o.runs;
|
|
525
|
+
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`;
|
|
526
|
+
const seen = `${o.seenCpus}${Number.isSafeInteger(o.seenMaxCpus) && o.seenMaxCpus > o.seenCpus ? ` to ${o.seenMaxCpus}` : ""} CPUs`;
|
|
527
|
+
const size = o.sizeVaries === true ? `sizes from ${cpusText(o.sizeCenti)}` : `a size of ${cpusText(o.sizeCenti)}`;
|
|
528
|
+
return `size per-CPU worker pools from PI_JOB_SIZE_CPUS: ${whose} counted at least ${seen} at ${size}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
422
531
|
/**
|
|
423
532
|
* The evidence of a suggestion in words, for doctor, the panel and the insights page alike: `{ memory, cpu, memoryHeld,
|
|
424
|
-
* memoryFact, cpuFact }`, each a short clause (empty when it does not apply), such as `2 runs ended
|
|
425
|
-
* largest size killed 4g)` or `p95 peak 2560m, largest 3g, over 24 runs`. Ids and numbers only. A fact
|
|
426
|
-
* information and a held raise as what the host offers
|
|
427
|
-
* host's budget.
|
|
533
|
+
* memoryFact, cpuFact, memoryCeiling }`, each a short clause (empty when it does not apply), such as `2 runs ended
|
|
534
|
+
* oom-killed (the largest size killed 4g)` or `p95 peak 2560m, largest 3g, over 24 runs`. Ids and numbers only. A fact
|
|
535
|
+
* is worded as information and a held raise as what the host offers, and `memoryCeiling` (issue #865) points to a
|
|
536
|
+
* variable: none of them is a call, and none advises growing a host's budget.
|
|
428
537
|
*/
|
|
429
538
|
export function suggestionEvidence(suggestion) {
|
|
430
539
|
const m = suggestion?.memory ?? {};
|
|
@@ -457,5 +566,6 @@ export function suggestionEvidence(suggestion) {
|
|
|
457
566
|
memoryHeld: heldWords[m.held] ?? "",
|
|
458
567
|
memoryFact: m.fact === "pressure" ? `${me.pressured} of ${plural(me.samples, "run")} reached the memory limit while stalled for memory more than 1% of their time` : "",
|
|
459
568
|
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` : "",
|
|
569
|
+
memoryCeiling: m.reason === "oom-killed" ? oomCeilingWords(me) : "",
|
|
460
570
|
};
|
|
461
571
|
}
|
package/src/touch-outbox.mjs
CHANGED
|
@@ -54,6 +54,11 @@ export async function trackTouchAction({ store, outbox = null, event, act, succe
|
|
|
54
54
|
const writer = outbox ?? { begin: (e) => store.begin(e), applied: (intent) => store.write(intent.event), abort: (intent) => store.abort(intent.event) };
|
|
55
55
|
const intent = await writer.begin(event);
|
|
56
56
|
let result;
|
|
57
|
+
// Aborted on any throw, also one whose answer was lost (`valkeyMayStillRun`, issue #867): a cancel's removal that then
|
|
58
|
+
// lands is a stop not counted, so the stops count can be one short (the rate is unaffected, since stops never enter
|
|
59
|
+
// its denominator). REJECTED: keeping the intent pending as unknown. Nothing resolves it (a rerun finds no job, the CLI
|
|
60
|
+
// has no outbox, `repair()` only begins it again), so the store reads `touch-actions-unresolved` with its coverage reset
|
|
61
|
+
// to now, the Zero touch page reads unavailable, and every later tracked action renews its 30-day TTL.
|
|
57
62
|
try { result = await act(); } catch (error) { await writer.abort(intent); throw error; }
|
|
58
63
|
if (!succeeded(result)) { await writer.abort(intent); return result; }
|
|
59
64
|
const tracked = await writer.applied(intent);
|
package/src/valkey-endpoint.mjs
CHANGED
|
@@ -58,7 +58,7 @@ export function defaultValkeyContext() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Where a client stands (issue #464, gate round 3): `{ envPath, shared, rootRefused, error, password }`.
|
|
61
|
+
* Where a client stands (issue #464, gate round 3): `{ envPath, shared, rootRefused, error, absent, password, url }`.
|
|
62
62
|
*
|
|
63
63
|
* The owner decision itself does not depend on this (gate round 3's simpler rule): on Linux EVERY client refuses a
|
|
64
64
|
* listener held by a uid that is neither this account's euid, nor one of its subordinate uids, nor root, from any cwd.
|
|
@@ -86,10 +86,12 @@ export function valkeyClientContext({ env = process.env, cwd = process.cwd(), pl
|
|
|
86
86
|
const envPath = join(cwd, ".env");
|
|
87
87
|
let content = null;
|
|
88
88
|
let error = null;
|
|
89
|
+
let absent = false;
|
|
89
90
|
try {
|
|
90
91
|
content = readEnv(envPath);
|
|
91
92
|
} catch (err) {
|
|
92
|
-
if (err?.code
|
|
93
|
+
if (err?.code === "ENOENT") absent = true;
|
|
94
|
+
else error = `${envPath} could not be read (${err?.code ?? err?.message})`;
|
|
93
95
|
}
|
|
94
96
|
const fileKeys = {};
|
|
95
97
|
if (content !== null) {
|
|
@@ -104,7 +106,7 @@ export function valkeyClientContext({ env = process.env, cwd = process.cwd(), pl
|
|
|
104
106
|
if (keys.error) error ??= keys.error;
|
|
105
107
|
else for (const k of [VALKEY_SHARED_KEY, VALKEY_PASSWORD_KEY, "VALKEY_URL"]) if (typeof keys.keys[k] === "string") fileKeys[k] = keys.keys[k];
|
|
106
108
|
}
|
|
107
|
-
return valkeyContextFromKeys({ env, envPath, fileKeys, error, platform, rootRefused });
|
|
109
|
+
return valkeyContextFromKeys({ env, envPath, fileKeys, error, absent, platform, rootRefused });
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/**
|
|
@@ -138,9 +140,10 @@ export const VALKEY_CONTEXT_KEYS = Object.freeze(["VALKEY_URL", VALKEY_PASSWORD_
|
|
|
138
140
|
* A context from keys a caller has already read out of the deployment's `.env` (`fileKeys`, the `VALKEY_CONTEXT_KEYS`
|
|
139
141
|
* it supplied) and this process's environment. `valkeyClientContext` reads the file itself; the admin panel passes what
|
|
140
142
|
* issue #471's resolver read, so the panel has one `.env` reader. `error` is a reason the file could not be read, as
|
|
141
|
-
* `valkeyClientContext`'s.
|
|
143
|
+
* `valkeyClientContext`'s. `absent` (issue #845) is true when nothing is at `envPath`: no file, which is not a file that
|
|
144
|
+
* sets nothing, so a client that reads its password again at each connect keeps the one it has (`freshPassword`).
|
|
142
145
|
*/
|
|
143
|
-
export function valkeyContextFromKeys({ env = process.env, envPath = null, fileKeys = {}, error = null, platform = process.platform, rootRefused } = {}) {
|
|
146
|
+
export function valkeyContextFromKeys({ env = process.env, envPath = null, fileKeys = {}, error = null, absent = false, platform = process.platform, rootRefused } = {}) {
|
|
144
147
|
const fileVenue = typeof fileKeys.PI_BACKENDS === "string" ? { PI_BACKENDS: fileKeys.PI_BACKENDS } : {};
|
|
145
148
|
const shared = valkeySharedOn(fileKeys[VALKEY_SHARED_KEY]);
|
|
146
149
|
const filePassword = fileKeys[VALKEY_PASSWORD_KEY] || null;
|
|
@@ -156,7 +159,7 @@ export function valkeyContextFromKeys({ env = process.env, envPath = null, fileK
|
|
|
156
159
|
const engine = typeof env.PI_OPENSHELL_ENGINE === "string" ? env.PI_OPENSHELL_ENGINE : fileKeys.PI_OPENSHELL_ENGINE;
|
|
157
160
|
const derived = platform === "linux" && ((venues.podmanUsed && !venues.localUsed) || openshellSetupOf(venues, { PI_OPENSHELL_ENGINE: engine }).valkeyEngine === "podman");
|
|
158
161
|
const envUrl = typeof env.VALKEY_URL === "string" ? env.VALKEY_URL : null;
|
|
159
|
-
return { envPath, shared, rootRefused: typeof rootRefused === "boolean" ? rootRefused : derived, error, password: { environment: envPassword, file: filePassword }, url: { environment: envUrl, file: fileUrl } };
|
|
162
|
+
return { envPath, shared, rootRefused: typeof rootRefused === "boolean" ? rootRefused : derived, error, absent: absent === true, password: { environment: envPassword, file: filePassword }, url: { environment: envUrl, file: fileUrl } };
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/**
|