@vzn/vx-history 0.0.0 → 0.0.648

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VZN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,210 @@
1
1
  # @vzn/vx-history
2
2
 
3
- Placeholder. Part of [vx](https://github.com/vznjs/vx).
3
+ Website: <https://vznjs.github.io/vx/> · this plugin: <https://vznjs.github.io/vx/plugins/vx-history/> · source: <https://github.com/vznjs/vx>
4
+
5
+ History-based scheduling for [`@vzn/vx`](https://vznjs.github.io/vx/): a `schedule` plugin that orders ready tasks by their expected REMAINING critical-path duration — a task's own p50 plus the longest chain of dependents behind it — learned from the workspace's local run history. Zero dependencies.
6
+
7
+ A task's p50 is the median of its last five timings on the current branch, else on any branch, else unknown. The timings are shared by every worktree of the repo, so a new worktree orders by them on its first run.
8
+
9
+ ## Usage
10
+
11
+ ```sh
12
+ bun add -d @vzn/vx @vzn/vx-history
13
+ ```
14
+
15
+ ```ts
16
+ // .vx/workspace.ts
17
+ import { vxHistory } from '@vzn/vx-history'
18
+
19
+ export default { plugins: [vxHistory()] }
20
+ ```
21
+
22
+ `vxHistory({ window?, assume?, file?, url?, issuer?, repository?, headers?, resources?, memory?, reservations? })` learns from the last `window` invocations (default 20) through core's `LocalHistoryProvider` over the cache's own SQLite handle. With one worker and two chains of identical shape, the chain history says is slow starts first; with no history yet, the scheduler's structural order stands.
23
+
24
+ `assume` is for the run that has no history: a fresh CI runner. It names durations (task id → ms) for tasks the history has not seen, so a long leaf task — a docs build, a cross-compile — starts first instead of last:
25
+
26
+ ```ts
27
+ export default {
28
+ plugins: [vxHistory({ assume: { '@vzn/vx-docs#build': 30_000 } })],
29
+ }
30
+ ```
31
+
32
+ A recorded p50 always wins over an assumption, and assumptions never feed the workspace median: they are a hint for the cold run, not evidence. A duration that is not a number of ms from 0 to 2^53 (`Number()` of an unset variable is NaN; a larger one summed along a chain past any number) is dropped with one warning naming its task ids; the rest still order the run. The example is vx's own docs build, once the 29 s tail of a 99 s cold gate: ready from the second second and started last (2026-09-10).
33
+
34
+ ## Timings from a history server
35
+
36
+ `url` (or `VX_HISTORY_URL`), its base URL without `/v1`, names a
37
+ [`@vzn/vx-history-server`](https://github.com/vznjs/vx/tree/main/packages/vx-history-server),
38
+ over `https://` (`http://` only to `127.0.0.1`, `::1` or `localhost`: every
39
+ request carries the CI token).
40
+ On a CI job with a token (GitHub's OIDC token, which needs
41
+ `permissions: id-token: write` to set `ACTIONS_ID_TOKEN_REQUEST_URL`
42
+ and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, or a GitLab `id_tokens` entry
43
+ `VX_HISTORY_TOKEN` with the server's origin as `aud`; a 5xx or 429 from
44
+ GitHub's endpoint, or a 429, 502, 503 or 504 from the server, is asked
45
+ again up to three times), each run asks it
46
+ for its tasks' numbers, by task definition, in the refs the run reads,
47
+ as a remote cache does: its branch's, the branch it merges into, then
48
+ the default branch's (`baseBranch`, else git's or the CI's). A run
49
+ that reads no ref (a detached HEAD with no default branch) asks nothing.
50
+ The run starts on its local history and re-ranks the tasks not yet
51
+ started when they land; it never waits for them. At its end it sends the
52
+ timings of the tasks it ran, once: under DTE that is the orchestrator,
53
+ with each worker's measurements. The upload starts as the last task
54
+ ends, beside core's own end of run, and the plugin's teardown waits at
55
+ most 2 s more for it (core bounds a teardown at 3 s); one not done by
56
+ then is dropped and said, with how many timings it held. A task that shared a crowded worker's
57
+ cores is sent as long as it would have taken alone, and its cores at
58
+ most its machine's, as local history keeps them. A refusal is said with the server's
59
+ words; a refused token (401) also names the repository and issuer the
60
+ workspace declares and the audience it must carry, and the server is
61
+ off for the rest of the run. A CI job with no token says so once (on GitHub Actions,
62
+ `GITHUB_ACTIONS`, naming the permission).
63
+ A run with no token (off CI) reads only, from a server whose
64
+ `READ_ACCESS` is `public`, and sends nothing; a server refusing it
65
+ names the fix for the CI it is on (`GITHUB_ACTIONS`, `GITLAB_CI`).
66
+ `issuer` (the CI issuer's URL, as its tokens name it) and `repository`
67
+ (`org/name` there) are required with a server: the run's one query
68
+ names them, with its refs and every task hash, and nothing guesses
69
+ them; `VX_HISTORY_URL` alone, with neither declared, is said and not asked.
70
+ `headers` are sent on every request to the server, for a proxy in
71
+ front of it (a Cloudflare Access service token:
72
+ `{ 'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID, … }`); an
73
+ unset value is left out, and a name the request sets itself
74
+ (`authorization`, `content-type`, `content-length`) is refused. A
75
+ redirect is not followed (it would carry them on): it fails the request,
76
+ naming where it pointed.
77
+
78
+ ```ts
79
+ export default {
80
+ plugins: [
81
+ vxHistory({
82
+ url: 'https://history.example.com',
83
+ issuer: 'https://token.actions.githubusercontent.com',
84
+ repository: 'acme/app',
85
+ }),
86
+ ],
87
+ }
88
+ ```
89
+
90
+ Each timing is of the task as it was: its command and `env.define`
91
+ (`taskDefinitions`). A task whose command changed reads none of its old
92
+ timings; editing its inputs, outputs or deps keeps them. A forecast
93
+ reads this branch's last five timings first, then the newest the server
94
+ answered, then any branch's, then `assume`. Of a timing the server
95
+ answers vx keeps only what a server would store: a duration that is not a whole
96
+ count of ms skips the timing, cores not finite from 0 or RSS not a whole
97
+ byte count are dropped (`1e400` parses as Infinity: kept, it failed every
98
+ later run's schedule); core's history store keeps no such number either.
99
+
100
+ ## Timings between CI runners, without a server
101
+
102
+ A fresh CI runner has no history. `file` names a JSON file, from the
103
+ workspace root, that carries it: read before ordering, rewritten after
104
+ each run with every task's p50 (its directory made if absent), so a
105
+ CI cache keeps one file. Git-ignore it: a task whose inputs cover it (a
106
+ root project's `**`) would re-key on every rewrite, and a rewrite of a
107
+ file git does not ignore warns.
108
+
109
+ ```ts
110
+ export default {
111
+ plugins: [vxHistory({ file: '.vx-timings.json' })],
112
+ }
113
+ ```
114
+
115
+ ```yaml
116
+ # GitHub Actions
117
+ - uses: actions/cache@v4
118
+ with:
119
+ path: .vx-timings.json
120
+ key: vx-timings-${{ github.run_id }}
121
+ restore-keys: vx-timings-
122
+ ```
123
+
124
+ The file is `{ "version": 1, "tasks": { "<task id>": <ms> } }`, keys
125
+ sorted; an entry that is not a number of ms from 0 to 2^53 is dropped. A p50 recorded on this machine wins over it, and it wins over
126
+ `assume`; like `assume`, it never feeds the workspace median. A task the
127
+ run did not see keeps its time, one another writer put there while the
128
+ run ran included. A file that is not one warns and is read as empty, one
129
+ it cannot write warns and the run goes on; `--dry` writes
130
+ nothing.
131
+
132
+ ## Reservations learned from history
133
+
134
+ The same history answers what each task used: the runner records every
135
+ execution's CPU time and peak RSS. Core's only gate is the worker count;
136
+ this plugin's `admit` hook packs the finer numbers, and core never
137
+ learns them. Each task's reservation is the largest peak RSS in the
138
+ window times a headroom (1.25), rounded up to 64 MB; cores are never
139
+ learned (the parallelism a build shows is what the machine let it have,
140
+ and reserving the solo reading packed 92 real builds two wide on four
141
+ cores for a 21% longer run, 2026-09-15), only declared. A task is
142
+ admitted while its reservation and those of everything running beside
143
+ it fit the budgets —
144
+ cores are the run's worker count, memory is what this process may use
145
+ (the machine's total, capped by the cgroup limit a container runs
146
+ under) — and a task over a whole budget runs alone. A task with no execution in the
147
+ window reserves nothing and runs freely, as it would with no plugin; a
148
+ spike or a refactor ages out with the window. A cache hit counts too:
149
+ the producing execution's usage rides the artifact, so a fresh CI runner
150
+ that restored a task from a remote cache has its reservation on the
151
+ next run without ever having executed it.
152
+
153
+ ```ts
154
+ plugins: [
155
+ vxHistory({
156
+ // MB. The default is the machine's total capped by the cgroup limit; set it to budget below either.
157
+ memory: 8192,
158
+ resources: { headroom: 1.5 },
159
+ // Declared by hand for what history cannot size (a first run); a declared axis wins, one left out keeps what history learned.
160
+ reservations: { 'app#e2e': { cpus: 2, memory: 4096 } },
161
+ }),
162
+ ]
163
+ ```
164
+
165
+ `window`, `memory` and `headroom` must each be a finite number above 0 (`window` a whole one). One that is not — `Number(process.env.X)` with `X` unset is NaN, and no reservation fits a NaN budget, so every task that reserved memory would wait for an idle machine — runs on its default, named in one warning per run and in `vx history`. So does a declared `reservations` axis (`cpus`, `memory`): one that is not a number above 0 is not declared, so the axis keeps what history learned (a NaN held its task alone and every reserving task beside it waited); `0` declares nothing on that axis, silently. An entry that is not an object (`{ "app#build": 2048 }`) declares nothing and is named the same way. A key other than `cpus` and `memory` there, or `headroom` in `resources`, is refused when the config loads, as a misspelt option is.
166
+
167
+ `vx history [--format pretty|json]` (a verb this plugin adds; `--help`
168
+ prints its usage) shows what it learned per task and the reservation it
169
+ will pack, with the budgets it packs into. Like a run, it reads
170
+ each task as it is now: runs of an earlier command are not counted.
171
+
172
+ ```
173
+ $ vx history
174
+ history: last 20 runs · budgets 4 cores (the default worker count; --concurrency changes it per run) · 13567 MB (what this process may use)
175
+ task runs p50 peak rss cpu reserves
176
+ app#build 12 8.41s 612 MB 1.9× 768 MB
177
+ app#e2e 3 41.20s 1.4 GB 1.1× 4096 MB · 2 cores (declared)
178
+ lib#test 12 1.02s — 1.0× —
179
+ 3 tasks with no execution in the window reserve nothing
180
+ ```
181
+
182
+ `--format json` emits `{ window, budgets, tasks }` with every task's
183
+ `runs`, `p50DurationMs`, `maxPeakRssBytes`, `maxCpuParallelism`,
184
+ `reservation` and `declared`; `schemas/history.json` (shipped) is its
185
+ JSON Schema, held to the output by core's
186
+ `tests/plugin-json-schemas.unsafe.test.ts`. A cache hit's row carries the producing
187
+ execution's usage, so a task restored from a remote cache shows a
188
+ reservation on a machine that never executed it. A task lighter than
189
+ vx itself has no peak on record (the runner reports one only above its
190
+ own footprint, since Linux hands a lighter child the parent's figure)
191
+ and reserves nothing — what a task under vx's own footprint needs.
192
+
193
+ `resources: false` turns the learning off (declared `reservations` are
194
+ still packed). Admission control, not enforcement: nothing is
195
+ cgroup-limited or reniced, and a task that exceeds its reservation is
196
+ the job of `exec.timeout` and the OS. Design:
197
+ `docs/design/resource-estimates-2026-09.md` in `@vzn/vx`.
198
+
199
+ ## What it does and does not do
200
+
201
+ - It is an ORDERING hint over the scheduler's structural baseline (how many tasks wait on each one, directly or through others), merged as `VxPlugin.schedule` returns it: task id → weight. It never changes what runs, only which ready task runs first.
202
+ - Cache hits are not modelled as zero-cost: predicting cache state needs the key and a probe, which the scheduler handles at run time (a confirmed hit is backfill, never a critical-path task). The estimate is "what if everything ran", which is exactly the case where order matters.
203
+ - It fails open: a broken history read warns (`[vx] vx-history: ordering falls back to the baseline`) and leaves the baseline order and only the declared reservations. Observability never breaks a run.
204
+ - Cost: one history read per run, serving both hooks, paid only by workspaces that declare the plugin. Core applies no plugin by default.
205
+
206
+ The score is core's `criticalPath(nodes, history, assume?)` (`@vzn/vx`), which any `schedule` plugin may call. `resourceEstimates(nodes, history, headroom)`, `withDeclared(learned, declared)` and `admits(id, running, reservations, budgets)` are exported for tests and for policies that want the same scoring over another history source, with the types `VxHistoryOptions`, `ResourceEstimate` and `Budgets`.
207
+
208
+ ## History
209
+
210
+ Core's opt-in `predictive` mode until 2026-09-02, then core's one bundled plugin (`@vzn/vx/plugins/schedule-history`) until 2026-09-10, when it moved here so core ships no plugin at all.
package/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Root entry shim: Bun 1.4.0's `--compile` binaries resolve an on-disk
2
+ // package by `<pkg>/index.ts` and ignore package.json `exports` / `main`
3
+ // (see packages/vx/index.ts). Same module as the exports map names.
4
+ export * from './src/index.js'
package/package.json CHANGED
@@ -1,8 +1,49 @@
1
1
  {
2
2
  "name": "@vzn/vx-history",
3
- "version": "0.0.0",
4
- "description": "Placeholder; real releases are published by CI.",
3
+ "version": "0.0.648",
5
4
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/vznjs/vx.git" },
7
- "publishConfig": { "access": "public" }
5
+ "description": "History-based scheduling for vx — a `schedule` plugin that orders ready tasks by their expected remaining critical path, learned from the local run history.",
6
+ "keywords": [
7
+ "vx",
8
+ "vx-plugin",
9
+ "monorepo",
10
+ "scheduler",
11
+ "critical-path"
12
+ ],
13
+ "type": "module",
14
+ "main": "./src/index.ts",
15
+ "types": "./src/index.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.ts",
19
+ "import": "./src/index.ts"
20
+ }
21
+ },
22
+ "files": [
23
+ "index.ts",
24
+ "src",
25
+ "schemas",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "bun": ">=1.4.4"
31
+ },
32
+ "//": [
33
+ "@vzn/vx-history-server, @vzn/vx-devtools: the tests run the plugin against the real server, in process.",
34
+ "jose: the tests sign the CI tokens the server checks."
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "peerDependencies": {
40
+ "@vzn/vx": "^0.0.648"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/vznjs/vx.git",
45
+ "directory": "packages/vx-history"
46
+ },
47
+ "homepage": "https://github.com/vznjs/vx/tree/main/packages/vx-history#readme",
48
+ "bugs": "https://github.com/vznjs/vx/issues"
8
49
  }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "vx history --format json",
4
+ "description": "What @vzn/vx-history learned per task, and the reservation it packs.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["window", "budgets", "tasks"],
8
+ "properties": {
9
+ "window": { "type": "integer", "description": "recent runs per task the history reads" },
10
+ "budgets": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["cpus", "memory"],
14
+ "properties": {
15
+ "cpus": { "type": "integer", "description": "the run's worker count" },
16
+ "memory": { "type": "number", "description": "megabytes" }
17
+ }
18
+ },
19
+ "tasks": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": [
25
+ "id",
26
+ "runs",
27
+ "p50DurationMs",
28
+ "maxPeakRssBytes",
29
+ "maxCpuParallelism",
30
+ "reservation",
31
+ "declared"
32
+ ],
33
+ "properties": {
34
+ "id": { "type": "string", "description": "project#task" },
35
+ "runs": { "type": "integer" },
36
+ "p50DurationMs": { "type": ["number", "null"] },
37
+ "maxPeakRssBytes": { "type": ["integer", "null"] },
38
+ "maxCpuParallelism": { "type": ["number", "null"] },
39
+ "reservation": {
40
+ "type": ["object", "null"],
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "cpus": { "type": "number" },
44
+ "memory": { "type": "number", "description": "megabytes" }
45
+ }
46
+ },
47
+ "declared": {
48
+ "type": "boolean",
49
+ "description": "the reservation came from the options, not the history"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }