Skip to content

fix(acp): settle sessions before agent removal - #97

Merged
dvaJi merged 2 commits into
masterfrom
fix/acp-agent-removal-settlement
Sep 9, 2026
Merged

dvaJi merged 2 commits into
masterfrom
fix/acp-agent-removal-settlement

Conversation

@dvaJi

@dvaJi dvaJi commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Uninstalling a disabled or uninstalled ACP registry agent was a permanent dead-end. Once a conversation bound to an ACP agent, acp_sessions rows were never deleted by any production path, so the uninstall guard stayed true forever. Sessions bound to a disabled agent also threw during transfer assessment (agent-type lookup filtered to enabled && installed), and the bulk delete/move routes performed raw row operations with no settlement — running generations were not cancelled and queued inputs silently leaked.

Inspired by ThinkInAIXYZ/deepchat#2188, re-implemented natively for our daemon-owned session architecture. SDD docs: docs/issues/acp-agent-removal-settlement/.

Root causes fixed

  1. Sticky uninstall guard — AcpSessionPersistence.deleteSession() had zero callers; deleting conversations never released acp_sessions bindings.
  2. Stranded sessions — disabled/uninstalled agents vanished from config.listAgents, so resolveAgentImplementation threw and impact/move/delete failed for their conversations.
  3. No settlement — sessions.deleteAgentSessions / sessions.moveAgentSessions / sessions.moveToAgent / sessions.delete deleted or repointed rows without cancelling active turns or discarding queued input.
  4. Hardcoded providerId: "acp" in daemon move handlers broke any move whose target was an Argos agent (next send failed with "ACP agent not found").

Changes

  • sessionSettlement (daemon): discard queue-mode pending inputs (steer kept — cancel suppresses the drain), cancel active generation with a bounded 10s wait for status to leave generating, purge ACP bindings best-effort.
  • Settlement wired into all four daemon ownership-change routes.
  • New config.getAgentType route: state-agnostic type lookup so disabled/uninstalled registry agents stay assessable.
  • purgeAcpSessionData on the ACP execution port: cancel + unbind + delete acp_sessions rows; the uninstall guard becomes accurate once conversations are gone.
  • Daemon move handlers now resolve the target agent type: ACP targets keep providerId: "acp"; Argos targets receive the target agent's default model.
  • AcpSettings uninstall flow: prefetches transfer impact and offers move/delete of conversations via the shared AgentTransferDialog instead of failing the guard.

UI (uninstall flow)

BEFORE                                     AFTER
[Uninstall agent w/ conversations]         [Uninstall agent w/ conversations]
        |                                          |
[Guard error toast - dead end]             [Confirm dialog: has conversations]
                                                   |
                                           [Transfer dialog]
                                            /          \
                                    [Move to agent]  [Delete conversations]
                                           \            /
                                        [Uninstall succeeds]

Testing

  • Daemon: 384 tests pass, incl. new settlement unit tests (queue discard/steer keep, cancel+settle wait, timeout fail-safe, purge tolerance), state-agnostic type lookup, delete-route settlement, Argos-target move regression.
  • Desktop: test:main 1737 passed / 6 skipped.
  • bun run typecheck, bun run lint (all architecture guards), bun run format clean.

Uninstalling a disabled or uninstalled ACP registry agent was a
permanent dead-end: acp_sessions binding rows were never deleted by any
production path, sessions bound to a disabled agent threw during
transfer assessment (agent type lookup filtered to enabled+installed),
and bulk delete/move routes performed raw row operations with no
settlement - running generations were not cancelled and queued inputs
leaked.

- add sessionSettlement: discard queue-mode pending inputs (steer kept,
  cancel suppresses the drain), cancel active turns with a bounded wait
  for status to leave generating, purge ACP bindings best-effort
- wire settlement into sessions.delete, sessions.deleteAgentSessions,
  sessions.moveAgentSessions, sessions.moveToAgent
- add config.getAgentType route: state-agnostic type lookup so disabled
  or uninstalled registry agents stay assessable
- add purgeAcpSessionData on the ACP execution port so the uninstall
  guard becomes accurate once conversations are gone
- make daemon move handlers target-aware: Argos targets receive the
  target agent's default model instead of a hardcoded acp label that
  broke the next send
- AcpSettings uninstall now offers move/delete of conversations via the
  shared AgentTransferDialog instead of failing the guard

SDD: docs/issues/acp-agent-removal-settlement
Copilot AI balanced review requested due to automatic review settings September 8, 2026 15:57
@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 09783d13-3913-4a1a-b759-d736052fbba1


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

React Doctor found 1 new issue in 1 file · 1 warning · score 93 / 100 (Great) · 1 fixed · vs master

1 warning

src/components/agent/AgentTransferDialog.tsx

  • ⚠️ L41 React function has high control-flow complexity no-high-complexity-react-function

Reviewed by React Doctor for commit 5f6f3a7. See inline comments for fixes.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

This PR is not yet safe to merge because failed moves can permanently strip source-session state, and failed queue cleanup can transfer pending inputs to the new agent.

Two ownership-transfer paths can produce materially incorrect state: irreversible settlement occurs before fallible target validation and mutation, while swallowed pending-input cleanup failures allow old queued inputs to survive a successful move. The uninstall UI also has a non-blocking stale-response race.

Files Needing Attention: apps/daemon/src/dispatch/daemonDispatcher.ts, apps/daemon/src/host/sessionSettlement.ts, packages/ui/settings/components/AcpSettings.tsx

Important Files Changed

Filename Overview
apps/daemon/src/dispatch/daemonDispatcher.ts Wires settlement and target-aware execution context into ownership-change routes, but performs destructive settlement before fallible target validation and transfer.
apps/daemon/src/host/sessionSettlement.ts Implements queue cleanup, cancellation polling, and ACP purge, but fails open when queued inputs cannot be read or deleted.
apps/daemon/src/host/acp-provider-execution.ts Adds best-effort cancellation and durable ACP binding deletion without an identified defect.
apps/daemon/src/host/daemonAcpConfig.ts Adds state-agnostic lookup for manual and registry ACP agents.
apps/daemon/src/host/daemonConfigPresenter.ts Resolves built-in, runtime Argos, and disabled or uninstalled ACP agent types.
packages/shared-contracts/src/routes/config.routes.ts Defines the typed state-agnostic agent-type route contract.
packages/backend-core/src/dispatch/config/configRouteHandler.ts Dispatches the new agent-type route through the config presenter.
packages/ui/settings/components/AcpSettings.tsx Adds move/delete choices to registry-agent uninstall, but asynchronous impact responses can become associated with the wrong selected agent.
packages/ui/src/components/agent/AgentTransferDialog.tsx Adds a reusable optional dialog-title override.
apps/daemon/test/daemonSessionSettlement.test.ts Covers ordinary settlement, timeout, and tolerated cleanup failures, but does not cover a failed cleanup followed by a move or a post-settlement move failure.

Fix all with Greploop Fix All in Codex

Prompt To Fix All With AI
### Issue 1
apps/daemon/src/dispatch/daemonDispatcher.ts:3172-3174
**Settlement Precedes Move Validation**

The route permanently settles the source session before validating the target or attempting the move. If the target no longer exists, has no usable default model, or the repository move fails, the session stays with its original agent even though its queued inputs have already been deleted, its active turn cancelled, and its ACP bindings purged. Validate the target first and avoid making these changes irreversible until the move can complete.

### Issue 2
apps/daemon/src/host/sessionSettlement.ts:75-87
**Failed Cleanup Leaks Inputs**

Failures to list or delete queued inputs are swallowed, so a move continues as though cleanup succeeded. Moving a session preserves its pending-input rows; therefore, any queue input left behind remains attached to the same session ID and can be consumed by the newly assigned agent even though it was intended for the previous agent. The move should stop when queued inputs cannot be enumerated or removed.

### Issue 3
packages/ui/settings/components/AcpSettings.tsx:449-467
**Stale Uninstall Results Win**

Each uninstall selection starts an asynchronous impact lookup that updates shared state without checking which agent is still selected. If the first dialog is closed and another agent is selected before the first request finishes, the older response can overwrite the newer agent's impact and targets. The second agent may then follow the wrong uninstall branch. Cancel stale requests or associate each response with its selected agent.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(acp): settle sessions before agent r..." | Re-trigger Greptile

Comment on lines 3172 to 3174
await settleSessionForOwnershipChange(input.sessionId, settlementHost);
const targetContext = await resolveMoveTargetContext(input.toAgentId);
const updated = await repo.moveSessionToAgent(input.sessionId, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Settlement Precedes Move Validation

The route permanently settles the source session before validating the target or attempting the move. If the target no longer exists, has no usable default model, or the repository move fails, the session stays with its original agent even though its queued inputs have already been deleted, its active turn cancelled, and its ACP bindings purged. Validate the target first and avoid making these changes irreversible until the move can complete.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/daemon/src/dispatch/daemonDispatcher.ts
Line: 3172-3174

Comment:
**Settlement Precedes Move Validation**

The route permanently settles the source session before validating the target or attempting the move. If the target no longer exists, has no usable default model, or the repository move fails, the session stays with its original agent even though its queued inputs have already been deleted, its active turn cancelled, and its ACP bindings purged. Validate the target first and avoid making these changes irreversible until the move can complete.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

Comment on lines +75 to +87
async function discardQueueInputs(sessionId: string, host: SettleSessionHost): Promise<string[]> {
const inputs = await host.listPendingInputs(sessionId).catch(() => [] as PendingSessionInputRecord[]);
const discarded: string[] = [];
for (const input of inputs) {
if (input.mode !== "queue") continue;
try {
await host.deletePendingInput(sessionId, input.id);
discarded.push(input.id);
} catch {
// A queued input that cannot be discarded must not block removal
// outright, but it also must not be silently lost: leave it in place.
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed Cleanup Leaks Inputs

Failures to list or delete queued inputs are swallowed, so a move continues as though cleanup succeeded. Moving a session preserves its pending-input rows; therefore, any queue input left behind remains attached to the same session ID and can be consumed by the newly assigned agent even though it was intended for the previous agent. The move should stop when queued inputs cannot be enumerated or removed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/daemon/src/host/sessionSettlement.ts
Line: 75-87

Comment:
**Failed Cleanup Leaks Inputs**

Failures to list or delete queued inputs are swallowed, so a move continues as though cleanup succeeded. Moving a session preserves its pending-input rows; therefore, any queue input left behind remains attached to the same session ID and can be consumed by the newly assigned agent even though it was intended for the previous agent. The move should stop when queued inputs cannot be enumerated or removed.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

Comment on lines 449 to +467
const confirmRegistryAgentUninstall = (agent: AcpRegistryAgent) => {
setUninstallAgent(agent);
setUninstallOpen(true);
setUninstallImpact(null);
setUninstallImpactLoading(true);
setUninstallTransferError(null);
// Prefetch the conversation impact so the confirm step can route to the
// transfer dialog when the agent still owns conversations.
void Promise.all([sessionClient.getAgentTransferImpact(agent.id), configClient.listAgents()])
.then(([impact, agents]) => {
setUninstallImpact(impact);
setUninstallTargets(agents ?? []);
})
.catch((error) => {
console.warn("[ACP] uninstall impact lookup failed:", error);
setUninstallImpact(null);
})
.finally(() => setUninstallImpactLoading(false));
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale Uninstall Results Win

Each uninstall selection starts an asynchronous impact lookup that updates shared state without checking which agent is still selected. If the first dialog is closed and another agent is selected before the first request finishes, the older response can overwrite the newer agent's impact and targets. The second agent may then follow the wrong uninstall branch. Cancel stale requests or associate each response with its selected agent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/settings/components/AcpSettings.tsx
Line: 449-467

Comment:
**Stale Uninstall Results Win**

Each uninstall selection starts an asynchronous impact lookup that updates shared state without checking which agent is still selected. If the first dialog is closed and another agent is selected before the first request finishes, the older response can overwrite the newer agent's impact and targets. The second agent may then follow the wrong uninstall branch. Cancel stale requests or associate each response with its selected agent.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Settlement currently fails open on cleanup errors, and the uninstall UI still blocks active or queued sessions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds daemon-owned session settlement so ACP agents can be uninstalled after their conversations are moved or deleted.

Changes:

  • Adds state-independent agent-type resolution and ACP binding cleanup.
  • Settles active/queued sessions across ownership-changing routes.
  • Introduces an ACP uninstall transfer workflow and regression tests.
File summaries
File Description
packages/ui/src/components/agent/AgentTransferDialog.tsx Supports custom dialog titles.
packages/ui/settings/components/AcpSettings.tsx Adds move/delete-before-uninstall flow.
packages/shared-contracts/src/routes/config.routes.ts Defines agent-type lookup contract.
packages/shared-contracts/src/routes.ts Registers the new route.
packages/backend-core/src/ports/hotPathPorts.ts Adds ACP purge capability.
packages/backend-core/src/dispatch/config/configRouteHandler.ts Dispatches agent-type lookup.
packages/acp-runtime/src/session/acpSessionPersistence.ts Deletes all conversation bindings.
docs/issues/acp-agent-removal-settlement/tasks.md Records implementation tasks.
docs/issues/acp-agent-removal-settlement/spec.md Documents requirements and decisions.
docs/issues/acp-agent-removal-settlement/plan.md Describes implementation layers.
apps/desktop/src/main/presenter/configPresenter/index.ts Uses daemon agent-type lookup.
apps/daemon/test/daemonSessionSettlement.test.ts Tests settlement behavior.
apps/daemon/test/daemonSessionRoutes.test.ts Tests route settlement and moves.
apps/daemon/test/daemonAcpConfig.test.ts Tests state-independent ACP lookup.
apps/daemon/src/index.ts Wires ACP purge into execution.
apps/daemon/src/host/sessionSettlement.ts Implements ownership settlement.
apps/daemon/src/host/daemonConfigPresenter.ts Resolves agent types across states.
apps/daemon/src/host/daemonAcpConfig.ts Looks up disabled/uninstalled agents.
apps/daemon/src/host/acp-provider-execution.ts Purges runtime and durable bindings.
apps/daemon/src/dispatch/daemonDispatcher.ts Settles delete/move routes and resolves targets.
Review details

Suppressed comments (2)

apps/daemon/src/host/sessionSettlement.ts:76

  • Pending-input inspection failures are treated as an empty queue, so a move can continue without discarding queued work and attach that work to the new owner. This should fail conservatively rather than recreating the leak this settlement path is intended to prevent.
  const inputs = await host.listPendingInputs(sessionId).catch(() => [] as PendingSessionInputRecord[]);

apps/daemon/src/dispatch/daemonDispatcher.ts:3173

  • This settles and purges the source before checking whether the target exists or has a usable model. If target resolution fails, the move reports an error after queued input and ACP continuity were already removed from the unchanged source; validate the target first.
      await settleSessionForOwnershipChange(input.sessionId, settlementHost);
      const targetContext = await resolveMoveTargetContext(input.toAgentId);
  • Files reviewed: 20/20 changed files
  • Comments generated: 9
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +61 to +65
try {
await host.purgeAcpSessionData?.(sessionId);
} catch {
// best-effort: purge failures must not block the ownership change
}
Comment on lines +70 to +73
async function currentStatus(sessionId: string, host: SettleSessionHost): Promise<string | null> {
const session = await host.getSession(sessionId).catch(() => null);
return session?.status ?? null;
}
deletedSessionIds.push(session.id);
continue;
}
const targetContext = await resolveMoveTargetContext(input.toAgentId);
Comment thread apps/daemon/src/host/sessionSettlement.ts Outdated
Comment on lines +462 to +465
.catch((error) => {
console.warn("[ACP] uninstall impact lookup failed:", error);
setUninstallImpact(null);
})
sourceAgentId={uninstallAgent?.id ?? ""}
sourceAgentName={uninstallAgent?.name ?? ""}
agents={uninstallTargets}
impact={uninstallImpact}
Comment on lines +56 to +66
## 5. Desktop shell (production path + legacy parity)

- `apps/desktop/src/main/presenter/configPresenter/index.ts`: `getAgentType` tries the new
`config.getAgentType` route first; falls back to agentRepository → `config.listAgents` chain.
- `apps/desktop/src/main/presenter/agentSessionPresenter/index.ts` (legacy no-daemon path):
- `assessTransferSession` also returns `hasPendingInput`.
- Add `settleSessionForOwnershipChange(session)`: discard queue inputs via the agent
implementation, cancel + poll status via `agent.getSessionState` (10 s cap), keep steer
inputs.
- Use it in `moveAgentSessions`, `moveSessionToAgentInternal`, `deleteAgentSessions`, and
`deleteSessionInternal` (before destroy), replacing the hard `blockReason` throws.
Comment on lines +87 to +89
- Desktop (vitest):
- `apps/desktop/test/main/presenter/agentSessionPresenter/settlement.test.ts` (new): legacy
path settles active/queued sessions before move/delete; assessment conservative on failure.
Comment on lines +50 to +51
- No desktop-local presenter rewrite; the daemon owns sessions and the daemon paths are fixed
first. The legacy no-daemon desktop path gets parity-level settlement only.
Review hardening for the settlement flow:

- settlement fails closed: pending-input list/delete failures, session
  status read failures, and ACP purge failures abort the ownership
  change instead of leaking queued inputs or leaving uninstall-guard
  bindings behind
- move routes resolve and validate the target context BEFORE settling,
  so an invalid or model-less target can no longer discard queued
  inputs or cancel turns of sessions that stay put; bulk moves resolve
  the target once
- AcpSettings uninstall: stale impact lookups are ignored per agent,
  failed lookups fail closed with a Retry action, and the transfer
  dialog accepts allowBlocked because settlement handles
  active/queued sessions
- align SDD docs with the dropped desktop-local settlement (D7)
@dvaJi
dvaJi merged commit 3dc6eed into master Sep 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants