mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 07:12:00 +01:00
e879a24293d751ff0d64161ffddccdede200e423
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1805a1d247 |
perf(workspaces): probe workspace run bindings through indexes (#20568)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The terminal workspace sweep checks each Git workspace for recent activity and for live consumers before it expires or moves the workspace > - These checks find the runs of a workspace with one SQL scope, `expirationScope`. The scope used one `OR` over all run bindings, and one branch decoded the context JSON of each run > - PostgreSQL could not use an index for that `OR`. Each check read every run of the company and its large context from TOAST. On a hosted database, each call took 2 to 5 seconds, about 5,200 s in total in one window > - This pull request splits the scope into one index probe per binding and adds one concurrent expression index for the legacy `executionWorkspaceId` context key > - The benefit is that each check reads a few index entries instead of all company runs. The cleanup decisions do not change ## Linked Issues or Issue Description **What existing behavior does this improve?** The workspace activity and cleanup-safety checks: `gitWorkspaceLastActivityAt`, `isGitWorkspaceExpirationEligible`, `workspaceMoveEligible` and the orphan-folder consumer check. All of them use `expirationScope` in `server/src/services/git-workspace-expiration.ts`. **Subsystem affected** Execution workspace cleanup in the server (`git-workspace-expiration.ts`, `isolated-repository-rehome.ts`, `isolated-task-directory-retention.ts` through the shared scope) and the `heartbeat_runs` schema in `packages/db`. **Current behavior** The `relevant_runs` CTE selects company runs where any of these is true: `issue_id` or `native_issue_id` is in the issue tree or the bound issues, the run is a bound issue's checkout or execution run, or the context JSON has a matching `issueId`, `taskId` or `executionWorkspaceId`. The planner does one sequential scan of the company runs and reads each context from TOAST. The cost grows with the run history of the company, not with the size of the workspace. **Proposed behavior** Each binding is a separate index probe. The `UNION` of run IDs goes to a primary-key probe. The SQL gives the same rows. **Reason and benefit** Local SQL measurements show a 34x to 169x decrease in time. The decrease grows with the run history (see Verification). ## What Changed - `expirationScope` builds `relevant_runs` from a `UNION` of index probes, one for each binding. The key sets go in as arrays (`= ANY(ARRAY(...))`). With `IN (subquery)`, the planner chose a hash join over a full scan, because it does not count the cost of reading TOASTed contexts. - The legacy context keys use `context_snapshot ->> 'key'`. This expression matches the existing indexes `heartbeat_runs_company_ctx_issue_created_idx` and `heartbeat_runs_company_ctx_task_created_idx`. For a non-object context, `->>` gives `NULL`, as the strict JSONPath decode did. Such a run can still match through a native binding. - New partial index `heartbeat_runs_company_ctx_execution_workspace_idx` on `(company_id, context_snapshot ->> 'executionWorkspaceId') WHERE context_snapshot ? 'executionWorkspaceId'`. The query repeats the predicate. For an array context, `?` can be true, but `->>` gives `NULL`, so the result does not change. The index is partial so that it serves only this probe. A full index that starts with `company_id` made the planner choose it for an unrelated company-scoped query. Migration 0460 builds it with `CREATE INDEX CONCURRENTLY IF NOT EXISTS`. The migration is in the list of migrations that run outside a transaction. - New tests: a legacy `issueId` or `executionWorkspaceId` binding on a run that is bound to a different issue still blocks cleanup. A plan test with `enable_seqscan = off` checks that no binding scans `heartbeat_runs` and that the three context indexes are used. - The benchmark script `scripts/benchmark-workspace-activity.ts` gets a `--runs-per-workspace` option. Unrelated fixture runs now have distinct context IDs, as real history does. - `doc/DATABASE.md` describes the probes and the new index. ## Verification - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/git-workspace-activity.test.ts src/__tests__/native-harness-backup-expiration.test.ts`: 132 of 132 tests pass. The second file checks that the native session index stays the planner's choice for its query. These include the existing route matrix (root, child, bound, native, context issue, context task, context workspace, checkout, execution run), exactly-once counts, malformed and uppercase contexts, company isolation, controller leases and lease vetoes. - `pnpm --filter @paperclipai/server typecheck` and `pnpm --filter @paperclipai/db check:migrations` pass. - Benchmark (local SQL measurement, PostgreSQL 18.1, synthetic fixture, 8 workspaces per sample, before and after alternate in each iteration). The script compares the complete ordered caller results and the complete `relevant_runs` multisets of the baseline and treatment. They are equal in every run. `pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-workspace-activity.ts --baseline-ref=origin/master --runs-per-workspace=<N>` | Fixture | Check | Before median (min–max) | After median (min–max) | Speedup | | --- | --- | --- | --- | --- | | 4,800 runs, 330 MB | last activity | 523 ms (460–598) | 12 ms (8–15) | 44x | | 4,800 runs, 330 MB | eligibility | 452 ms (432–473) | 13 ms (11–15) | 34x | | 4,800 runs, 330 MB | idle eligibility | 465 ms (442–573) | 12 ms (10–13) | 40x | | 24,000 runs, 1.7 GB | last activity | 2,240 ms (2,214–2,454) | 13 ms (11–14) | 169x | | 24,000 runs, 1.7 GB | eligibility | 2,245 ms (2,097–2,377) | 17 ms (14–19) | 133x | | 24,000 runs, 1.7 GB | idle eligibility | 2,196 ms (2,179–2,381) | 15 ms (13–18) | 143x | Before: one `Seq Scan` on `heartbeat_runs`. After: `heartbeat_runs_company_issue_created_idx`, `heartbeat_runs_company_native_issue_contract_id_uq`, the three context indexes and `heartbeat_runs_pkey`. - Index build time: on a synthetic table with 200,000 runs and 8 KiB contexts (1.8 GB), the full form of this index took 3.7 s to build concurrently and used 15 MB. The partial index reads the same rows and holds only runs that have the key. ## Risks - Cleanup safety: a wrong "allowed" result could delete an active workspace. The scope keeps every binding of the old query. The benchmark checks exact equality of the run sets, and the existing tests check each binding route and each veto. - Migration: the index build reads every run context once. It runs concurrently and does not block writes. Until migration 0460 builds the index, the new query is still correct but the `executionWorkspaceId` probe scans. - Plan choice: the array form makes index probes the cheapest plan for a small tree. A very large issue tree gives a large array, but each element is still one index probe. - The sweep frequency does not change. This pull request reduces only the cost of each check. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [ ] All Paperclip CI gates are green - [ ] 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 Opus 5.5 <noreply@anthropic.com> |
||
|
|
3be54534c2 |
perf(server): decode workspace run context keys once (#17898)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Managed Git workspaces retain task and run obligations until cleanup is safe. > - Activity, expiration, retention and rehome share a run association query. > - That query repeatedly extracted three keys from wide historical JSON contexts. > - This pull request decodes those keys together and projects the six fields its consumers use. > - Native bindings, cleanup vetoes, lifecycle locks and fresh deletion checks stay in place. > - The benefit is less database work while preserving the same cleanup decisions. ## Linked Issues or Issue Description **What happened?** Workspace activity and eligibility queries repeatedly decompress large run contexts. An OR predicate extracts issue, task and workspace compatibility keys separately. The run scope also materializes columns its lifecycle consumers do not read. **Expected behavior** Read the same relevant runs and preserve all deletion and rehome vetoes with less repeated JSON work. Keep existing native, historical and pointer bindings independent. **Steps to reproduce** Run the synthetic PostgreSQL comparison below against baseline `db70c5acd92f50fa37fb4f0dea3da0e7685089a2`. Repeat the small, large-tree and combined controls documented in the PR. Related work: #17464 preserves workspace descendants. #17877 reconstructs native workspace obligations. This change preserves both contracts and adds no scheduling change. ## What Changed - Decode the three legacy context keys together with `jsonb_to_record`. A strict root-object guard preserves scalar, array and null behavior. Independent native and pointer bindings still match. - Materialize the full textual tree-ID set once. This avoids inflated recursive estimates triggering JIT compilation for the correlated decoder. - Project the six run lifecycle columns used by activity, expiration, orphan retention and rehome. - Add binding, lease, malformed-shape and fresh-read regression cases. Add a reproducible four-control benchmark and detailed evidence documentation. ## Verification - Full repository typecheck passed on the integrated final source. - Six relevant suites passed all 209 tests against real PostgreSQL, including the 20 new binding/shape cases, 25 eligibility cases, full Git expiration/restoration, task-directory retention and configuration, and isolated repository rehome. These cover conflicting bindings, company isolation, duplicate admission, pending cleanup, controller leases, non-object contexts and newly active consumers. - PostgreSQL 17.11 comparisons use two warmups and five alternating samples per variant. Each sample runs eight sequential workspace queries. Every complete caller result and six-column relevant-run multiset matches. - Stress fixture: 4,813 runs and about 332 MB of run storage, with unique wide contexts. Last-activity median: 1,071.54 to 743.45 ms per eight workspaces, 1.44 times faster. Closed eligibility: 814.29 to 713.86 ms, 1.14 times faster. Idle eligibility: 721.46 to 481.69 ms, 1.50 times faster. - Small-history control improves 1.23–1.36 times. The 250-descendant stress control improves 1.24–1.30 times. Combined small-history/large-tree activity and idle checks improve 1.19–1.22 times. Combined closed eligibility is neutral: 61.11 to 62.82 ms per eight, about 0.21 ms extra per workspace. - Stress last-activity ranges do not overlap: 920.27–1,111.22 ms before and 689.14–880.40 ms after. Closed-eligibility ranges overlap; its smaller median gain is directional. - Last-activity buffer accesses fall from 113,202 to 38,081. Eligibility run width falls from about 1,414 to 57 planner bytes. All final plans retain one history scan with no JIT. - Reproduce: `pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-workspace-activity.ts --baseline-ref=db70c5acd92f50fa37fb4f0dea3da0e7685089a2 --docker-postgres=17 --reject-new-jit --output-dir=/tmp/workspace-activity-stress`. - Repeat with `--small`, `--descendants=250`, and both flags, using distinct output directories. Each run creates and removes its own synthetic database. - Full CI passed on `bc4edd700f983e22f093d0a01e2f4c795075b982`: 54 successful checks, one neutral review and two expected Storybook skips, plus successful Snyk status. Greptile is 5/5 on this exact head. There are zero unresolved threads and the merge state is clean. [CI run](https://github.com/paperclipai/paperclip/actions/runs/40530494291). - Full repository typecheck and build passed on the integrated final source. The local full test command was stopped after native PostgreSQL initialization failed repeatedly, producing 16 failures and skipped database suites. It is incomplete, not a pass. Focused real-database suites used disposable Docker PostgreSQL. Current-head CI passed all full gates separately. ## Risks - Cleanup safety depends on complete run association. Tests compare every retained run field and exercise downstream vetoes, not only row counts. - PostgreSQL plan estimates matter. Rejected per-key index probes were up to 56 times slower on a small-history/large-tree control. An ANY variant also regressed. The final shape passes all four controls under default JIT settings. - The final combined eligibility control is neutral. Synthetic SQL timings do not establish deployed latency, provider behavior or actual compute sleep. - Review suggested a CASE/jsonb_typeof root guard as an optional alternative. The strict JSONPath guard is retained because its scalar, nested-array and independent-native behavior passes real PostgreSQL tests and all four measured controls. No correctness defect was identified. A replacement needs its own comparative measurements. - Future consumers that need another run column must extend this explicit projection. - No migration, index, startup job, polling change or deployment is included. ## Model Used OpenAI Codex, configured model `gpt-6.1-sol` with xhigh reasoning, tool use and code execution. Independent blast-radius review also used `gpt-6-astra` and `gpt-6.1-sol`. The runtime context window was not exposed. ## 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 Co-authored-by: Paperclip <noreply@paperclip.ing> |