fix(web): report a native dialog's outcome on the conversation's own turn frame - #2172
Conversation
|
Verdict: REQUEST_CHANGES
The formal sent by |
…turn frame A native dialog reported through the app bridge, so its report expired with the card that opened it: a reader who pressed Create after the card was settled — by the session ending, by a fifth card in the conversation, or by the page being closed — got the agent they asked for and a conversation that never heard about it, waiting on something that already existed. The bridge is the authorization model for a SANDBOXED frame: agent-authored HTML may not name its own sender, so everything it can reach is looked up from the card the daemon itself opened. A native dialog has no such question to answer — it is the Console's own form, running on the reader's session under their own JWT — so that gate buys nothing there and only loses the report. It now reports the way the composer sends, and the way an approval decision already did: an ordinary webchat turn, addressed by conversation and agent, with no card in the path. That also fixes a silent drop the bridge was not responsible for. A turn's React key changes when its live steps become persisted rows, which REMOUNTS the card under a dialog the reader is still filling in. The old callback belonged to the unmounted instance and returned at its first line, reporting nothing and showing nothing; the cleanup had already closed the dialog, and the module-level open-once guard kept the remounted card from putting it back. Per-open deduplication now lives in the dialog's own closure, and a remount resumes the dialog instead of discarding it — while a reader who actually left still leaves nothing armed behind them. Settlement is now a frame of its own, because the daemon no longer learns of one by carrying the other: a delivered report closes the card, which is what keeps a fresh browser session from opening a dialog over a form already submitted. A report that was not delivered settles nothing, and a settlement arriving after a report no longer shuts a dialog holding its own final reveal step. The sandboxed frame's bridge is untouched, and so is the daemon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review catches on the report path. Settling the card on `pgNotice`'s verdict read acceptance as delivery. The conversation takes a turn synchronously and may hold it — queued behind a running turn, where the reader can still cancel it — so a card settled there would report once, silently, into a message that never went: the failure this branch exists to remove, in a narrower window. A report now settles nothing, and the card stays open for the submit it may still owe. What a delivered report records is local and presentational — this browser submitted this form, so a second tab, which has its own `sessionStorage`, does not open a dialog over it. The remount resume was also unbounded, so navigating away with a dialog open and coming back later reopened a form the reader had walked away from. A remount lands in the commit that unmounted the card; returning to a route cannot. The offer is therefore bounded by that window, and the test that covers it advances the clock instead of completing the dialog first — which is what let the old case pass without exercising the path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1667ead to
9bfdbb0
Compare
|
Thanks — two of three were real. Addressed in [P1] [P1] [P2] the On the environment note: |
There was a problem hiding this comment.
The updated revision addresses all prior findings. It restores the 1.12.0-agentconnect.2 sandbox runtime pin, keeps the daemon-owned card live when a report turn is only locally accepted or queued, and bounds the remount handoff so a later navigation return does not reopen an abandoned dialog. The local reported marker is presentational and leaves the explicit reopen path available. git diff --check passes. I could not rerun the focused Vitest case in this runner because its file-creation-limited dependency linking did not complete; the implementation and updated regression cases are consistent with the intended lifecycle.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
|
Thanks for the clarification. I reviewed sent by |
A native dialog reported its outcome through the app bridge, so the report expired with the
card that opened it. A reader who pressed Create after the card had settled — the session
ended, a fifth card arrived in the conversation, or the page was closed — got the agent they
asked for and a conversation that never heard about it, left waiting on something that
already existed.
The bridge is the authorization model for a sandboxed frame: agent-authored HTML may not
name its own sender, so everything it can reach is looked up from the card the daemon itself
opened. A native dialog has no such question to answer — it is the Console's own form,
running on the reader's session under their own JWT — so that gate buys nothing there and
only loses the report. It now reports the way the composer sends, and the way an approval
decision already did: an ordinary webchat turn, addressed by conversation and agent, with no
card in the path.
That also fixes a silent drop the bridge was not responsible for. A turn's React key changes
when its live steps become persisted rows, which remounts the card under a dialog the
reader is still filling in. The old completion callback belonged to the unmounted instance
and returned at its first line — reporting nothing, showing nothing — while the cleanup had
already closed the dialog and the module-level open-once guard kept the remounted card from
putting it back. Per-open deduplication now lives in the dialog's own closure, and a remount
resumes the dialog instead of discarding it; a reader who actually left still leaves nothing
armed behind them.
Settlement becomes a frame of its own, because the daemon no longer learns of one by carrying
the other: a delivered report closes the card, which is what keeps a fresh browser session
from opening a dialog over a form already submitted. A report that was not delivered
settles nothing, and a settlement arriving after a report no longer shuts a dialog holding its
own final reveal step.
The sandboxed frame's bridge is untouched, and so is the daemon — an older Console against a
newer daemon still reports the old way.
Changes
McpAppCardgrows anonReportmember;NativeIntegrationAppCardreports and gates itsliveness on that instead of
onRpc.SessionDetailViewsupplies it frompgNotice, behind the same reachability gateappRpcand
closeAppalready ride.dialog the card still owns.
docs/designs/webchat-native-integration-ui.mdrecords where a native dialog parts companywith a sandboxed frame, and why settlement is now separate from the report.
Verification
pnpm --filter @agentconnect.md/web test— 237 files, 2725 tests, green.typecheck,eslintandprettier --checkclean on the touched files.NativeIntegrationAppCard.test.tsxmoves toonReportand gains four cases: a remountunder an open dialog still reports, a reader who left is not followed by a reopened dialog,
a conversation that refuses the report says so, and settlement fires only on a delivered
report. The pre-existing "reopenable after a refresh" case now models a real reload
(
vi.resetModules()+ re-import) rather than an in-process remount — as written it was thesame shape as the bug, which is why it moved.
Not yet exercised against a deployed Console; the fix is client-side and the next deploy is
where the round trip gets its live check.
🤖 Generated with Claude Code