You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: two records that disagreed with the code (issue #103)
Neither changes behaviour. Both make the written record lie to the next
reader, which is how the #99 class of bug got in.
session.reason was documented as a CLOSED enum
INT-RUN-HISTORY-FILE-CONTRACT listed nine tokens and omitted promote-failed,
which promoteSession's outer catch returns on any fs fault during the swap and
which mergeSession writes straight into the record on the completed path. A
full disk or a permissions change mid-promotion would therefore have produced
a token the spec called impossible, and a reader checking the record against
the spec would have concluded the worker invented it.
Added, with a producer table, because the flat list is what made this hard to
see: three code paths write this one field (resolve, runner, promote) and a
refused promotion WINS over the other two. The table also records what the
list cannot show, that expired never arrives from the promote path, and that
promoted is a return value reaching no record.
no-key stays and is deliberately NOT in the enum. It is unreachable in a wired
worker: sessionKeyFor is total and binary, so resolveSession returns null
rather than a keyless session, and promoteSession is only ever called with
what prepare handed over. Kept as the DI-seam backstop for the same reason the
store's no-sessionsDir return is kept, and now carrying the same explanation
instead of being a bare one-liner. A token no wired worker can emit does not
belong in the record's vocabulary.
processor.mjs's cold-start list named "no key" for the same reason, and a job
with no key gets no mount and no reason at all; promote-failed is the token
that actually belongs there. docs/sessions.md, the operator-facing mirror,
gains the promote-path pair, why a refused promotion wins, and the two values
that are not cold starts at all.
Pinned by a fault-injection test that faults renameSync, the one call only the
promote path makes, so the lock is already held when it blows up: the reason
is promote-failed, the canonical transcript is untouched, and the lock does
NOT outlive the failure. A leaked lock would cold-start every future run for
that key, quieter and worse than the fault itself.
A comment that contradicted its own code, three lines below
loader.mjs said staged pi packages "ride this same option" as
PI_GLOBAL_ALLOW_EXTENSIONS. The spread is unconditional and the rest of the
system agrees it should be: the worker emits PI_PACKAGES independently of the
opt-out, having already applied the per-trigger run.packages decision, so
re-gating here would withhold packages the operator did arm. The comment was
the defect, and it existed twice, mirrored verbatim into
INT-SDK-SESSION-OPTIONS. Both corrected to state the split and why, since this
is exactly what someone reads before answering "how do I stop all third-party
extension code loading in my containers" and the honest answer is BOTH
switches.
No test covered the false case: allowGlobalExtensions appeared once in the
whole loader suite, as true. Added, and verified by mutation. Gating
packagePaths on the option, the "fix" the old comment invited, now fails
instead of silently withholding every staged package from every job on every
deployment that sets the opt-out, on a clean exit 0.
INT-SESSION-STORE-CONTRACT UNCHANGED, checked: its write-path prose names no
reason tokens, so this drift was only ever visible from the record contract.
Verified with a live Valkey in the CI posture
(PI_DISPATCH_REQUIRE_{LOADER,WORKER,RECEIVER}_TESTS=1): 1959 tests, 0 failures,
0 skipped.
Signed-off-by: Rob Boerman <robboerman@live.nl>
"bytes": <int> | null } | null } // null when the job had no session at all
1574
1577
```
1575
1578
Field order is the serialisation order (`JSON.stringify` emits insertion order). The filename uses the
1576
1579
**sanitized** id (`:` → `_`, because `repeat:<sched>:<millis>` is NTFS-illegal); the record **body**
1577
1580
keeps the raw `jobId`. `reason` is a fixed enum passed through from the terminal outcome — never
1578
1581
free-form and never payload text — and `turns` is `null` when the container died before emitting the
1579
1582
runner `exit` line.
1583
+
1584
+
`session.reason` reads as one flat list but has **three producers**, which is why a token can look
1585
+
unreachable from whichever half of the code you happen to be in:
1586
+
1587
+
| Producer | Tokens |
1588
+
|---|---|
1589
+
| **resolve path**, host-side, before the container (`readCanonical`) | `resumed`, `absent`, `expired`, `too-large`, `unparseable`, `not-a-regular-file`, `pi-version-changed` |
1590
+
| **runner**, in the container (`image/runner/src/session.mjs`) | `disabled` (every unarmed job), `resumed`, `absent`, `unparseable` |
1591
+
| **promote path**, only on a `completed` exit (`promoteSession`) | `absent`, `not-a-regular-file`, `too-large`, `locked`, `promote-failed` |
1592
+
1593
+
A refused promotion **wins** over the other two (`mergeSession`, `worker/src/processor.mjs`): on a
1594
+
completed run it is the more useful reason, because it says why the NEXT run for this key will cold
1595
+
start. Three things follow that the list cannot show. `expired` never arrives from the promote path,
1596
+
which checks the file but not the TTL. `promoted` is a `promoteSession` return value that reaches no
1597
+
record, because the merge reads a promotion's reason only when it refused. And `promote-failed` is the
1598
+
one an operator meets in the wild: a full disk or a permissions change mid-promotion produces it.
1599
+
`promoteSession`'s remaining return, `no-key`, is deliberately **absent from this enum** and cannot
1600
+
reach a record — it is a DI-seam backstop, unreachable in a wired worker for the same reason the
1601
+
store's own no-`sessionsDir` return is, since `sessionKeyFor` is total and binary and `resolveSession`
1602
+
therefore returns `null` rather than a keyless session.
1580
1603
- **Why**: The admin extension is a separate process (`DES-ADMIN-VIA-PI-EXTENSION`) that reads this as a
1581
1604
read-model it does not share memory with — the worker writes the files, the admin extension reads them, and
1582
1605
nothing crosses in RAM. The worker writes on both terminal paths: `worker/src/index.mjs` `makeProcessor`
@@ -1995,6 +2018,7 @@ recorded repair is re-running `/dispatch setup` (or editing the pointer by hand)
1995
2018
1996
2019
| Date | Change |
1997
2020
|---|---|
2021
+
| 2026-08-08 | Issue #103, two records that disagreed with the code. **INT-RUN-HISTORY-FILE-CONTRACT**: the nested `session.reason` enum was documented as CLOSED while omitting `promote-failed`, which `promoteSession`'s outer catch returns on any fs fault during the swap and which `mergeSession` writes straight into the record on the completed path — a full disk would have produced a token the spec called impossible. Added, together with a producer table, because the enum reads as one flat list while three separate code paths write it (resolve, runner, promote) and a refused promotion WINS over the other two. Recorded three things the list cannot show: `expired` never arrives from the promote path, `promoted` is a return value that reaches no record, and `no-key` is deliberately NOT in the enum — a DI-seam backstop unreachable in a wired worker, since `sessionKeyFor` is total and binary so `resolveSession` returns `null` rather than a keyless session. Pinned by a new fault-injection test in `worker/test/session-store.test.mjs`. **INT-SDK-SESSION-OPTIONS**: the `additionalExtensionPaths` comment claimed operator-staged pi packages "ride this same option" as `PI_GLOBAL_ALLOW_EXTENSIONS`; the spread is and remains UNCONDITIONAL, so the comment was the defect, in both this file and `image/runner/src/loader.mjs`. Corrected to state the split and why (the worker applies `run.packages` before emitting `PI_PACKAGES`, so re-gating here would withhold what the operator armed), and the previously untested `allowGlobalExtensions: false` case is now pinned in `image/runner/test/loader.test.mjs`. **INT-SESSION-STORE-CONTRACT UNCHANGED, checked** — its write-path prose names no reason tokens, so the drift could only ever have been visible from the record's own contract. |
1998
2022
| 2026-08-07 | Issue #102: **INT-PI-PACKAGES-FILE-CONTRACT** records that `pi-packages.json` is now the override-and-addition layer rather than the only source (discovery reaches the same validator, so it adds candidates and never exemptions), and that the receipt gained `from` as a CLOSED enum with a default — which covers both compatibility directions at once, since a pre-#102 receipt carries no `from` and reads as declared while an older worker drops it as an unknown key. Also records that the receipt is now read at EACH job start rather than once at boot, why the boot read was right until discovery made re-staging routine, and why a failed read keeps last-known-good instead of degrading to none (an empty set emits no `PI_PACKAGES`, so the runner's path assertion would have nothing to refuse and the job would run toolless on a clean exit 0). **INT-CONTAINER-RUNTIME-CONTRACT, INT-TRIGGERS-FILE-CONTRACT, INT-CONTAINER-JOB-INPUTS UNCHANGED, checked** — the mount, `PI_PACKAGES`, `run.packages` and the pre-spend refusal are all untouched; only who fills the manifest, and how often it is read, moved. |
1999
2023
| 2026-08-04 | Documentation audit fallout (issue #99). **INT-TRIGGERS-FILE-CONTRACT** amended on `run.resume`, which this file had described as carried on **ALL FOUR** kinds for a month while the wiring covered three: `resolveSession` is handed to the forge preparers only, so a cron job with the flag armed staged no transcript, mounted no `/session`, promoted nothing and exited `0` as though it had. That is the flag's own believed-on-while-off inversion reached through the wiring rather than through a truthy `"false"` string, so the fix is `run.replicas`' fix: **refused at load**, worded *not yet covered* rather than impossible, because `session-key.mjs` already derives the local key from the scheduler id and nothing reaches it. Only `true` is refused — `false` and absent still validate and still land in `data` byte-identically, since `false` is the documented default and refusing an operator for writing down present behaviour would also change a shape pinned as byte-identical; the asymmetry with `run.replicas` (which refuses ANY value on cron) is recorded rather than left to read as an oversight, `1` being a no-op flag where `false` is the truth. The same bullet gains the **pre-spend** half, which the triggers file cannot answer by construction: whether a session store exists is deployment state, not file content, so an armed trigger under a deployment with no `PI_SESSIONS_DIR` is refused per delivery and `doctor` keeps the load-time warning. **INT-RUN-HISTORY-FILE-CONTRACT**: the `reason` enum gains one token, **`sessions-dir-unset`**, on `job-image-missing`'s precedent — a policy outcome with `budgetReserved: false`, since it is answered from two values already in hand before the mint, the branch check, the clone, the token-cap read and the budget INCR. Its shape follows `settings-overlay-invalid`'s (`<config artifact>-<its bad state>`) and its words are the spec's own, so the token greps to the text that mandates it. The nested `session.reason` enum's **`disabled`** was audited as unimplemented and is **UNCHANGED, checked**: the runner produces it (`image/runner/src/session.mjs`) whenever no session file is mounted, which is every unarmed job, so the entry was right and the audit finding was wrong. **INT-SESSION-STORE-CONTRACT UNCHANGED, checked**: the store's own no-`sessionsDir` return is now unreachable in a wired worker and kept as the DI-seam backstop, which changes no byte of its contract. |
2000
2024
| 2026-08-04 | The panel learns to find a deployment built elsewhere (issue #92). Added **INT-DEPLOYMENT-POINTER-CONTRACT**: `<agent dir>/pi-dispatch-deployment.json` (override `PI_DISPATCH_DEPLOYMENT_FILE`), an allowlisted absolute-paths-only env map layered UNDER the operator's env once at extension load — env wins key by key, the worker/receiver never read it, and `PI_DISPATCH_RUN_ROOTS`/credentials in the file have no effect by construction (a pointer that widened the AI-run allowlist would be a second unreviewed door to a gated capability). Deliberate divergence from INT-SUBSCRIPTIONS' loud version refusal, recorded in the entry: a broken/newer pointer degrades to exactly the pre-pointer behavior with a one-line surfaced notice, never a throw — the read-model's never-throw doctrine outranks fail-loud here because the pointer is an availability aid, not a data file. **INT-SUBSCRIPTIONS-FILE-CONTRACT / INT-CONFIG-OVERLAY-CONTRACT UNCHANGED, checked**: the pointer changes how their Locations are *found*, not what the files contain. |
0 commit comments