feat: runner-aware tools#346
Draft
branchseer wants to merge 24 commits intofeat/output-restorationfrom
Draft
Conversation
4ba6a19 to
64f1651
Compare
Member
Author
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
7 tasks
- vite_task_ipc_shared: shared protocol (Request/GetEnvResponse, NativeStr) - vite_task_server: per-task IPC server (Handler trait + Recorder) - vite_task_client: sync Rust client - vite_task_client_napi + @voidzero-dev/vite-task-client: node addon + JS wrapper - vite_task: wire IPC server into spawn; inject VP_IPC + VP_RUN_NODE_CLIENT_PATH; bundle with fspy via Tracking struct; materialize .node addon on first use Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 6 of docs/runner-task-ipc/plan.md. - Apply `ignoreInputs` to filter inferred fspy reads (directory-aware) - Apply `ignoreOutputs` to filter auto-detected writes (overlap check + archive) - Short-circuit cache update on `disableCache()` via new `CacheNotUpdatedReason::ToolRequested` - Embed `tracked: true` envs in `PostRunFingerprint.tracked_envs`; validate on lookup by comparing against the current parent env - Recorder env_map sources from `std::env::vars_os()` so tools can resolve envs the user never declared - Bump cache schema to 13 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New fixture `ipc_client_test` exercises each IPC method through the JS
wrapper (@voidzero-dev/vite-task-client) inside a real cached task:
- ignoreInput → the ignored dir can mutate without invalidating cache
- ignoreOutput → read-write overlap under an ignored dir still caches
- disableCache → forces re-execution on next run
- fetchEnv(tracked: true) → env change invalidates cache; same value hits
The e2e harness now copies packages/vite-task-client into each staging
node_modules so fixtures can `import { ... } from "@voidzero-dev/vite-task-client"`
without pnpm install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies a small pnpm patch to vite 8.0.8 that auto-injects a runner-aware
plugin at plugin-resolution time. When `VP_RUN_NODE_CLIENT_PATH` is set
(i.e. the child runs under `vp run`), the plugin:
- `ignoreInput(outDir)` — suppress fspy reads of the output dir (emptyDir
scans dist/ before writing)
- `ignoreInput/Output(<root>/node_modules)` — machine state (pnpm store +
vite's `.vite`/`.vite-temp` caches) is not user input/output
- `getEnv("NODE_ENV", true)` — tracked; drives DCE and define replacements
New e2e fixture `vite_build_cache` proves `vt run --cache build` produces
a cache hit on the second run and restores `dist/assets/main.js` after
deletion, all with zero manual input/output configuration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Extensions
Rework `patches/vite.patch` to match the shape of the eventual upstream
Vite PR:
- Drop the synthetic `vite:runner-aware` plugin. Each IPC call is now
inlined right at the Vite code that triggers the fs / env access:
- `ignoreInput(outDir)` in `prepareOutDir` before `emptyDir` scans it
- `ignoreInput(depsCacheDir)` + `ignoreOutput(depsCacheDir)` in
`loadCachedDepOptimizationMetadata` before the dep optimizer cache
is read / written
- `fetchEnv("NODE_ENV", { tracked: true })` in `resolveConfig` before
`process.env.NODE_ENV` is first consulted
- `ignoreInput`/`ignoreOutput` of `.vite-temp/` in
`loadConfigFromBundledFile` (bundled-config temp write+import)
- Static `import` of `@voidzero-dev/vite-task-client` by name — the
wrapper no-ops when no runner is connected, so no guard is needed at
the call sites.
- Add a `packageExtensions` entry in `pnpm-workspace.yaml` that injects
the wrapper as a real dependency of Vite. The final upstream PR would
instead declare it in `packages/vite/package.json`; the only delta
between experiment and PR is that one line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous snapshot embedded Vite's minified JS output, which would churn on every Vite version bump. Add a tiny `vtt stat-file` helper that reports `exists` / `missing` and use that instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Demonstrates end-to-end that Vite's patched `fetchEnv("NODE_ENV", { tracked: true })`
reaches the runner: flipping NODE_ENV between runs yields `tracked env
'NODE_ENV' changed`, while holding it constant still produces a cache hit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous case proved the cache invalidated when NODE_ENV flipped, but not that the tool actually used the new value. Source now carries a `process.env.NODE_ENV` branch whose marker (`BUILD_MODE_PROD` / `BUILD_MODE_DEV`) is DCE-pruned by Vite's define + minifier, so only the branch matching the current mode survives in the output. Add a `vtt grep-file` helper to inspect the bundle without dumping its whole (minified) body into the snapshot, and assert both markers against the production and development builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nv caching Makes the effect of NODE_ENV changes visible in `dist/assets/main.js`: the bundle contains only the surviving literal (`PROD build` or `DEV build`) after Vite's define-plugin substitution + DCE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…olves Also update the inspection hint in the comment to match the default `dist/assets/index-<hash>.js` filename now that vite.config.js is gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… fields Follows the convention introduced in main (#347): per-`[[e2e]]` and per- step descriptions use the TOML `comment` field instead of bare `#` lines, so they render under the snapshot headings and inside each step's block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The accept loop's `tokio::select!` could exit via the shutdown branch before ever observing a connection that had already been established at the kernel level, so fire-and-forget clients that connect, write, and exit right before the runner signals stop_accepting would silently lose their requests. After the main loop exits we now do one non-blocking `poll!` of `listener.accept()` per iteration until it returns Pending, ensuring every backlog-queued connection gets its handle_client future pushed and drained. Also: - drop the now-redundant `crates/vite_task_client_napi/tests/e2e.rs`; the IPC path is covered end-to-end by the `ipc_client_test` fixture plus `vite_build_cache` - oxfmt the fixture scripts and the JS wrapper Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64f1651 to
f6605e3
Compare
0008bd7 to
994624a
Compare
…ests Two CI fixes rolled together: 1. `cargo-shear --deny-warnings` failed after the removal of `vite_task_client_napi/tests/e2e.rs`: the crate still listed the tests's deps (rustc-hash, tokio, vite_task_server, vite_path) and the workspace still referenced `vite_task_client_napi` in non-shear-aware ways. Drop those deps from the napi crate and add `vite_task_client_napi` to the workspace-level cargo-shear ignore list (same rationale as fspy_preload_*: it's an artifact dep loaded by string name, not `use`-d in Rust). 2. Revert the speculative server-side drain-accept loop — on Windows the interprocess Listener's named-pipe implementation crashed the integration test binary at startup (no tests even ran). Instead, have each fire-and-forget test end with a tiny `flush(&client)` round-trip (a cheap `get_env` that waits for a response). Since frames on a single stream are read sequentially by the server, once the flush's response returns, every preceding fire-and-forget frame has definitely been dispatched to the handler — no server-side race fix needed. 10/10 repeat runs pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`cargo-shear 1.11.1 --deny-warnings` treats the 'test = true on lib target X but source contains no tests' messages as errors. Add `test = false` (plus `doctest = false` where missing) to the `[lib]` sections of the four IPC crates so cargo does not generate empty test harnesses for them. Integration tests in `tests/*` are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's `RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items` fails on the `[`SpawnFingerprint`]` link in `collect_tracked_envs`'s docstring — it's not in scope at that site. Rewrite the prose to drop the link; no information lost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
typos 1.45.1 rejected the PR because: - `./patches/vite.patch` includes Vite's own hunk-header line containing a truncated identifier (`environmen`) that looks like a typo but isn't ours to fix. Add `patches` to `.typos.toml` extend-exclude. - `docs/runner-task-ipc/index.md:39` had a real typo `respone` → `respond`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c abs paths On Windows, forward-slash paths without a drive letter (`/tmp/x.txt`) are RELATIVE, so the client's `resolve_path` joined them with the cwd (`D:\...\tmp\x.txt`) and the server-side assertion blew up. Use `/tmp/` on unix and `C:\tmp\` on windows so the paths are absolute on each platform and reach the server unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows CI these ignored tests crash their child processes with "failed to start the persistent thread of the Interprocess linger pool: Access is denied" from interprocess 2.4 as soon as the Node addon's client connects. The server-side unit tests on Windows already cover the IPC protocol; the crash is a downstream interprocess crate issue that doesn't affect our code paths. Add `platform = "unix"` so the ignored suite passes on Windows CI, with a comment pointing at the upstream root cause. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cpojer
reviewed
Apr 20, 2026
| * @param {string} name | ||
| * @param {{ tracked?: boolean }} [options] | ||
| */ | ||
| export function fetchEnv(name, { tracked = true } = {}) { |
Member
There was a problem hiding this comment.
Since this is synchronous, and doesn't fetch anything, I recommend calling it getEnv instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Set up a IPC channel between vite-task and the processes it spawns, so the spawned tools can declare at runtime what they actually read, wrote, or cared about, and then vite-task uses that to decide what to fingerprint in the cache.
Design notes:
docs/runner-task-ipc/.Problems this PR solves
Every example below is exercised by
patches/vite.patch, which wiresvite buildinto the IPC through@voidzero-dev/vite-task-client.1. Dynamic tracked envs
Before: the user had to declare every relevant env in
vite-task.json, statically:{ "tasks": { "build": { "env": ["NODE_ENV", "VITE_*"], "cache": true } } }This duplicates knowledge the tool already has. Forgetting
NODE_ENVsilently skips cache invalidation on mode change.envPrefix-matching envs (VITE_*by default) get inlined into the bundle throughimport.meta.env.*— so changingenvPrefix: 'MYAPP_'invite.config.jswithout updatingvite-task.jsondrifts: the runner still tracksVITE_*while the build output is driven byMYAPP_*.After: the tool declares its envs at runtime, driven by its own config.
The
buildtask invite-task.jsonneeds noenv:at all. ChangingenvPrefixinvite.config.jsdynamically changes the set of envs the runner tracks, with zero config edits on the runner side.2. Exclude tool's cache dir from input/output
Vite stores pre-bundled deps under
node_modules/.vite/and bundled configs undernode_modules/.vite-temp/. Every build reads the cache metadata (to check staleness) and writes fresh entries when it isn't stale. Without intervention the runner sees:There is a workaround already in vite-plus: voidzero-dev/vite-plus#1096 plus its follow-up #1198 hardcode
!node_modules/.vite-temp/**,!node_modules/.vite/**/results.json, and!dist/**as negative input globs on everyvpsubcommand (build,test,pack). That's not good enough:.vite/, moved temp dir, new subcommand with its own transient files), vp has to ship a matching glob update. It's a lockstep coupling that design-wise shouldn't exist..nuxt/, SvelteKit's.svelte-kit/, Next's.next/), needs its own hand-maintained list — vp can't ship it generically.After:
The declaration lives with the tool that owns the directory. The dep cache is vite's private concern.
3. Exclude output from input when a tool clears the folder before writing it
vite buildcallsemptyDir(outDir)before writingdist/.emptyDirhas to read the directory entries to know what to delete — those reads look identical to genuine input reads. Sincedist/is also where vite writes its final output, the runner sees a read-write overlap on the same paths and refuses to cache.After:
Only the writes count. The pattern generalizes: any tool that wipes-then-writes the same directory needs to tell the runner "my enumeration reads aren't inputs."
What's in this PR
vite_task_ipc_shared): message types + serialization shared by both ends.vite_task_server+vite_task_client): async server, sync blocking client, tested Rust-to-Rust.fspyfor dylib embedding. (Landed onmainvia refactor: extract materialized_artifact crate out of fspy #344 asmaterialized_artifact.)vite_task_client_napi+@voidzero-dev/vite-task-clientJS wrapper (fetchEnvsingle-name +fetchEnvsglob, with dedupe against already-setprocess.env).serve()'s returned iterator.Test plan
vite_task_server/tests/integration.rs)ignore_input,ignore_output,fetch_env,fetch_envs_glob,disable_cachevite buildviapatches/vite.patch(vite_build_cachefixture): NODE_ENV-change invalidation,envPrefix-driven tracked-env set change,dist/write restoration on cache hit