feat(workspace): per-run execution worktrees via strategy runScope (BLO-19063) - #1143
Conversation
|
@ally please review at head 67ab77b. Focus, in priority order:
Known and deliberate: |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
Re: the It was posted at All five items it listed are present in the current body — No review has been posted on this PR yet — zero entries in Remaining state: 11 checks queued/in-progress at |
…LO-19063) Recovered from an uncommitted working tree left by an earlier run of this issue. Committing verbatim first so the work is durable; review follows in subsequent commits.
…kspaceRunScope; document (BLO-19063)
Three defects found by actually running the recovered work:
1. applyRunScopeToBranchName derived its token via sanitizeBranchName, whose
empty-input fallback is the literal 'paperclip-work'. Every run WITHOUT a run
id therefore got the same token ('papercli') and silently collapsed back onto
one shared tree while still looking per-run -- strictly worse than the
intended clean fallback. Now derived from the raw run id, so a missing id
degrades to the issue-scoped name as documented.
2. ExecutionWorkspaceRunScope was never re-exported from the shared package
barrels, so the server import failed typecheck. Tests passed regardless
because vitest resolves the source directly. Exported from both
types/index.ts and index.ts.
3. No documentation. Added doc/EXECUTION-WORKSPACE-RUN-SCOPE.md with the
mechanism, how to enable it, and the measured per-run cost.
…O-19063) The validator tests proved runScope parses; they did not prove the runtime sees it. realizeExecutionWorkspace reads config.workspaceStrategy, so a scope that parsed cleanly but was dropped building the adapter config would be silently inert -- config present, isolation absent.
80c9ea9 to
5a248c3
Compare
…llection (BLO-19063) The cost section claimed `per_run` moves worktree spend "under the managed path (stampGitWorktreeOwnership / pruneOwnStaleGitWorktree), where it can be collected". That is wrong, and it was load-bearing: it is the basis on which the per_run pilot was approved as a bounded ~14 GB steady-state cost. pruneOwnStaleGitWorktree reclaims git registry entries, not disk — it returns removed:false whenever the directory still exists, and its only call site is the workspace restore path. The only code that frees disk is cleanupExecutionWorkspaceArtifacts, whose sole automatic caller is a catch-block rollback for a failed row persist. cleanupEligibleAt is not a query predicate anywhere. Nothing collects at end of run. Measured on the live host: five runtime-managed worktrees spanning 8 days and 38 hand-rolled ones spanning 7 days, none collected; 95 registered; 2.0 GB apparent per tree. So per_run converts a per-issue leak into a per-run leak, and is gated on a collector existing rather than ready to enable. Co-Authored-By: Claude <noreply@anthropic.com>
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (1)
Important Issues (2)
Suggestions (2)
Strengths
Recommended Action
|
PR #1143 derives a run-scoped branch inside realizeExecutionWorkspace, but heartbeat pins the issue to `reuse_existing` and the restore path never calls realize again. The run token was therefore derived once and frozen: every run after the first landed back in the first run's tree while the config still read as per-run isolation. Isolation that looks configured and delivers none is worse than not offering the mode. Two changes: - resolveExecutionWorkspaceReuseRequestForIssue refuses to restore when the issue's persisted settings ask for `runScope: "per_run"`, forcing a fresh realization per run. Checked against persisted settings rather than only where the preference is written, so issues already pinned to `reuse_existing` before per_run was configured are rescued too. - The pin itself is no longer written for per_run issues, so persisted state stays honest instead of relying on the guard to paper over a contradiction. `per_issue` (the default) is untouched: shared_workspace remains default-safe, so this is not a forced fleet-wide migration. Co-Authored-By: Claude <noreply@anthropic.com>
PR #1143 derives a run-scoped branch inside realizeExecutionWorkspace, but heartbeat pins the issue to `reuse_existing` and the restore path never calls realize again. The run token was therefore derived once and frozen: every run after the first landed back in the first run's tree while the config still read as per-run isolation. Isolation that looks configured and delivers none is worse than not offering the mode. Two changes: - resolveExecutionWorkspaceReuseRequestForIssue refuses to restore when the issue's persisted settings ask for `runScope: "per_run"`, forcing a fresh realization per run. Checked against persisted settings rather than only where the preference is written, so issues already pinned to `reuse_existing` before per_run was configured are rescued too. - The pin itself is no longer written for per_run issues, so persisted state stays honest instead of relying on the guard to paper over a contradiction. `per_issue` (the default) is untouched: shared_workspace remains default-safe, so this is not a forced fleet-wide migration. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Ally — Consolidated PR Review
Post-hoc review of merged PR #1143 at the merge commit. Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bb0da6a
Prior Findings Dispositioned (3)
- prior:47b4d93 critical 1 — still-present —
server/src/services/heartbeat.ts:21074— after persisting a newly realized isolated workspace, the issue is set toreuse_existing; the next run restores that single workspace rather than callingrealizeExecutionWorkspacewith its own run id. - prior:47b4d93 important 1 — still-present —
server/src/services/workspace-runtime.ts:674— the fixed-width slice occurs after the identifier check and can remove a custom-template identifier placed near the tail. - prior:47b4d93 important 2 — still-present —
server/src/services/heartbeat.ts:4213— model-profile and issue adapter overrides shallow-spread over the resolved workspace config, so either can replaceworkspaceStrategywithoutrunScope.
Critical Issues (1)
- [gstack/review] prior:47b4d93 critical 1
server/src/services/heartbeat.ts:21074—per_runis defeated after the first run. The persistence path records one workspace on the issue and forcesreuse_existing;provisionExecutionWorkspaceForFreshnessDecisionrestores it at:5559-5572instead of deriving a branch with the next run id. Concurrent later runs therefore share the initial run's tree and uncommitted state.- Do not bind
per_runworktrees to the issue-wide reuse path. Add a heartbeat-level regression starting with a persisted first-run workspace.
- Do not bind
Important Issues (2)
- [pr-review-toolkit] prior:47b4d93 important 1
server/src/services/workspace-runtime.ts:674— a long custom branch template that includes the issue identifier only near its end passesapplyIssueIdentifierToBranchName, then loses that identifier during truncation. This breaks the documented PR ref-linking guarantee.- Reserve valid identifier space as well as suffix space, and test the tail-identifier template.
- [gstack/review] prior:47b4d93 important 2
server/src/services/heartbeat.ts:4213— shallow model-profile or issue adapter-config overlays can replace the resolvedworkspaceStrategyobject and silently return the runtime to theper_issuedefault.- Deep-merge the resolved strategy or reapply it after adapter overrides; cover both override sources in the production configuration path.
Suggestions (1)
- [native-codex]
server/src/services/workspace-runtime.ts:665— derive the suffix from a hash of the complete run id instead of its first eight alphanumeric characters, avoiding collisions for distinct IDs sharing that prefix.
Strengths
- The public shared types are additive and are re-exported from both barrels.
- The merge-commit documentation correctly identifies that no automatic collector exists and points to the real disk-growth risk.
- BLO-22984 tracks lifecycle collection; BLO-23144 tracks the configuration and branch-hardening defects.
Recommended Action
- Fix the issue-wide reuse path before enabling
per_runfor concurrent agents. - Address the two configuration/identifier defects in BLO-23144.
- No revert is necessary while
per_runremains disabled; revert or disable activeper_runconfigurations if they exist before the critical fix lands.
PR #1143 derives a run-scoped branch inside realizeExecutionWorkspace, but heartbeat pins the issue to `reuse_existing` and the restore path never calls realize again. The run token was therefore derived once and frozen: every run after the first landed back in the first run's tree while the config still read as per-run isolation. Isolation that looks configured and delivers none is worse than not offering the mode. Two changes: - resolveExecutionWorkspaceReuseRequestForIssue refuses to restore when the issue's persisted settings ask for `runScope: "per_run"`, forcing a fresh realization per run. Checked against persisted settings rather than only where the preference is written, so issues already pinned to `reuse_existing` before per_run was configured are rescued too. - The pin itself is no longer written for per_run issues, so persisted state stays honest instead of relying on the guard to paper over a contradiction. `per_issue` (the default) is untouched: shared_workspace remains default-safe, so this is not a forced fleet-wide migration. Co-Authored-By: Claude <noreply@anthropic.com>
Thinking Path
Linked Issues or Issue Description
criticalDedup search: searched open PRs for
workspace|worktree|runScope|isolationand all PRs referencingBLO-19063. Only related PR is #843 (BLO-19001, merged). No duplicate.What Changed
packages/shared/src/types/workspace-runtime.ts— newExecutionWorkspaceRunScope(per_issue|per_run) and an optionalrunScopeonExecutionWorkspaceStrategy.packages/shared/src/validators/{issue,project}.ts— acceptrunScopeon the strategy (both schemas arestrict(), so an unknown scope is rejected, not silently persisted).packages/shared/src/{index,types/index}.ts— re-export the new type from both barrels.server/src/services/execution-workspace-policy.ts— preserve only a recognizedrunScopethroughparseExecutionWorkspaceStrategy.server/src/services/workspace-runtime.ts— new exportedapplyRunScopeToBranchName, applied afterapplyIssueIdentifierToBranchNameso the issue identifier survives.server/src/__tests__/execution-workspace-per-run-isolation.test.ts— new, 13 tests.doc/EXECUTION-WORKSPACE-RUN-SCOPE.md— new: mechanism, how to enable, measured cost.Keying the branch (not just the path) is load-bearing: git refuses to check one branch out in two worktrees, so a per-run tree genuinely requires a per-run branch.
Verification
Assertions: two runs on different issues get non-overlapping cwds with neither an ancestor of the other; two runs on the same issue likewise, both appearing as real
git worktree listentries, and a file written into one tree is unreadable from the other; the issue identifier survives for PR ref-linking; omittingrunScopestill reuses one tree (created: falseon the second run); the token clamps to git's 120-char ceiling without being truncated away; validators reject an unknown scope; and the scope survives into the adapter configrealizeExecutionWorkspaceactually reads.Pre-existing failures, not from this change:
workspace-runtime.test.tshas 2EADDRINUSEauto-port failures. I reproduced them identically on untouchedorigin/masterin a clean worktree — port contention on the shared node.Measured cost (this repo, 4,606 tracked files, production node, CephFS):
git worktree addofforigin/masterpnpm install --frozen-lockfile, cold storepnpm install --frozen-lockfile, warm storeThe pnpm store is not global — it resolves under the execution workspace's own home, so a fresh execution workspace pays the cold number.
Risks
Low for existing agents, by construction —
per_issueis the default and every current caller keeps today's behaviour. There is a test asserting the second run still reuses one tree whenrunScopeis omitted.Risks specific to opting in:
stampGitWorktreeOwnership/pruneOwnStaleGitWorktreelifecycle. If collection underperforms, disk grows faster than under per-issue.120 - suffix.length; a test asserts the token is never the part truncated, since that would silently collapse two runs onto one tree.One bug worth flagging, caught by running it: the first implementation derived the run token via
sanitizeBranchName, whose empty-input fallback is the literal"paperclip-work". Every run without a run id therefore got the identical tokenpapercliand silently shared one tree while still appearing per-run — strictly worse than no isolation, because it presents as isolation. Now derived from the raw run id so a missing id degrades to the issue-scoped name.Ref-linking note (corrected): an earlier revision of this description claimed the Paperclip backlink bot had mis-resolved this PR to BLO-21427. That was my error — I read a truncated copy of the comment. The bot correctly links all four referenced issues, BLO-19063 included. No ref-linking defect here; disregard that note if you read it.
Model Used
Claude Opus 5 (
claude-opus-5[1m], 1M context), extended thinking, with tool use and code execution.Checklist