Summary
There is no way to free an agent name without destroying the agent's history, so the operator escape hatch fails in exactly the situation that needs it: an agent that has been doing work and now needs its name back.
deleteAgent issues a bare delete:
// packages/engine/src/engine/agent.ts:277
export async function deleteAgent(db: Db, workspaceId: string, name: string) {
const [agent] = await db
.select()
.from(agents)
.where(and(eq(agents.workspaceId, workspaceId), eq(agents.name, name)));
if (!agent) return false;
await db.delete(agents).where(eq(agents.id, agent.id));
await db.delete(nodes).where(eq(nodes.id, directNodeIdForAgent(agent.id)));
return true;
}
Four foreign keys to agents.id are declared bare — no onDelete, i.e. RESTRICT:
| table / column |
packages/engine/src/db/schema.ts |
channels.created_by |
:455 |
messages sender |
:503 |
files |
:666 |
webhooks.created_by |
:759 |
Every other FK to agents.id in that file carries an explicit onDelete: 'cascade' or 'set null'. These four do not, so the delete is refused for any agent that has ever created a channel, sent a message, uploaded a file, or created a webhook — which is every agent worth reclaiming a name from.
The failure is server-side, so agent-relay agent remove <name> fails too. There is currently no supported operation that frees a name.
Why this is worth fixing first
This is what turned a two-minute node restart into an unrecoverable name loss on 2026-08-07. A broker whose process is killed uncleanly never deregisters; the record stays behind holding the name; the name cannot be freed; the node has to come back under a different name and every --node <name> reference in the fleet has to be rewritten. With this fixed the same incident is a ten-second recovery.
It is also the only path that recovers an already-stranded name.
Do not fix with cascade
Adding onDelete: 'cascade' to these four would make the delete succeed by deleting every message the agent ever sent, plus the channels it created and the files it uploaded. The history is the point — that is a worse outcome than the current failure, and it is silent.
Proposed fix — tombstone rename
The unique constraint is (workspace_id, name). The name only has to stop colliding; the row does not have to go away.
Rename the row to a tombstone and mark it inactive:
name → <name>#released-<timestamp> (and handle to match)
status → inactive/released
- record the release in
metadata — note that metadata.release { reason, releasedAt } is already an established shape on live records, so this reuses an existing convention rather than inventing one
Result: the name is immediately free for re-registration, all four FK targets remain valid, every message keeps its sender, and no schema migration is required.
Worth deciding as part of the fix:
- whether this replaces
deleteAgent or becomes a sibling releaseAgentName, and what agent-relay agent remove maps to
- whether listing endpoints filter tombstoned rows by default (they should, or
agent list fills with #released- entries)
- tombstone name collision if the same name is released twice in the same second
Verification
deleteAgent and the four schema lines read from main at 08ddec7. The FK audit is the complete set of bare .references(() => agents.id) in schema.ts — lines 217, 257, 286, 353, 393, 476, 538, 570, 644, 711, 807, 852, 921, 950 all carry an explicit onDelete; only 455, 503, 666 and 759 do not.
Context
Filed from an incident brief covering the 2026-08-07 node outage. Two companion issues cover the relay side. Do not merge without Khaliq — he owns the merge gate.
Summary
There is no way to free an agent name without destroying the agent's history, so the operator escape hatch fails in exactly the situation that needs it: an agent that has been doing work and now needs its name back.
deleteAgentissues a bare delete:Four foreign keys to
agents.idare declared bare — noonDelete, i.e. RESTRICT:packages/engine/src/db/schema.tschannels.created_by:455messagessender:503files:666webhooks.created_by:759Every other FK to
agents.idin that file carries an explicitonDelete: 'cascade'or'set null'. These four do not, so the delete is refused for any agent that has ever created a channel, sent a message, uploaded a file, or created a webhook — which is every agent worth reclaiming a name from.The failure is server-side, so
agent-relay agent remove <name>fails too. There is currently no supported operation that frees a name.Why this is worth fixing first
This is what turned a two-minute node restart into an unrecoverable name loss on 2026-08-07. A broker whose process is killed uncleanly never deregisters; the record stays behind holding the name; the name cannot be freed; the node has to come back under a different name and every
--node <name>reference in the fleet has to be rewritten. With this fixed the same incident is a ten-second recovery.It is also the only path that recovers an already-stranded name.
Do not fix with cascade
Adding
onDelete: 'cascade'to these four would make the delete succeed by deleting every message the agent ever sent, plus the channels it created and the files it uploaded. The history is the point — that is a worse outcome than the current failure, and it is silent.Proposed fix — tombstone rename
The unique constraint is
(workspace_id, name). The name only has to stop colliding; the row does not have to go away.Rename the row to a tombstone and mark it inactive:
name→<name>#released-<timestamp>(andhandleto match)status→ inactive/releasedmetadata— note thatmetadata.release { reason, releasedAt }is already an established shape on live records, so this reuses an existing convention rather than inventing oneResult: the name is immediately free for re-registration, all four FK targets remain valid, every message keeps its sender, and no schema migration is required.
Worth deciding as part of the fix:
deleteAgentor becomes a siblingreleaseAgentName, and whatagent-relay agent removemaps toagent listfills with#released-entries)Verification
deleteAgentand the four schema lines read frommainat08ddec7. The FK audit is the complete set of bare.references(() => agents.id)inschema.ts— lines 217, 257, 286, 353, 393, 476, 538, 570, 644, 711, 807, 852, 921, 950 all carry an explicitonDelete; only 455, 503, 666 and 759 do not.Context
Filed from an incident brief covering the 2026-08-07 node outage. Two companion issues cover the relay side. Do not merge without Khaliq — he owns the merge gate.