Skip to content

fix(web): report a native dialog's outcome on the conversation's own turn frame - #2172

Merged
spacedragon merged 2 commits into
mainfrom
dev/yulong/sixgill
Sep 20, 2026
Merged

spacedragon merged 2 commits into
mainfrom
dev/yulong/sixgill

Conversation

@spacedragon

Copy link
Copy Markdown
Contributor

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

  • McpAppCard grows an onReport member; NativeIntegrationAppCard reports and gates its
    liveness on that instead of onRpc.
  • SessionDetailView supplies it from pgNotice, behind the same reachability gate appRpc
    and closeApp already ride.
  • The open-once guard distinguishes a remount from a reload; the inert-close only closes a
    dialog the card still owns.
  • docs/designs/webchat-native-integration-ui.md records where a native dialog parts company
    with 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, eslint and prettier --check clean on the touched files.
  • NativeIntegrationAppCard.test.tsx moves to onReport and gains four cases: a remount
    under 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 the
    same 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

@agentconnect-md-test

Copy link
Copy Markdown
Contributor

Verdict: REQUEST_CHANGES

  • [P1] docker/runtime-sandbox.Dockerfile:8 rolls Codex ACP back to .1, reintroducing the protected Full-access MCP cancellation bug fixed by base commit chore(docker): pin codex-acp 1.12.0-agentconnect.2 in the sandbox image #2171. Keep .2.

  • [P1] NativeIntegrationAppCard.tsx:94 treats pgNotice() as delivery confirmation, but it returns before connection and daemon acknowledgement. It can even represent a cancellable queued message. The card is immediately closed, so a later delivery failure loses the report and settles the card anyway.

  • [P2] NativeIntegrationAppCard.tsx:43 stores every open-dialog unmount in a module-scoped reopening set. Navigating away and returning later therefore reopens an abandoned dialog automatically. The added test completes the dialog before unmounting, so it does not cover this case.

git diff --check passed. The focused test did not reach Vitest because pnpm was absent and Corepack began materializing the entire 1,640-package workspace; I stopped that environment setup run.

The formal submitCodeReview attempt was rejected by GitHub with 422 Unprocessable Entity, so the REQUEST_CHANGES review was not formally recorded.

sent by review-bot (Codex · gpt-5.6-sol) · open in session

spacedragon and others added 2 commits September 20, 2026 06:43
…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>
@spacedragon

Copy link
Copy Markdown
Contributor Author

Thanks — two of three were real. Addressed in 9bfdbb0f.

[P1] runtime-sandbox.Dockerfile:8 — not this PR. The branch was cut from 8de9c84a, before #2171 landed, so the .1 you saw is main moving forward rather than a rollback: git diff --name-only over the branch lists six files and no Dockerfile, and git diff origin/main...HEAD (three-dot) shows none either. Rebased onto 786d6629 so the two-dot view agrees.

[P1] pgNotice() is acceptance, not delivery — agreed, and the settle is gone. You are right that it returns before the connection and the daemon ack, and right that it can represent a queued turn the reader may still cancel. Settling there would have reproduced the exact failure this PR removes, 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 instead is local and presentational — a localStorage marker so a second tab, which has its own sessionStorage, does not open a dialog over a form this browser already submitted. It suppresses the automatic opening and names the outcome; the card's own button still opens the form again.

[P2] the reopening set was unbounded — agreed. A remount lands in the commit that unmounted the card; returning to a route later cannot. The offer is now a timestamp checked against a 250 ms window, so navigating away with a dialog open and coming back leaves the card behind its button. You were also right about the test: it completed the dialog before unmounting, so it never exercised the path. It is split in two — one case advances the clock with an open dialog, the other keeps the completed-first path — plus two for the record-on-delivery behaviour above.

On the environment note: pnpm --filter @agentconnect.md/web test is green locally (237 files, 2727 tests), as are typecheck, eslint and prettier --check.

@agentconnect-md-test agentconnect-md-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@agentconnect-md-test

Copy link
Copy Markdown
Contributor

Thanks for the clarification. I reviewed 9bfdbb0f, confirmed the rebase, no-settlement behavior, and bounded remount handoff, and approved that revision.

sent by review-bot (Codex · gpt-5.6-sol) · open in session

@spacedragon
spacedragon merged commit 7acb2fd into main Sep 20, 2026
2 checks passed
@spacedragon
spacedragon deleted the dev/yulong/sixgill branch September 20, 2026 06:53
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.

1 participant