fix: keep budget views readable when a scope has been deleted - #11058
Open
markkuhr wants to merge 2 commits into
Open
fix: keep budget views readable when a scope has been deleted#11058markkuhr wants to merge 2 commits into
markkuhr wants to merge 2 commits into
Conversation
|
✅ All checks passing — ready for Greptile review and maintainer approval. — commitperclip |
Contributor
Greptile SummaryThis PR keeps budget read paths available when an agent or project referenced by a policy has been deleted, while retaining strict scope validation for writes.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| server/src/services/budgets.ts | Adds tolerant missing-scope handling to budget read paths while preserving strict policy-upsert validation. |
| server/src/tests/budgets-service.test.ts | Verifies through the real agent-deletion service path that a dangling agent-scoped policy no longer breaks the overview. |
Reviews (4): Last reviewed commit: "test: delete the agent through the real ..." | Re-trigger Greptile
scopeId on a budget policy is a soft reference with no foreign key, so an agent or project can be deleted while policies and incidents still point at it. resolveScopeRecord threw notFound in that case. The overview resolves every scope in one pass, so a single deleted agent took out the whole budget and dashboard view rather than degrading the one affected row. Scope resolution now takes a strict flag. Read paths that render many scopes pass false and get a tombstone record for a missing scope. Mutating paths keep the strict behaviour, so writing a policy against a scope that does not exist still fails.
markkuhr
force-pushed
the
fix/deleted-agent-cleanup
branch
from
August 7, 2026 20:02
6ced1d2 to
cbadac5
Compare
…bstone test Review flagged that the regression test removed the agent row directly and so never proved the dangling-scope state is reachable in production. Deleting via agentService.remove instead makes the test carry that proof: agent deletion clears its own dependent rows but never touches budget_policies, and scopeId has no foreign key, so the policy is left pointing at a row that is gone. The premise behind the review comment was that deletion always rolls back on routine and cost-event foreign keys. That holds only for an agent that has accrued cost events or is assigned to a routine; an agent with neither deletes cleanly today, which is exactly the case this test now exercises. Also documents why the company scope stays strict in both modes: its policy and incident rows carry a real foreign key to companies, so it cannot outlive its scope the way an agent or project can.
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.
Thinking Path
Linked Issues or Issue Description
No existing issue. Describing it here per CONTRIBUTING.md.
What happened?
When a budget policy points at an agent or project that no longer exists, the budget overview and dashboard fail with
notFoundinstead of rendering.Expected behavior
A scope that no longer exists degrades to a placeholder row. The rest of the view renders.
Steps to reproduce
Paperclip version or commit
master, a388ea1.
Deployment mode
Reproduced on
local_trusted. Not mode specific.Installation method
From source.
What Changed
resolveScopeRecordtakes astrictoption. When false it returns a tombstone record for a missing agent or project instead of throwing.agentService.remove, and then reads the overview.companies, so unlike an agent or project it cannot outlive its scope. Documented in place so the asymmetry does not read as an oversight.Verification
npx vitest run server/src/__tests__/budgets-service.test.ts— 8 pass.pnpm --filter @paperclipai/server typecheckis clean.Reverting
budgets.tsand keeping the test makes it fail, and the failure is atoverview, not at the delete:The test deletes through
agentService.removerather than the table, and asserts the delete returned a row, so it also proves the dangling-scope state is reachable in production rather than only constructible in a test. Agent deletion clears its own dependent rows but never touchesbudget_policies, andbudget_policies.scope_idcarries no foreign key.Risks
Low, and deliberately asymmetric. Only read paths degrade; write paths still reject a missing scope, so a policy cannot be created against a scope that does not exist. The tombstone is display only and carries
paused: false, so it cannot cause a spurious pause.Behavioral shift worth noting: the overview now renders rows for scopes that no longer exist, where it previously failed outright. That is the intent, but an operator can now see a policy whose target is gone. Cleaning up such policies is out of scope.
Scope note: this PR previously also changed agent deletion. That half is split out to #11062 after review feedback, because clearing an agent's cost events rewrites spend history and the fix for it needs
cost_events.agent_idto stop being notNull, which is a schema migration.Known adjacent bug, deliberately not fixed here: an agent that has accrued cost events, or is assigned to a routine, still cannot be deleted at all —
cost_events.agent_idis notNull with a foreign key androutines.assignee_agent_idis a foreign key with noonDelete. That is #11062. It narrows which agents can reach the dangling-scope state; it does not remove it, since an agent with neither deletes cleanly today.Model Used
Claude Opus 4.5 (
claude-opus-4-5), extended thinking, 1M context, with tool use and code execution.Checklist