fix(workspace): deletion must take the agents' data with it - #187
Open
WhichPaths wants to merge 1 commit into
Open
fix(workspace): deletion must take the agents' data with it#187WhichPaths wants to merge 1 commit into
WhichPaths wants to merge 1 commit into
Conversation
The purge sweeps its soft-scoped tables with `DELETE FROM <t> WHERE company_id = $1`, which is only as good as the company_id the writers put there. Two of them do not put one. agent_workspace: the agent's own filesystem endpoint wrote (agent_id, path, body, meta) and no tenant, so every file an agent wrote through it — its memory index included — landed with company_id NULL. Every other writer of that table (skills.ts, cli.ts, router.ts, the baseline backfill) supplies it, and also refreshes it on conflict. agent_climate: the column carries DEFAULT 'personal' and NEITHER of its two INSERT sites names it, so every climate row in every workspace is labelled 'personal'. The delete can never match one. Both survive the sweep. What is left behind is an agent's memory and the notes it wrote about the people it worked with, for a workspace whose owner asked for it to be deleted. Demonstrated against Postgres 16: the FUSE-shaped insert and a peer writer's insert for the same agent, then the sweep — one row deleted, one row surviving. Two halves: - fs-endpoints fills the tenant from the agent's participants row via a scalar subquery, so it stays a single round trip and still inserts when the lookup finds nothing, exactly as before. The ON CONFLICT arm heals rows already written without it. - The purge also sweeps agent-owned tables by agent_id. Those rows belong to agents that are about to cease existing, and deleting by owner reaches the rows already written tenant-less without needing a backfill migration. The existing purge test cannot catch this: it seeds every row with a correct company_id, which is the one shape the broken writers never produce. The new tests seed the way the real writers do — and one drives the actual PUT /runtime/fs/write endpoint, so the writer half is pinned too, not just the sweep. Both halves go red when reverted separately.
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.
Deleting a workspace leaves the agents' memory and their written notes about people behind.
The purge sweeps its soft-scoped tables with
which is exactly as good as the
company_idthe writers put there. Two of them do not put one.agent_workspace— the agent's own filesystem endpoint writes no tenant:Every other writer of that table does —
skills.ts:262,cli.ts:2713/4116/4122/4363,router.ts:2962, and the baseline backfill atmigrate.ts:647— and they refresh it on conflict too. This is the one path that doesn't, and it is the path an agent uses to write its own memory.agent_climate— the column isTEXT NOT NULL DEFAULT 'personal'(migrate.ts:822) and neither of its two INSERT sites (climate.ts:57,cli.ts:4250) names it. So every climate row in every workspace is labelled'personal', andDELETE FROM agent_climate WHERE company_id = 'co-acme'can never match one.Demonstrated
Postgres 16, one agent in
co-tenant, one row written the FUSE way and one written the way a peer writer does:Same agent, same table, same deletion — one row goes, one stays. The one that stays is the memory file.
The fix, in two halves
Stop producing them.
fs-endpointsfills the tenant from the agent'sparticipantsrow with a scalar subquery, so it stays a single round trip and still inserts when the lookup finds nothing — behaviour is unchanged where it was already correct. TheON CONFLICTarm usesCOALESCE(EXCLUDED.company_id, agent_workspace.company_id), so a row already written without one heals on the agent's next write and a known tenant is never overwritten with NULL.Reach the ones already written. The purge also sweeps the agent-owned subset by
agent_id:agentIdsis already built earlier in the same transaction forboard_mention_reads. Deleting by owner needs no backfill migration to reach rows written before this, which matters because those rows exist in every deployment today.computersis deliberately not in the subset — it is keyed by the machine, not an agent. I confirmed against the live schema that all eight tables in the subset really do have anagent_idcolumn.Why the existing test didn't catch it
workspace-management.test.ts's purge test seeds every row with a correctcompany_id:`INSERT INTO agent_runs (id, agent_id, company_id) VALUES ('run-managed', 'agent-managed', 'co-managed')`That is the one shape the broken writers never produce, so the test passes on the broken code. The new tests seed the way the real writers do.
Verification
fs-endpointschange → test 5 fails (the row is written without a workspace)PUT /runtime/fs/writewith a minted agent token rather than issuing the SQL itself, so it pins the writer, not my restatement of it.company_idsweep already reached is still removed, and another workspace's agent keeps its data — deleting by owner must not reach past the workspace being deleted.workspace-management.test.tsstays green, 12/12.tsc --noEmit,biome lint ., all three source guards clean.Related, not fixed here
agent_climate's writers should probably namecompany_idrather than relying on a'personal'default that is wrong for every non-personal workspace — the column and its index (idx_agent_climate_company) exist as if it were meaningful, andAGENT_ID_CASCADE_TABLESscopes on it. But climate reads are per-agent and global by design (ADR 0004 says so explicitly), so changing what the column holds is a separate decision with its own blast radius. This PR makes deletion correct without taking that on.