From 90e32923ef77d094e758a7140eb8f3e41d755098 Mon Sep 17 00:00:00 2001 From: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> Date: Thu, 6 Aug 2026 16:38:47 -0400 Subject: [PATCH] fix(desktop): control_result is delivery confirmation, not terminal outcome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per interface note from Paul (2026-08-06): control_result statuses (sent | no_active_turn | channel_full | channel_closed | no_channel) confirm whether the permission_decision click was delivered to the harness, not whether the permission was applied/denied. Terminal outcomes arrive as enveloped acp_write frames correlated by requestNonce. The card retirement matrix will be wired once Thufir's review of Duncan's buzz-acp contract lands and NIP-AO is pinned. Updated the control_result handler to preserve card actionability on delivery — the PermissionDecisionButtons component already handles button-level pending-state reset via its own catch handler if the fire-and-forget send fails. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/ui/agentSessionTranscript.ts | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.ts b/desktop/src/features/agents/ui/agentSessionTranscript.ts index e3c7f4f9c..1a352ed4e 100644 --- a/desktop/src/features/agents/ui/agentSessionTranscript.ts +++ b/desktop/src/features/agents/ui/agentSessionTranscript.ts @@ -1177,35 +1177,24 @@ export function processTranscriptEvent( } } } else if (event.kind === "control_result") { - // Retire any pending actionable permission card on a `permission_decision` - // control result. All terminal statuses (applied, denied, timed_out, - // cancelled, uncertain) close the card. "uncertain" gets the pinned copy: - // the agent process stopped before the harness could continue, so the - // outcome is genuinely unknown — never "denied", never "failed closed". + // `control_result` for `permission_decision` is a **delivery confirmation**, + // not a terminal outcome. Status values are: sent | no_active_turn | + // channel_full | channel_closed | no_channel. + // + // A non-"sent" status means the click did not reach the harness — the card + // stays actionable so the user can retry. Terminal outcomes (applied, + // denied, timed_out, cancelled, uncertain) arrive as enveloped acp_write + // frames correlated by requestNonce (see the acp_write branch above). + // That path will be wired once Thufir's review of Duncan's contract lands. const payload = asRecord(event.payload); const frameType = asString(payload.type); if (frameType === "permission_decision") { - const nonce = asString(payload.requestNonce); - const terminalStatus = asString(payload.status); - const itemId = nonce ? d.pendingPermissionsByNonce.get(nonce) : null; - if (itemId && terminalStatus) { - const existing = d.itemsById.get(itemId); - if (existing?.type === "lifecycle") { - const outcomeText = - terminalStatus === "uncertain" - ? "Approval outcome unknown; agent process stopped before it could continue." - : describePermissionOutcome(terminalStatus, null, new Map()); - replaceItem(d, itemId, { - ...existing, - outcome: outcomeText, - actionable: false, - }); - } - if (nonce) { - d.pendingPermissionsByNonce = new Map(d.pendingPermissionsByNonce); - d.pendingPermissionsByNonce.delete(nonce); - } - } + const deliveryStatus = asString(payload.status); + // If delivery failed, the PermissionDecisionButtons component handles + // button-level pending-state reset via its own catch handler. No card + // retirement here — the card stays actionable until a terminal acp_write + // frame confirms the outcome. + void deliveryStatus; // acknowledged; no card mutation on delivery results } }