Skip to content

feat(guest): turn-based guest participation (fix iOS guest never replying)#3

Open
boggspa wants to merge 3 commits into
masterfrom
feat/ios-guest-turn-based
Open

feat(guest): turn-based guest participation (fix iOS guest never replying)#3
boggspa wants to merge 3 commits into
masterfrom
feat/ios-guest-turn-based

Conversation

@boggspa

@boggspa boggspa commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Problem

Guest chats (host + one attached guest) worked on desktop but the guest never replied on iOS — you could add/configure a guest, but only starting a side-chat with it directly produced a response.

Root cause (GAP B from the 2026-06-16 guest-parity review): the guest trigger + reply-mirror lived only in the renderer (App.tsx). iOS-origin turns run through the Mac bridge's composerPromptFn, which never touches the renderer — so it only ever dispatched the host. The bridge didn't even tell the host a guest was attached.

Separately, desktop was a parallel fan-out (host and guest fired together), so the guest never saw the host's reply to the same turn.

Fix — turn-based guest participation on both platforms

Per the chosen behaviour: host responds first, then the guest (so the guest sees the host's reply); an @-tag aims the prompt at only that agent (ensemble-style).

  • New shared module src/main/GuestParticipantRun.ts (node-safe, unit-tested): steering preamble, parent-transcript peer-context builder, full guest-prompt composer, and the guest-return-${runId} reply-mirror message builder (deduped by guestRunId). Provider-label injected so it's tree-agnostic.
  • Bridge (the iOS fix)composerPromptFn now passes guestParticipant to the host prompt (host-awareness), routes @-tags (@guest → guest only, @parent/@host → host only, no tag → host then guest), and after the host run finalizes dispatches the guest as a normal leaf run on its child chat (so it answers with the host's reply in context), then mirrors the guest's reply into the parent transcript on the guest run's finalize. Wired via a module-ref runner assigned in the bridge closure so the module-scope finalizeBridgeRunTranscript can reach the closure dispatch helpers.
  • Renderer (parity) — the fan-out now defers the guest: it's dispatched from the run-completion handler once the host run on that chat finishes (with a fresh chatRecord), instead of firing in parallel. @guest stays immediate; @parent/@host stays host-only.

iOS needs no changes — it already renders guestParticipantReply provider-tinted/attributed (GAP A, cb74513a) and excludes the guest from the side-chats tab (GAP C, 56899180). Once the Mac mirrors + broadcasts the guest reply, the phone shows it.

Verification

  • npm run typecheck:node + npm run build (main/preload/renderer) — green; confirms the cross-tree extractGuestParticipantAddressTarget import bundles into main.
  • Full vitest suite: 405 files / 4838 tests passing.
  • New unit tests: GuestParticipantRun.test.ts (13). @-routing already covered by ComposerMentionTrigger.test.ts; host guest-context by PromptComposition.test.ts.

Remaining verification (not in this PR)

  • Manual device test: on a paired iPhone, send to a guest chat → host replies, then the guest reply appears in the parent transcript; @guest / @parent route correctly.
  • Optional fake-provider e2e for the bridge host→finalize→guest→mirror chain (the bridge wiring is integration-level; the pure logic is unit-tested). Happy to add it if wanted before merge.

🤖 Generated with Claude Code

boggspa and others added 3 commits June 18, 2026 10:11
New node-safe module shared by the renderer and the Mac bridge: the
steering preamble, parent-transcript peer-context builder, the full
guest prompt composer, and the guest->parent reply mirror message
builder (deduped by guestRunId). Provider-label is injected so the
module stays free of any renderer/main label-resolution split.

Foundation for making guest participation turn-based AND reaching
iOS-origin turns (which run through the bridge, not the renderer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…turns)

iOS-origin turns run through the bridge composerPromptFn, which only ever
dispatched the host — so a configured guest never replied on the phone
(the trigger lived only in the renderer). Now the bridge:

- tells the host a guest is attached (passes guestParticipant to
  composeRunPrompt) so it can anticipate/avoid conflicts;
- routes @-tags like ensemble (@guest -> guest only, @parent/@host ->
  host only, no tag -> host then guest);
- after the host run finalizes (reply persisted), dispatches the guest as
  a normal leaf run on its child chat so it answers WITH the host's reply
  in context (turn-based);
- mirrors the guest's reply into the parent transcript on the guest run's
  finalize (guest-return-${runId}, deduped by guestRunId).

Driven via a module-ref runner assigned in the bridge closure so the
module-scope finalizeBridgeRunTranscript can reach the closure dispatch
helpers. iOS needs no changes — it already renders guestParticipantReply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The renderer fanned the guest out in parallel with the host, so the guest
never saw the host's reply to the same turn. Now a fan-out send defers the
guest: it's dispatched from the run-completion handler once the host run on
that chat finishes, with the host's reply already in the parent transcript
(passed via a fresh chatRecord). @guest stays immediate (guest only);
@parent/@host stays host only. This matches the new bridge behavior so
desktop and iOS guest chats now behave identically (turn-based).

Also drop the host-success gate on the bridge guest dispatch so the guest
still answers when the host run fails (fallback / second opinion), matching
the renderer completion hook which fires regardless of exit code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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