Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
0d8324d
WL-0MVK6S4HM005GBCO: Implementation complete
Oct 23, 2026
5d66255
WL-0MVK6S9TN00500UL: Implementation complete
Oct 23, 2026
f39b246
WL-0MVK6SD5O001K901: Implementation complete
Oct 24, 2026
5abdb3e
WL-0MVK6SDU90036BBX: Implementation complete
Oct 24, 2026
4348109
chore(standup): add standup report 2026_10_24.md
Oct 24, 2026
6c59820
WL-0MVM92MZ50075E6B: whitelist the agent identity for wl sync
Oct 24, 2026
7d07562
WL-0MVLJTP51003SME9: pin reservation handshake types, fixtures and co…
Oct 24, 2026
0f565c9
WL-0MVLJTR02007QL7Z: Implementation complete
Oct 24, 2026
11c8975
WL-0MVLJTSPP004PC6Z: one-per-idle-window reservation race guard
Oct 24, 2026
37e0b8c
WL-0MVLJTV03000KIQZ: Implementation complete
Oct 24, 2026
98e3304
WL-0MVLJTWVI004G4NX: Implementation complete
Oct 24, 2026
fe34744
WL-0MVK0UO3Q002TXW6: Implementation complete
Oct 24, 2026
041d4ea
chore(worklog): allow sorra@thewizardscode.com in sync identity allow…
Oct 24, 2026
92535bb
WL-0MVMSE2S0006L56D: Fix auto-sync isolation perf flake with in-windo…
Oct 25, 2026
f5aa729
WL-0MVLIHWOY003IP5E: Implementation complete
Oct 25, 2026
3f6ba3f
WL-0MVLIHXD1004L0UO: Implementation complete
Oct 25, 2026
6ece5fa
WL-0MVN6UUEH009GID8: Implementation complete
Oct 25, 2026
ee382b6
WL-0MVMSE5LN003KCPQ: Implementation complete
Oct 25, 2026
290fcf8
chore(standup): add standup report 2026_10_25.md
Oct 25, 2026
e0249ca
WL-0MVLIHYOU000YQ0R: Implementation complete
Oct 25, 2026
d861d77
WL-0MVNPNGIR000DBYM: Implementation complete
Oct 25, 2026
e2a2cf8
WL-0MVLIHY1P002WP0D: Implementation complete
Oct 25, 2026
3010d90
WL-0MVLIHZBT006B1UI: Implementation complete
Oct 25, 2026
2c7aa85
WL-0MVLIHZZI004D7EK: Implementation complete
Oct 25, 2026
fd9a997
WL-0MVNUMI1B0021IGI: Make pane-spawn failure-trace assertion portable
Oct 25, 2026
ba81ba2
WL-0MVLII0NO007JJBA: Implementation complete
Oct 25, 2026
629fb9e
WL-0MVLII1AO007TTRK: Implementation complete
Oct 25, 2026
2459b6f
WL-0MVNUHWH500196K1: Implementation complete
Oct 25, 2026
c7ec4a3
WL-0MVLII2080074S5L: extract downtime-worker orchestrator modules
Oct 25, 2026
7709bf6
WL-0MVLII2OY004S3BN: Implementation complete
Oct 25, 2026
40143a3
WL-0MVM9E3SV003III0: Implementation complete
Oct 25, 2026
ef537bf
WL-0MVGB23MM004X501: Implementation complete
Oct 25, 2026
4acdfad
WL-0MVG4FF9200346NR: Implementation complete
Oct 25, 2026
e406951
OSL-0MVOB9VGA005L9MI: resolve scan roots from config/env instead of a…
Oct 26, 2026
d6d1193
WL-0MVNUIW77000VYAX: Implementation complete
Oct 26, 2026
490d51d
WL-0MVOGBJ7V00973BI: Implementation complete
Oct 26, 2026
4f00724
WL-0MVG4FF9200346NR: fix canonical-suite coverage guard filter parsing
Oct 26, 2026
7af609d
WL-0MVOA0FN3000ND4J: Implementation complete
Oct 26, 2026
52b7c5b
WL-0MVOA0HYY005RG06: Implementation complete
Oct 26, 2026
2ec6b8d
WL-0MVOA0K6T009KTN1: Implementation complete
Oct 26, 2026
5e95642
WL-0MVOA0MBH004KKL0: Implementation complete
Oct 26, 2026
f30d3e8
WL-0MVOA0ODW002W5C4: Implementation complete
Oct 26, 2026
f31b8d9
WL-0MVOA0Q9W006WWZL: Implementation complete
Oct 26, 2026
1099b2f
WL-0MVOA0S1E0022UV7: Implementation complete
Oct 26, 2026
235f44b
WL-0MVOTIJ2U000JI44: Implementation complete
Oct 26, 2026
2619511
WL-0MVOTIJ2U000JI44: Implementation complete
Oct 26, 2026
be2d777
WL-0MVP643Q4007BZJ8: Implementation complete
Oct 26, 2026
e2de9eb
WL-0MVOBOX1U0013HNF: add database split decomposition guard
Oct 26, 2026
6977815
WL-0MVND6BL30066V6B: Implementation complete
Oct 26, 2026
b9535ef
WL-0MVOA0U84003EXVK: Implementation complete
Oct 26, 2026
a6df757
WL-0MVOBOYVT009UABL: extract database context foundation module
Oct 26, 2026
078ceb1
WL-0MVOA0W1P000IZKK: Implementation complete
Oct 26, 2026
cc04953
WL-0MVND6DHH008R3LG: Extract worklist constants, types and layout
Oct 26, 2026
34d7f0d
WL-0MVP9UDNI005IEIT: Implementation complete
Oct 26, 2026
4310569
WL-0MVND6F9V004IQ3V: Implementation complete
Oct 26, 2026
27cac1b
WL-0MVOBP0U5007BAZD: Implementation complete
Oct 26, 2026
bf46c7f
WL-0MVP9UFHF001FA2F: Implementation complete
Oct 26, 2026
f40fcd7
WL-0MVP9UHCF005XHR2: Implementation complete
Oct 26, 2026
4f0b383
WL-0MVP9MSJO0099V0G: Refactor guard: sync split invariants (test-firs…
Oct 26, 2026
9f4341d
WL-0MVP9PCOU004L8HN: extract sync types and git-exec primitives (leaf…
Oct 26, 2026
193e456
WL-0MVOBP2Q2002ZYCF: Implementation complete
Oct 26, 2026
87237be
WL-0MVP9UJ6S009LLK4: Implementation complete
Oct 26, 2026
1fec88c
WL-0MVP9UKYR006EQRT: Implementation complete
Oct 26, 2026
bce0f70
WL-0MVP9QLWU003NV41: extract merge predicates into src/sync/merge-pre…
Oct 26, 2026
feda83f
WL-0MVP9QLWU003NV41: extract field-level merge into src/sync/field-me…
Oct 26, 2026
0e615ba
WL-0MVP9QLWU003NV41: extract public mergeWorkItems into src/sync/merg…
Oct 26, 2026
58b97e2
WL-0MVP9QLWU003NV41: extract collateral merges into src/sync/merge-co…
Oct 26, 2026
0ce7c8d
WL-0MVOBP4J6003ZKML: Implementation complete
Oct 26, 2026
3a34585
WL-0MVP9R3PU004EGYE: extract refs helpers into src/sync/refs.ts
Oct 26, 2026
272b50e
WL-0MVP9R3PU004EGYE: extract repo-context helpers into src/sync/repo-…
Oct 26, 2026
7dd161c
WL-0MVP9R3PU004EGYE: extract prefix filter into src/sync/prefix-filte…
Oct 26, 2026
a4efe29
WL-0MVP9R3PU004EGYE: extract author-identity gate into src/sync/autho…
Oct 26, 2026
a0acda4
WL-0MVP9RW4Z006VLW1: extract temp-worktree helpers into src/sync/work…
Oct 26, 2026
fa72987
WL-0MVP9RW4Z006VLW1: extract remote fetch + delta replay into src/syn…
Oct 26, 2026
129bbae
WL-0MVP9RW4Z006VLW1: extract push + rewrite paths into src/sync/push.ts
Oct 26, 2026
05c5330
WL-0MVP9RW4Z006VLW1: re-point push --no-verify source assertion at sr…
Oct 26, 2026
7deb624
WL-0MVOBP6K0007574C: extract dependency and comment modules (move-only)
Oct 26, 2026
4679090
WL-0MVP9SEJZ000KWXJ: finalise sync barrel, empty split allowlist, doc…
Oct 26, 2026
d688a37
WL-0MVOBP6K0007574C: add dependency/comment extraction structural gate
Oct 27, 2026
5402857
WL-0MVPUDYHD000QI0K: Implementation complete
Oct 27, 2026
38233e2
WL-0MVPUDZ7P008UZGD: Implementation complete
Oct 27, 2026
4b0ee3b
WL-0MVPUE4SE004QYUT: Implementation complete
Oct 27, 2026
d028711
WL-0MVPUE4C9005380J: Implementation complete
Oct 27, 2026
95a1331
WL-0MVPUEFJ8008THX5: Implementation complete
Oct 27, 2026
a5ca33e
WL-0MVPUEFXN0092243: Document calibration-gated, occupancy-only prefi…
Oct 27, 2026
f2fa05d
WL-0MVPE2DXB00978T5: Implementation complete
Oct 27, 2026
0a7d0b7
WL-0MVP9RW4Z006VLW1: add worktree isolation doc referenced by the ext…
Oct 28, 2026
9163cd1
WL-0MVR8FI4Q0007CGR: Implementation complete
Oct 28, 2026
080e101
WL-0MVR8FKMU009XTED: Implementation complete
Oct 28, 2026
fc4909b
WL-0MVR8FN1D002SCIR: Implementation complete
Oct 28, 2026
c98ce02
WL-0MVR8FN1D002SCIR: Implementation complete
Oct 28, 2026
e4e4027
WL-0MVS5CZFT001GHK9: Implementation complete
Oct 28, 2026
598b829
WL-0MVSFQ3J4004SNJ9: bound dispatch-parity spawns so host contention …
Oct 28, 2026
26d5526
WL-0MVS9Z965006MC4R: Implementation complete
Oct 28, 2026
1abe961
WL-0MVSLXT79008D4X6: make repo-wide guard walks tolerate concurrent w…
Oct 28, 2026
2401d87
WL-0MVSI1RX2005O9NE: Implementation complete
Oct 28, 2026
2d1110e
WL-0MVSI1S5P003UAMB: Implementation complete
Oct 28, 2026
4927671
WL-0MVSBF5XZ001H7V7: Implementation complete
Oct 29, 2026
1bb6148
WL-0MVSSAIO5004ERW3: Implementation complete
Oct 29, 2026
d0229e2
WL-0MVSSAKAU001RC2Z: root-only top level for all items (remove critic…
Oct 29, 2026
a06a149
WL-0MVSSAM5H001VMQN: Implementation complete
Oct 29, 2026
00b43c9
WL-0MVTDXMXB005P99J: Implementation complete
Oct 29, 2026
1d8c366
WL-0MVSSANRK000V00S: Implementation complete
Oct 29, 2026
77566b1
WL-0MVSSAPIY0008ADC: Implementation complete
Oct 29, 2026
84ef065
WL-0MVTESPHB0021SI7: Implementation complete
Oct 29, 2026
ad42dc2
WL-0MVSNHCSM004JYEY: document local-preferred deferral verification a…
Oct 29, 2026
e4f8fbb
Bump version to v1.0.24
Oct 29, 2026
ae18263
Update CHANGELOG.md for v1.0.24
Oct 29, 2026
700a8f1
Merge origin/dev into main (automated)
Oct 29, 2026
4334c51
Regenerate derived version files for v1.0.24
Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .pi/test-config.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
{
"suiteCommands": [
"npm --silent test -- tests/cli",
"npm --silent test -- tests/unit"
"npm --silent test -- tests"
],
"timeoutPerCommand": 1800,
"timeoutPerCommand": 3600,
"groups": {
"herdr": {
"commands": ["npm --silent test -- packages/herdr/src"],
"paths": ["packages/herdr/**"]
"commands": [
"npm --silent test -- packages/herdr/src"
],
"paths": [
"packages/herdr/**"
]
},
"tui-extensions": {
"commands": [
"npm --silent test -- packages/tui/extensions"
],
"paths": [
"packages/tui/extensions/**"
]
}
}
}
20 changes: 20 additions & 0 deletions .worklog/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,23 @@ autoSync: false
githubRepo: TheWizardsCode/ContextHub
githubLabelPrefix: 'wl:'
githubImportCreateNew: true
# syncAllowedAuthors — finite allow-list for the `wl sync` author-identity gate
# (WL-0MTGDW58X007UJDS). `refs/worklog/data` is written by more than one
# environment, so its history legitimately carries commits from both the
# operator identity and the agent-store identity. The gate always allows this
# store's local `user.email` in addition to the addresses below, so listing both
# identities makes the gate pass whichever of them a given environment has
# configured. See docs/SYNC_IDENTITY_GATE.md.
#
# Kept here rather than in `.worklog/config.defaults.yaml`: that file is the
# project template (`projectName: TestProject`, `prefix: TEST`), so identities
# listed there would leak into every new project. Keep this list NARROW — never
# `null`/`true` (allow-all). Empty-author commits are refused unconditionally
# regardless of this setting. Add a new identity explicitly, under a work item,
# only when a new legitimate writer appears on the ref (WL-0MVM92MZ50075E6B).
syncAllowedAuthors:
- 250240+SorraTheOrc@users.noreply.github.com
- pi@local
# Operator's second environment (7 legitimate sync commits on
# refs/worklog/data); added under WL-0MVMZP6A5005PB66.
- sorra@thewizardscode.com
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ only (`wl update <id> --assignee <agent> --if-status completed --if-stage
in_review --if-assignee ""`), so the item stays `completed`/`in_review`. A
non-empty assignee is an intentional hold from auto-dispatch (producer sentinel
`wl update <id> --assignee Map`; clear `--assignee ""` to release); claims
clear the assignee on session end. See
clear the assignee on session end. An all-held dispatch window is surfaced
**read-only** in the worklist (header count + warning banner; never clears a
hold, never changes dispatch — WL-0MVR8FN1D002SCIR). See
[docs/dev/downtime-dispatcher.md](./docs/dev/downtime-dispatcher.md) and the
global instructions (`~/.pi/agent/AGENTS.md`).

Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## v1.0.24 (2026-10-29)
> **Release focus:** This release focuses on making background and downtime work more reliable and responsive, with smarter dispatch to local or remote capacity, automatic retries, and fixes for stalls, idle workers, and idle leaders. It also improves sync speed, memory safety during large database loads, save-data import handling, and overall stability during heavy activity.
### Features
- Syncing when nothing changed is now instant instead of taking minutes. (WL-0MVK3WRY9009KV6P)
- Downtime work now starts only when capacity is truly free, so jobs no longer stall or silently fall back. (WL-0MVIKRFP0005JDL8)
- Low-priority jobs now stay on the local slot, while urgent work keeps the fast remote route. (WL-0MVNUIW77000VYAX)
- Keeps the game running smoothly by preventing CPU overload during heavy activity. (WL-0MVK0UO3Q002TXW6)
- Unattended background work now runs on remote capacity by default instead of stalling when the local slot is busy. (WL-0MVNUHWH500196K1)
- Local-first work now waits for local capacity instead of failing, and resumes automatically when it frees up. (WL-0MVSI1JMN002CGC0)
- The full test suite now covers all tests, so fewer bugs slip through to release. (WL-0MVG4FF9200346NR)
- Downtime dispatch is now split into smaller, safer modules, making future fixes and improvements to background work more reliable. (WL-0MVF3F9A7000HKP7)
### Bug Fixes
- Worklog sync now accepts commits from the team's shared author, so updates flow again. (WL-0MVMZP6A5005PB66)
- View refreshes stay responsive even during long background syncs. (WL-0MVMSE2S0006L56D)
- Fixed a save-data issue that could cause imports to fail or duplicate entries. (WL-0MVM9E3SV003III0)
- Large database loads now stay within safe memory limits, preventing slowdowns and crashes. (WL-0MVMSE5LN003KCPQ)
- Fixed a rare issue where downtime activities could incorrectly report success instead of failure. (WL-0MVOTIJ2U000JI44)
- Idle leaders now step down so active players can take over, and peers no longer wait 60 minutes for work. (WL-0MVP7QHUB003ZA2A)
- Fixed a bug where running tasks in parallel could wipe shared game files and break the work tracker. (WL-0MVPADZ1Z000FOSG)
- Improved reliability of performance monitoring so lag spikes are detected more consistently. (WL-0MVS5CZFT001GHK9)
- Fixed slow startup times when launching the game repeatedly. (WL-0MVS9Z965006MC4R)
- Fixed a flaky test that could fail during automated checks, keeping builds reliable. (WL-0MVSLXT79008D4X6)
- Critical child items no longer appear twice in the worklist; their parent is shown instead. (WL-0MVN4WESM007T4XI)
- Idle workers now explain why they're not dispatching, and audits no longer get stuck behind the queue. (WL-0MVR8EXR6009F05V)
- Temporary local model congestion now retries automatically instead of ending your turn. (WL-0MVSBF5XZ001H7V7)
- Idle machines now start queued work instead of waiting forever. (WL-0MVPRZU0A000TSWD)
- Agents can now sync worklog data without manual workarounds. (WL-0MVM92MZ50075E6B)
- Work item titles now reliably appear in the footer, even for items from other projects. (WL-0MVGB23MM004X501)

## v1.0.23 (2026-10-23)
> **Release focus:** This release focuses on performance and responsiveness across the app, with faster `wl` commands, quicker tests, and lower CPU use in Herdr panes and the Work Items pane. It also improves GPU scheduling so downtime tasks start promptly and run one at a time, ensures your own dispatched work jumps ahead of background tasks, and fixes several reliability issues including late-joining agent status, test gate failure messages, and long-running AI sessions blocking overnight work.
### Features
Expand Down
31 changes: 30 additions & 1 deletion CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ path's dynamic entrypoints and their runtime module union, plus the measured
`src`/`dist` baselines and regression headroom. Background and re-run
instructions: [`docs/dev/wl-startup-import-budget.md`](docs/dev/wl-startup-import-budget.md).

**Missing shared build output.** Before anything resolves
`@worklog/shared/*`, the fast path checks that the workspace build output
(`packages/shared/dist`) actually exists and, when it does not, prints one
actionable line (the missing artifact, the absolute path checked and the fix)
and exits non-zero instead of an `ERR_MODULE_NOT_FOUND` stack trace. Recovery
is always the same:

```sh
npm run build:shared
```

The check resolves through the same `node_modules/@worklog/shared` link the
runtime uses, so a linked worktree without its own `dist/` (which resolves the
main checkout's build) never trips it. Details:
[`docs/dev/wl-shared-dist-diagnostic.md`](docs/dev/wl-shared-dist-diagnostic.md).

### Spawn instrumentation and CPU-seconds per refresh cycle

When `WL_SPAWN_COUNT_FILE=<path>` is set, every `wl` process appends one line
Expand Down Expand Up @@ -744,7 +760,9 @@ WL-0MVI0IT4S0070G78):
"worklogDir": "/abs/path/.worklog", // resolved worklog dir (honours --worklog-dir)
"requestedCount": 20, // the -n value that was used
"subsets": {
// priority critical, status open|in-progress|blocked|completed, ALL parent levels
// priority critical, status open|in-progress|blocked|completed, ALL parent
// levels — the discovery feed; the top-level-only rule lives in the Herdr
// consumer (see below)
"critical": [ /* WorkItem[] */ ],
// status completed|in-progress, stage in_review, root-only
"reviewQueue": [ /* WorkItem[] */ ],
Expand All @@ -765,6 +783,17 @@ same filter expressions and hierarchy ordering as the equivalent `wl list`
invocations. `browse` is item-for-item and order-for-order identical to
`wl next` with the same browse window and `--no-re-sort`.

The `critical` subset is deliberately **all parent levels** (it is the only
mandatory subset without `rootOnly`): it doubles as the discovery feed that
lets the Herdr layer find the parents of still-actionable critical children.
Rendering nothing but roots at the top level — critical items included — is a
Herdr-side rule: the plugin hides a critical child and substitutes its root
ancestor, so the work stays reachable without duplicating a row
(WL-0MVN4WESM007T4XI, `packages/herdr/README.md` § *Selection List
Behaviour*). Other consumers of `wl worklist` and of
`wl list --priority critical` (e.g. the ship critical-items gate) still see
child items.

### `next` [options]

Suggest the next work item(s) to work on. Non-actionable items (deleted, completed, in-review, in-progress, dependency-blocked) are excluded by default.
Expand Down
24 changes: 24 additions & 0 deletions LOCAL_LLM.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,30 @@ Prefer a hosted model (Foundry) candidates (tasks where larger-model reasoning,
- Double-check endpoint shape vs your OpenCode provider mode
- Ensure the API key is present in the environment OpenCode is using

### A local pane is refused with 429 “Local admission refused”

With `X-Local-Preference: local-preferred` the llama-proxy **defers** local
work instead of pinning it: when every local slot is busy it answers `429`
with `code: local_admission_reject`, `retryable: true` and a `Retry-After`
hint. That is capacity backpressure, not a rate limit — the recovery module
in the Worklog pi extension retries after the hinted wait and the turn
resumes once a local slot frees up (`packages/tui/extensions/README.md`,
“Local-Admission Refusals”).

What you should see and do:

- **The pane waits, then continues on the local model.** Nothing to do; the
wait is honoured in full, with no cloud fallback.
- **`retryable: false` with `prefill_budget_exceeded` / `context_too_large`.**
The prompt is above the admission budget and will *never* be admitted as-is;
the refusal is terminal and the proxy's `suggested_action` (compaction)
applies. Compact the context (or start a fresh session) rather than waiting
for a slot.
- **Confirm the route afterwards:** each turn appends one line to
`<worklog-root>/.worklog/local-route.log` with the preference sent, the route
the proxy took and any fallback reason. `route: "local"` after a deferred
wait is the expected end-to-end sequence.

---

## Appendix: WSL networking setup for Foundry Local
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ The repository includes a Pi extension that auto-loads into every pi session
agent-side capabilities that complement the Herdr plugin: an activity
indicator and session health/model display in the pi footer, guardrails that
protect worklog database files, a `skill_path` tool, automatic error recovery
(`/retry`), and proactive Local Proxy model lease release. Work item browsing
(`/retry`), and proactive Local Proxy model lease release. Work item titles in
the footer resolve across sibling projects (an ID from another project's
worklog is looked up with the project's own `--worklog-dir`, via the same
prefix-to-sibling scan the skills layer uses — see
[`docs/wl-integration.md`](docs/wl-integration.md)). Work item browsing
and management is provided by the [Herdr plugin](packages/herdr/).

Install the extension globally:
Expand Down
103 changes: 91 additions & 12 deletions bench/startup-budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@
* under contention, so the pass/fail gate is CPU time: `process.resourceUsage`
* (`user + system`) is recorded per process by the F1 spawn instrumentation
* (WL-0MVFRR98A008MM19) and is load independent. The recorded p95 CPU time is
* compared with `cpuHeadroomPct` headroom; wall time and peak RSS are checked
* against the same budget with their own (wider) documented headroom. Every
* measurement must also be a genuine fast-path invocation — the expected spawn
* kind (`cache-hit`, or `version`) must be present for every sample.
* compared with `cpuHeadroomPct` headroom; wall time is checked against the
* recorded budget with its own (wider) documented headroom, **normalised by
* the host-load factor the same run's bare-node control measures** — a host
* that measures N times slower at its bare-node floor lifts the wall budget
* N times, so a saturated host cannot flake the ratchet (the
* WL-0MVS9Z965006MC4R failure). A faster host never tightens the budget
* (factor clamped at 1) and a wild control sample never masks a real wall
* regression (`maxHostLoadFactor` clamp, default 8). Peak RSS keeps its own
* headroom; every measurement must also be a genuine fast-path invocation.
*/

import * as fs from 'fs';
Expand Down Expand Up @@ -96,7 +101,19 @@ export interface StartupAc1Targets {
note: string;
}

/** Host-load tolerance policy for the measured regression test. */
/**
* Host-load factor the wall budget is normalised by when the same run's
* bare-node control measures the host N times slower than the baseline did.
*
* Clamped to this maximum so a wild control sample (a one-off stall) cannot
* lift the wall limit so far that a real wall-path regression would be masked:
* CPU time remains the primary, load-independent regression gate.
*/
export const MAX_HOST_LOAD_FACTOR = 8;

/**
* Host-load tolerance policy for the measured regression test.
*/
export interface StartupRegressionPolicy {
/** Sample count the measured regression test uses. */
runs: number;
Expand All @@ -111,6 +128,11 @@ export interface StartupRegressionPolicy {
rssHeadroomPct?: number;
/** Absolute wall-time slack (ms) on top of the percentage headroom. */
wallFloorMs: number;
/**
* Upper bound on the host-load factor (default `MAX_HOST_LOAD_FACTOR`).
* Never below 1; a faster host never tightens the wall budget.
*/
maxHostLoadFactor?: number;
note: string;
}

Expand Down Expand Up @@ -171,10 +193,13 @@ export function defaultStartupBudget(): StartupBudget {
cpuHeadroomPct: 50,
wallHeadroomPct: 200,
wallFloorMs: 250,
maxHostLoadFactor: MAX_HOST_LOAD_FACTOR,
note:
'CPU time (process.resourceUsage user+system, from the F1 spawn records) is the ' +
'load-independent regression gate: a shared/loaded host inflates wall time but not ' +
'the process CPU consumed. Wall and peak RSS get wider documented headroom.',
'the process CPU consumed. Wall and peak RSS get wider documented headroom; the wall ' +
'budget is also normalised by the host-load factor measured by that run\'s bare-node ' +
'control (clamped to maxHostLoadFactor).',
},
baselines: {},
};
Expand Down Expand Up @@ -313,6 +338,11 @@ export interface BudgetEvaluation {
target: string;
checks: BudgetCheck[];
violations: string[];
/**
* Host-load factor the wall budget was normalised by (1 = the host matched
* the baseline's bare-node floor; > 1 = the host measured slower).
*/
hostLoadFactor: number;
}

/** The subset of a benchmark result the policy needs. */
Expand All @@ -328,6 +358,8 @@ export interface BudgetEvaluationInput {
p95CpuMs: number | null;
p95RssMb: number | null;
}>;
/** Same-run bare-node control (the host-load reference for wall time). */
bareNode?: { p95WallMs: number } | null;
}

/** Format a check failure for the violation list. */
Expand All @@ -338,14 +370,43 @@ function violationText(check: BudgetCheck): string {
);
}

/**
* Host-load factor: how much slower the host was during this run than when
* the baseline was captured, measured by the same run's bare-node control.
*
* `1` when either reference is missing, the host measured at or faster than
* the baseline (a faster host never tightens the budget), or the ratio would
* exceed the `maxHostLoadFactor` clamp (a wild control sample must not mask a
* real wall regression — CPU time remains the load-independent gate).
*/
function hostLoadFactor(
result: Pick<BudgetEvaluationInput, 'bareNode'>,
baseline: Pick<StartupBaseline, 'bareNode'>,
maxFactor: number
): number {
const current = result.bareNode?.p95WallMs;
const recorded = baseline.bareNode?.p95WallMs;
if (
current === undefined || current === null || current <= 0 ||
recorded === undefined || recorded === null || recorded <= 0
) {
return 1;
}
const factor = current / recorded;
if (!(factor > 1)) return 1;
return Math.min(factor, Math.max(1, maxFactor));
}

/**
* Evaluate measured startup numbers against the recorded budget.
*
* Rules (in order):
* 1. every scenario must have a recorded baseline for the measured target;
* 2. every sample must be a genuine fast-path invocation (expected spawn kind);
* 3. p95 CPU time must stay within `cpuHeadroomPct` (the host-load-tolerant gate);
* 4. p95 wall time must stay within `wallHeadroomPct` + `wallFloorMs` slack;
* 4. p95 wall time must stay within `wallHeadroomPct` + `wallFloorMs` slack,
* after normalising the recorded wall budget by the host-load factor the
* same run's bare-node control measured;
* 5. p95 peak RSS must stay within `cpuHeadroomPct` headroom;
* 6. when `ac1Targets.enforced` and the measured target is the recorded
* `measuredTarget`, the parent AC1 numbers are asserted too.
Expand All @@ -358,6 +419,11 @@ export function evaluateStartupBudget(
const violations: string[] = [];
const policy = budget.regression;
const baseline = budget.baselines[result.target];
const loadFactor = hostLoadFactor(
result,
baseline ?? { bareNode: undefined },
policy.maxHostLoadFactor ?? MAX_HOST_LOAD_FACTOR
);

const push = (check: BudgetCheck): void => {
checks.push(check);
Expand All @@ -369,7 +435,7 @@ export function evaluateStartupBudget(
`no recorded baseline for target '${result.target}' in ${BUDGET_RELATIVE_PATH} — ` +
`regenerate it with \`npm run benchmark:wl-startup -- --target ${result.target} --update-budget\``
);
return { ok: false, target: result.target, checks, violations };
return { ok: false, target: result.target, checks, violations, hostLoadFactor: 1 };
}

for (const scenario of result.scenarios) {
Expand Down Expand Up @@ -410,16 +476,23 @@ export function evaluateStartupBudget(
});
}

const wallLimit = recorded.p95WallMs * (1 + policy.wallHeadroomPct / 100) + policy.wallFloorMs;
const wallLimit =
recorded.p95WallMs *
loadFactor *
(1 + policy.wallHeadroomPct / 100) +
policy.wallFloorMs;
push({
scenario: scenario.name,
metric: 'p95WallMs',
measured: scenario.p95WallMs,
limit: Math.round(wallLimit),
ok: scenario.p95WallMs <= wallLimit,
detail:
`recorded p95 ${recorded.p95WallMs} ms + ${policy.wallHeadroomPct}% wall headroom ` +
`+ ${policy.wallFloorMs} ms floor`,
`recorded p95 ${recorded.p95WallMs} ms × host-load factor ` +
`${loadFactor.toFixed(2)} (bare-node control ${String(
result.bareNode?.p95WallMs ?? '-')
} / ${String(baseline.bareNode?.p95WallMs ?? '-')} ms) + ` +
`${policy.wallHeadroomPct}% wall headroom + ${policy.wallFloorMs} ms floor`,
});

const rssHeadroomPct = policy.rssHeadroomPct ?? policy.cpuHeadroomPct;
Expand Down Expand Up @@ -458,7 +531,13 @@ export function evaluateStartupBudget(
}
}

return { ok: violations.length === 0, target: result.target, checks, violations };
return {
ok: violations.length === 0,
target: result.target,
checks,
violations,
hostLoadFactor: loadFactor,
};
}

export interface FastPathAllowlistResult {
Expand Down
Loading
Loading