fix(cli): cascade dependent rows on groups delete (#2525)#2526
Merged
Conversation
The generic single-table DELETE handler for `ncl groups delete` always failed with SQLITE_CONSTRAINT_FOREIGNKEY when any session, destination, approval, role grant, membership, or channel wiring still pointed at the group — which is approximately always. Replace with a `customOperations.delete` handler on the `groups` resource that runs a single sync better-sqlite3 transaction and deletes the dependent rows in FK-respecting order before the final DELETE on `agent_groups`. Polymorphic `agent_destinations` rows with `target_type='agent'` and `target_id` pointing at the deleted group are also cleaned up so they don't dangle. Module tables (`agent_destinations`, `pending_approvals`) are guarded with `hasTable(getDb(), ...)` so installs without the agent-to-agent or approvals modules degrade silently. `container_configs.agent_group_id` already has ON DELETE CASCADE, so that row is removed automatically by the final DELETE. Out of scope (filed separately): killing any running container for the group, and on-disk cleanup of `groups/<folder>/` and `data/v2-sessions/<group-id>/`. The DB cascade is the load-bearing fix; the filesystem leak is cosmetic.
Apply prettier formatting to groups.ts and groups.test.ts.
Move the row-count queries out of a separate pre-flight pass and source the `removed` counts from each DELETE's `.changes` instead, so the response describes exactly what the transaction did rather than a snapshot from before it ran. Also drops the two double-quoted SQL strings (the `'agent'` literal is now a bound parameter) so quoting is consistent with the rest of the file.
migration-014 has ON DELETE CASCADE on container_configs.agent_group_id, so the row was already being removed by the final DELETE FROM agent_groups. Doing the delete explicitly here mirrors the shape of every other table in the cascade and lets the handler surface a container_configs count in the `removed` response, matching the rest of the breakdown.
This was referenced May 26, 2026
tamasPetki
pushed a commit
to tamasPetki/nanoclaw
that referenced
this pull request
Jun 4, 2026
…-cascade fix(cli): cascade dependent rows on groups delete (nanocoai#2525)
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.
Type of Change
Description
Fixes #2525.
ncl groups deletealways failed withSQLITE_CONSTRAINT_FOREIGNKEYbecause the generic single-table DELETE handler (src/cli/crud.ts) ran a bareDELETE FROM agent_groups WHERE id = ?while nine other tables held FKs into it. The group could not be deleted via the CLI at all.Why this shape
ON DELETE CASCADEto nine existing FK columns means rebuilding every dependent table (SQLite cannotALTER TABLE … ADD CONSTRAINT). That is a much larger blast radius than this bug warrants, and it bakes the cleanup policy into the schema rather than into one CLI op. A handler is the smallest change that fixes the failure.customOperations.deleteon thegroupsresource, not an edit todeleteAgentGroupinsrc/db/agent-groups.ts. That helper is a primitive used by other code paths that may not want a cascade; leaving it alone keeps the blast radius to the CLI verb.db.transaction()wrapping all the deletes — better-sqlite3 rolls the whole thing back if any statement throws. Counts are sourced from each DELETE's.changesso the response reports exactly what the transaction did, not a separate pre-flight snapshot.hasTableguards for the two module-installed tables (agent_destinations,pending_approvals) so an install without those modules degrades silently instead of throwing onno such table.messaging_groupsrows are shared platform identities (keyed onchannel_type+platform_id) and are deliberately preserved on agent-group delete; only the agent-specific join rows inmessaging_group_agentsare removed.What changed
src/cli/resources/groups.ts— removeddelete: 'approval'fromoperationsand added acustomOperations.deletehandler that runs the FK-ordered cascade inside a transaction. Returns{ deleted, removed: { sessions, pending_questions, pending_approvals, agent_destinations_owned, agent_destinations_pointing, pending_sender_approvals, pending_channel_approvals, messaging_group_agents, agent_group_members, user_roles, container_configs } }.src/cli/resources/groups.test.ts— new file. Three tests covering the regression: (1) full fixture exercising every dependent table includingcontainer_configs, (2) polymorphicagent_destinationsrow in agent B pointing at deleted agent A gets cleaned up, (3) unknown id returnshandler-error(not the cryptic SQLite error).Test plan
pnpm run typecheckcleanpnpm testclean (331/331)ncl groups delete --id <gid>for a group with a session, destination, and pending approval — response should contain non-zeroremovedcounts and the group should be gone fromagent_groups.Out of scope (will file follow-up)
data/v2-sessions/<group-id>/, and only exit on its own. Worth a separate change to callkillContainerfor each session before the DELETE.groups/<folder>/anddata/v2-sessions/<group-id>/. These are left untouched so an accidental delete is recoverable (re-insert the row, the folders are still there). Worth re-evaluating once we have a soft-delete / restore flow.Confidence
High on the fix correctness and the regression coverage. Medium on the FK ordering being exhaustive — I cross-checked every
REFERENCES agent_groups(id)insrc/db/(5 in initial schema, 1 each in migrations 011, 012, 014 with CASCADE, plus the two module migrations), and the test fixture inserts a row into every one. If a new FK lands later, the test will catch the regression because the bare delete throwsSQLITE_CONSTRAINT_FOREIGNKEY.