mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 12:14:16 +01:00
9fb06afbfab284f8dcecf6ebc3dee3bfcda4e09d
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6ef37398c8 |
fix(build): fit typecheck and image build in 4 GiB (#18737)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Contributors, agents, and image builds run `tsc` to check and compile the repository. > - The repository uses TypeScript 7, which is a native binary. Node heap flags do not limit it. > - The server is one large program, so one `tsc` process needs about 10 GiB. > - The operating system stops `tsc` under a 4 GiB memory limit. > - This pull request checks the server in layers, removes the server from the CLI program, and removes declarations and checks from the image emit. > - The benefit is that the full typecheck and the image `tsc` step now complete in 4 GiB. ## Linked Issues or Issue Description I found no duplicate pull request or issue. A search for `tsc`, memory, typecheck, and OOM found only runtime memory work, which is a different subject. **What happened?** `tsc` used too much memory, and the kernel stopped it with exit code 137 in resource-constrained environments. Measured peaks before this change: | Command | Peak | | --- | --- | | Server `tsc --noEmit` | About 10 GiB | | Server `tsc` emit, as in the Docker build | About 10.7 GiB | | CLI `tsc --noEmit` | About 7 GiB | | `pnpm -r typecheck`, whole process tree | About 10.2 GiB | **Expected behavior** The typecheck and the image `tsc` step complete under a 4 GiB memory limit. **Steps to reproduce** 1. Start a Linux container with `--memory=4g --memory-swap=4g --cpus=4`. 2. Install the dependencies on the base commit. 3. Run `pnpm -r typecheck`. The kernel stops the process with exit code 137. 4. Run `pnpm --filter @paperclipai/server build`. The `tsc` step needs more than 4 GiB. **Paperclip version or commit** Base commit `09162fe893`, with TypeScript 7.0.2. **Deployment mode** Not applicable. This issue applies to development, CI, and image builds. It does not change the server at runtime. ## What Changed - Add `server/scripts/typecheck-layers.mjs`. It reads the import graph on each run and runs one `tsc` process per layer. The layers are the files below the largest import cycle, the cycle, the files above the cycle, and the tests. Each layer reads the earlier layers as declarations. - Add `scripts/run-tsc.mjs`. It adds `--singleThreaded` when `PAPERCLIP_TYPECHECK_LOW_MEMORY=1` is set. The server layers and the UI typecheck use it. - Add `scripts/typecheck-low-memory.mjs`. It checks one package at a time in this mode. It has no `package.json` entry, as `.greptile/rules.md` requires. - Change the server `typecheck` script to build only the Runner TypeScript output. It does not build the Runner release binary now. `pnpm build` and `typecheck:build-gaps` still run the full Runner build and its generated-file checks. - Change the `tsc` step of the server `build` script to `scripts/run-tsc.mjs`. The `Dockerfile` sets `PAPERCLIP_TSC_EMIT_ONLY=1` for that build, so the image emits JavaScript without checks or declarations. Without this variable, `build` still does a full check for npm packages and CI. - Use a non-literal specifier for the `@paperclipai/server` import in `cli/src/commands/run.ts`. The import result already had no useful type, and the literal specifier added the full server to the CLI program. - Add a layered server typecheck step to the `pr-trusted` typecheck job, because `typecheck:build-gaps` does not run this script. - Update `AGENTS.md` and `doc/DEVELOPING.md`. ## Verification Results on Linux use a clean copy of this branch, a new `pnpm install`, Node 24, Rust 1.97.1, 4 CPUs, and a 4 GiB memory limit with no swap. Results before the merge of `master`: | Command | Result | Peak non-reclaimable memory | | --- | --- | --- | | `node scripts/typecheck-low-memory.mjs` | Exit 0, 0 OOM kills | 3,492 MiB | | `PAPERCLIP_TSC_EMIT_ONLY=1 pnpm --filter @paperclipai/server build` | Exit 0 | 3,656 MiB, from the Rust release build | | Server single-program `tsc --noEmit --singleThreaded` | Exit 137, 1 OOM kill | Reached the limit | Results on macOS on the merged head, with peak memory for the full process tree: | Command | Before | After | | --- | --- | --- | | `pnpm -r typecheck` | 10.2 GiB, exit 0 | 5.47 GiB, exit 0 | | `node scripts/typecheck-low-memory.mjs` | Not available | 3.81 GiB, exit 0 | | CLI typecheck | 7.06 GiB | 1.68 GiB, before the merge | Other checks: - Compiler tests: new tests run the real compiler on a small fixture. They prove that the layers emit declarations and pass, that a renamed export fails the layer that imports it, and that a changed type fails the test layer. - Declaration imports: a new compiler test proves that types from `.d.mts` and JSON imports stay correct in later layers. On the real server, a wrong use of `AgentFileManifest` in the cycle layer failed with `TS2353`. - Coverage: a test on the real server source asserts that each source file is in one layer and that no import goes to a later layer. The test layer contains the remaining files. `src/__tests__` stays out of the root list, as in `server/tsconfig.json`. - Error detection: I added one type error to one file in each of the four layers. The typecheck reported all four errors and exited with 1. - Cross-layer errors: I renamed an export in the lower layer. The cycle and upper layers reported the error and the typecheck exited with 1. I reverted these temporary edits. - Emit: full `tsc` and `--noCheck --declaration false --declarationMap false` gave byte-identical JavaScript and source maps for 2,249 files on the base commit. - Runtime: `scripts/server-boot-smoke.sh node server/dist-runtime/index.mjs` passed with a bundle from the emit-only build. - CLI: the esbuild bundle keeps the dynamic import, and `node --check` passed. - Tests on the merged head: `typecheck-layers`, `prepare-runner-vendor`, `cli-invocation-safety`, the CLI `runtime` tests, and all 503 `.github/scripts/tests` tests passed. Before the merge, `release-verify-workflow` and `runner-release-crate-cache` passed. - Full `pnpm --filter @paperclipai/server build` passed before the merge. Failures during this work: - The first low-memory run failed in the UI with `TS5023: Unknown compiler option '-b'`, because the flag came before `-b`. I corrected the argument order and added a test that runs build mode with the flag. - The first rename check did not rename the export, because BSD `sed` does not support `\b`. Its exit 0 was not evidence. I ran the check again with `perl`, and the result above is from that run. - The first version of the real-compiler fixture had a `TS2774` error in its own source. The test found this error, and I corrected the fixture. - CI failed `server-package-build-script.test.ts` on `1abc5e5a43`, because that test reads the steps of the server `build` script, and I had moved them to `build:prepare` and `build:assets`. My first search for readers of this script did not include `server/src`. I removed those scripts and `build:image`, restored one `build` script, and kept the test unchanged. - `GOMEMLIMIT` did not help enough, and I removed it from the plan. On these steps, `3GiB` saved at most about 300 MiB. With default checkers, the cycle layer was stopped with and without it, and the lower layer went from 7 seconds to 151 seconds. Not run or not proven: - I did not run the full `pnpm test:run`, a root `pnpm build`, or a full `docker build`. I expect CI to run the test and build gates. - I did not measure the Docker build steps other than the server build. - I did not run the 4 GiB checks on x86-64. - I did not measure the merged head in the 4 GiB container. The macOS result on the merged head is 3.81 GiB, and the merge added one source file to the lower layer. ## Risks - The margin is small. In the 4 GiB container, the cycle layer peaks about 600 MiB below the limit, and the Rust release build in the image build peaks about 440 MiB below it. Growth of the largest import cycle uses this margin first. - `node scripts/typecheck-low-memory.mjs` applies only to `pnpm -r` packages. These commands still check the server as one program and need more than 4 GiB: `pnpm build`, `test:e2e:runner:typecheck`, `test:lifecycle-baseline:typecheck`, and the `tests/speko-native` project. - A local flow that used the Runner binary from a previous typecheck must now run `pnpm --filter @paperclipai/paperclip-runner build`. - The image does not include server declarations now. I found no consumer in this repository. A downstream image that reads `server/dist/*.d.ts` must change. - An image build does not find type errors now. A typecheck in CI must find them before the image build. - The import scan uses a regular expression. A false edge can only make a layer larger. A missed edge causes a `TS6307` failure, so it cannot skip a file. - The new CI step adds about one minute to the typecheck job. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used Anthropic Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with reasoning, tool use, and code execution. The runtime does not show the context window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7df517d18f |
perf(build): reuse a current Runner build in the server's prepare:runner-vendor step (#15926)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Its CI runs a `Build` job on every pull request. That job runs `pnpm build` and then verifies the public npm install. Since the server test shards got faster, `Build` is the slowest check in most runs (419-497s on the six fully green `master` runs created 2026-10-11 19:46Z to 20:17Z). > - Inside `pnpm build`, the server package is the last package to build. Its first step, `prepare:runner-vendor`, runs the full build of `@paperclipai/paperclip-runner` so the server can vendor `packages/paperclip-runner/dist`. In `pnpm -r build` that package was built seconds earlier by the same command. The second build took 12-16s on those six runs and sits on the critical path of the job. > - The step cannot just be removed. A standalone `pnpm --filter @paperclipai/server build` (the Dockerfile's path) must produce a current vendored Runner on its own. > - This pull request keeps that guarantee and skips only the build that is provably redundant. The Runner's `build` records a digest of its inputs, captured before its first step and confirmed after its last, together with a manifest of every file it left under `dist`. The server's `prepare:runner-vendor` reuses the build when the digest matches, the stamp is for this platform, and every recorded output is still there at its recorded size. Any other state runs the same full Runner build as before. > - The benefit is 12-16s off the `Build` job and off every other `pnpm build` (the Canary Dry Run's `release.sh` build, local builds), with no change to what a standalone server build produces. The PR's own first run measured it: the server step reused the build in 56ms where `master` runs rebuilt for 11-15s. ## Linked Issues or Issue Description No GitHub issue exists for this. The description below follows the enhancement template. **What existing behavior does this improve?** The server build's `prepare:runner-vendor` step, which builds `@paperclipai/paperclip-runner` so the server can vendor its `dist`. **Subsystem affected** Build and CI (`server/package.json`, `packages/paperclip-runner/package.json`, the `Build` job). **Current behavior** `prepare:runner-vendor` runs the Runner's full `build` every time. Inside `pnpm -r build`, the Runner was already built by the same command 20 to 25 seconds earlier, with every check passing. On six fully green `master` runs (2026-10-11, 19:46Z to 20:17Z) the repeated build took 11.8s to 15.9s, measured from the `server build$ pnpm run prepare:runner-vendor` log line to the Runner's `Finished release profile` line. The server package builds last, so that time is on the critical path of `Build`, which is the slowest check in most pull request runs. **Proposed behavior** - The Runner's `build` starts with `node scripts/record-build-inputs.mjs --begin` and ends with `node scripts/record-build-inputs.mjs --finish`. `--begin` removes the previous stamp and records a digest of every build input before the first step reads anything. `--finish`, after every step and check has passed, recomputes the digest and writes `packages/paperclip-runner/node_modules/.cache/paperclip-runner-build-inputs.json` only if the inputs are still the ones the build began with, together with the path and size of every file under `dist`. A stamp therefore exists only for a build that completed every step on inputs that did not move while it ran. - The server's `prepare:runner-vendor` becomes `node scripts/prepare-runner-vendor.mjs`. It reuses the build if the stamp is for this platform and architecture, every recorded output is still there at its recorded size, and the recomputed digest matches. In every other case it removes the stamp and runs `pnpm --filter @paperclipai/paperclip-runner build`, the exact command the step ran before. - `PAPERCLIP_RUNNER_VENDOR_REBUILD=1` forces the rebuild. **Reason and benefit** 12-16s off the `Build` job and off every other `pnpm build` (the Canary Dry Run's `release.sh` build, local builds), with no change to what a standalone `pnpm --filter @paperclipai/server build` produces. **Breaking changes** None. The rebuild path runs the same command as before. A fresh checkout, a changed input, a missing or truncated output, an edit that landed during the build, or a stamp from another platform always rebuilds. ## What Changed - `packages/paperclip-runner/scripts/build-inputs.mjs` (new): defines the input set, the digest, the output manifest and the reuse decision. Inputs are every file under `packages/paperclip-runner` except `dist`, `dist-browser`, `runner/target`, `node_modules` and dot-directories; every file under `packages/paperclip-eval-kernel` except `dist` and `node_modules` (the Runner's `build:typescript` builds it first); every file under the root `patches` directory (`check:acpx-profiles` hashes `acpx@0.13.1.patch`, and `build-verified-provider-entrypoints.mjs` copies the `pi-acp` and `brace-expansion` patches into `dist/cli/pi-provision-inputs`); and the root `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml` and `tsconfig.base.json`. The toolchain pins are inside the package (`rust-toolchain.toml`, `runner/Cargo.lock`) and the TypeScript version is in the lockfile, so both are covered. A symlink is hashed as its target path and is not followed. Outputs are every file under `dist`, the tree the server copies wholesale, recorded with their sizes. Exports `beginRunnerBuild`, `recordRunnerBuild` and `assessRunnerBuild` (returns `current` plus a stable reason code: `no_stamp`, `schema_mismatch`, `platform_mismatch`, `missing_output`, `outputs_changed`, `inputs_changed`, `inputs_unchanged`). - `packages/paperclip-runner/scripts/record-build-inputs.mjs` (new): the two phases, `--begin` and `--finish`, added as the first and last step of the Runner's `build` in `packages/paperclip-runner/package.json`. Each build chain has its own pending record under `node_modules/.cache/paperclip-runner-build-inputs.pending/`, named after the shell pnpm runs the chain in (the parent of both phases; `--finish` also accepts a record named after its own pid, for a shell that execs the last command of the chain), so two Runner builds running at once in one checkout do not consume each other's record. `--finish` without a pending record for its chain is an error, so the chain cannot silently lose its first step; `--finish` after an edit that landed mid-build prints `not recorded: inputs changed while the build ran` and leaves no stamp. Records of builds that never finished are pruned by the next `--begin` after a day. - `server/scripts/prepare-runner-vendor.mjs` (new): the reuse-or-rebuild step. It resolves the pnpm that is running it through `npm_execpath` with the same rule as the CLI's `resolvePnpmInstallInvocation` (a JavaScript entry runs through this Node, a native executable runs directly, otherwise `pnpm` on PATH). It prints one `paperclip.runner-vendor.timing.v1` line with `action`, `reason`, `check_ms` and `build_ms`, so the saving is measured on this PR's own `Build` job. - `server/package.json`: `prepare:runner-vendor` now runs that script. The `build` and `typecheck` scripts are unchanged. - `server/src/__tests__/server-package-build-script.test.ts`: pins the new indirection and the command behind it (imported from the script, so the Dockerfile's standalone path stays pinned), and a new test asserts the Runner's `build` starts with `--begin`, ends with `--finish`, and has exactly those two record steps. - Tests: `packages/paperclip-runner/scripts/build-inputs.test.mjs` (node:test, added to `test:typescript:prep`) and `server/scripts/prepare-runner-vendor.test.mjs` (vitest, in the server lane). - `doc/DEVELOPING.md`: a "Reused Runner build in the server build" section under Test Commands. ## Review findings on the first head, all fixed in [ |