Skip to content

fix(web): late callback dedup across invocation boundaries#410

Closed
mindfn wants to merge 30 commits intomainfrom
fix/late-callback-dedup
Closed

fix(web): late callback dedup across invocation boundaries#410
mindfn wants to merge 30 commits intomainfrom
fix/late-callback-dedup

Conversation

@mindfn
Copy link
Copy Markdown
Collaborator

@mindfn mindfn commented Apr 9, 2026

Summary

  • Preserve just-finalized foreground/background stream refs across one invocation boundary so true late callbacks can still replace their original stream bubble
  • Fence finalized-fallback replacement with time/content and reply-target compatibility guards so callbacks do not overwrite a different invocation or reply chain
  • Disable store-level callback→finalized-stream bridging without invocation evidence, and mark callback-only unmatched bubbles so the store does not re-merge them heuristically
  • Restore late-stream suppression for callback-first, invocationless-placeholder, and finalized-fallback flows in both active and background paths

Scope

  • Foreground hook: packages/web/src/hooks/useAgentMessages.ts
  • Background flow: packages/web/src/hooks/useSocket-background.ts, packages/web/src/hooks/useSocket-background-system-info.ts, packages/web/src/hooks/useSocket.ts, packages/web/src/hooks/useSocket-background.types.ts
  • Reply-target compatibility: packages/web/src/hooks/reply-target-compat.ts
  • Store dedup contract: packages/web/src/stores/chatStore.ts, packages/web/src/stores/chat-types.ts
  • Regression coverage: packages/web/src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts, packages/web/src/hooks/__tests__/useSocket-background.test.ts, packages/web/src/stores/__tests__/chatStore-multithread.test.ts

Related

Verification

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts
  • Additional follow-up regressions were added in the touched test files during review iterations; the exact passing count changed across later heads, so the commands above are the source of truth

@mindfn mindfn requested a review from zts212653 as a code owner April 9, 2026 15:58
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2069219c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn mindfn force-pushed the fix/late-callback-dedup branch 2 times, most recently from f18fc65 to fa9c482 Compare April 10, 2026 05:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa9c482a65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

mindfn and others added 7 commits April 10, 2026 13:26
…vocation boundary

Root cause: When invocation_created fired, it cleared finalizedStreamRef,
preventing late callbacks (without msg.invocationId) from finding their
finalized stream bubble. This caused duplicate message bubbles when:
1. Backend omitted invocationId on callback (scheduler, rich blocks)
2. A new invocation started before the old callback arrived
3. All three lookup paths failed (strict match, invocationless scan, finalized ref)

Two-layer fix:
- Hook layer: Stop clearing finalizedStreamRef in invocation_created handler.
  Late callbacks can now find their target via the existing fallback path.
  Thread switch (resetRefs) still correctly clears all refs.
- Store layer: Add TD112 Phase 3 safety net in findAssistantDuplicate —
  merges callback into recent finalized stream bubble (with invocationId,
  non-streaming, within 30s, matching visibility/replyTo) as last resort.

Tests: 5 new test cases covering the specific gap + Phase 3 constraints.

[宪宪/Opus-46🐾]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y loss (#586 P1+P2)

Address gpt52 review findings:

P1 — Silent history loss: When inv-2 is callback-only (no stream), its
callback would merge into inv-1's finalized bubble via finalizedStreamRef,
silently replacing inv-1's content. Fix: invocation_created now "fences"
the finalizedStreamRef entry instead of clearing it. findInvocationless-
StreamPlaceholder applies a dual guard: time fence (5s grace) + content
match. Callbacks with different content are rejected — they create new
bubbles instead of overwriting the previous invocation's output.

P2 — Background thread parity: Apply the same fence mechanism to
finalizedBgRefs in useSocket-background-system-info.ts and
useSocket-background.ts. Background threads now use identical
time + content guards when matching callbacks across invocation
boundaries.

Store-level: TD112 Phase 3 (findAssistantDuplicate) also gains a
content guard — callbacks with different content won't merge into
recent finalized stream bubbles at the store dedup layer.

Tests: 6 new/updated regression tests covering both P1 (different content
→ no merge) and P2 (background path parity) scenarios.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…2 inline P1)

Empty-string content was skipping the comparison due to truthy checks,
allowing silent overwrite when finalized bubble had empty content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mindfn mindfn force-pushed the fix/late-callback-dedup branch from 2dd346d to 8f4204a Compare April 10, 2026 05:27
@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Rebased onto latest main, kept the late-callback fence fix, and pushed 8f4204a5 to address the merge-commit lint failure (project-setup-card-ime.test.tsx formatting). Local verification:

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f4204a5a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the two new P1s on top of 5c67618c.

What changed:

  • Unmatched callback-only bubbles no longer inherit an inferred invocationId; only explicit callback invocation IDs are persisted.
  • Added an internal callbackBridge.skipDedup guard so store Phase 2/3 dedup will not re-bridge a callback bubble that the hook layer already proved was unmatched.
  • Added regression coverage for both active-thread and background-thread paths.

Local verification:

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c67618c5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Follow-up on ad2f6b17 for the new P2s.

Adjustment:

  • Restored late-stream suppression for callback-first flows without explicit invocationId.
  • Kept the P1 safety fence: inferred suppression now only kicks in when there is no dangling finalized ref for that cat/thread, so delayed callbacks from an older invocation still stay isolated.
  • Added callback-first regression coverage for both active and background paths.

Local verification:

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

if (invocationId) {
replacedInvocationsRef.current.set(msg.catId, invocationId);

P1 Badge Skip inferred replacement lock after fenced callback merge

In the callback replacement path, invocationId may be inferred from the current cat state even when the replacement target came from a fenced finalized bubble (i.e., a late callback from a prior invocation). Persisting that inferred ID in replacedInvocationsRef makes shouldSuppressLateStreamChunk drop subsequent stream chunks for the current invocation, so users can lose live output right after the late callback merge. This became reachable with the new finalized-ref fence flow, so replacement suppression should not be locked from inferred IDs in this branch.


if (replacementTarget.invocationId) {
options.replacedInvocations.set(streamKey, replacementTarget.invocationId);

P1 Badge Avoid inferred suppression lock in bg fenced-merge path

The background callback replacement path has the same issue: replacementTarget.invocationId can be inferred from current thread state when the match came from finalizedBgRefs after fencing, and storing it in replacedInvocations causes shouldSuppressLateBackgroundStreamChunk to drop real stream text for the active invocation. In a late-callback-from-old-invocation scenario, this incorrectly suppresses the new invocation’s stream and can hide output until recovery logic runs.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P1 on f4c01ad3.

Local verification:

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts

This patch prevents fenced fallback callback merges from writing an inferred replacement lock for the new invocation, while preserving callback-first late-stream suppression when the replacement matched by invocation ID.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4c01ad35a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P2s on 13fb8eff.

Local verification:

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts

This keeps inferred suppression only for exact matches or active invocationless placeholders, while fenced finalized fallback merges still avoid locking the new invocation.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13fb8eff9b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P2 on f0b17dc8.

Local verification:

  • pnpm check
  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts

Background callback replacement now skips invocationless fallback matching whenever the callback carries an explicit invocationId, so explicit callback results can no longer overwrite unrelated placeholder bubbles.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0b17dc85a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P1 on d0c5d1ac.

What changed:

  • Unmatched callback bubbles with an explicit invocationId now write the late-stream suppression lock only when the current bound invocation already matches that same ID.
  • This avoids stale explicit callbacks suppressing legitimate live stream chunks while invocation binding is still catching up.
  • Applied the same guard to the background-thread path and added a matching regression test there.

Local verification:

  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-bubble-merge.test.ts src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts
  • pnpm check

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a81c8f1fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P2s on 7bf1bb59.

What changed:

  • finalized_fallback now requires replyTo compatibility in addition to the existing time/content fence before it can reuse a finalized stream bubble.
  • Applied the same reply-target fence to the background-thread path.
  • Added active/background regressions for same-content callbacks with different reply chains after done + invocation_created.

Local verification:

  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-bubble-merge.test.ts src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts
  • pnpm check

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bf1bb5919

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P2 on ed28553a.

What changed:

  • isReplyTargetCompatible now treats a missing replyTo on the finalized stream bubble as unknown evidence rather than a hard mismatch.
  • Concrete reply-target mismatches still block finalized_fallback, so the previous reply-chain fence remains intact.
  • Added active/background regressions for the late-callback case where the finalized stream placeholder had no reply metadata but the callback does.

Local verification:

  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-bubble-merge.test.ts src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts
  • pnpm check

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed28553a3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Addressed the latest P2 on f9bcc669.

What changed:

  • isReplyTargetCompatible now only rejects finalized_fallback when both reply targets are present and different.
  • Missing replyTo on either the finalized stream bubble or the callback is treated as unknown evidence, so legitimate late callback merges still go through.
  • Added active/background regressions for the missing-callback-reply case.

Local verification:

  • pnpm --filter @cat-cafe/web exec vitest run src/hooks/__tests__/useAgentMessages-bubble-merge.test.ts src/hooks/__tests__/useAgentMessages-late-callback-dedup.test.ts src/hooks/__tests__/useSocket-background.test.ts src/stores/__tests__/chatStore-multithread.test.ts
  • pnpm check

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9bcc669d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 10, 2026

@codex review

Latest fixes are in 6914bb74.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a033cd227a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@zts212653
Copy link
Copy Markdown
Owner

Hi @mindfn — thanks for the deep investigation and the thorough regression coverage. The race condition you've identified is real: clearing finalizedStreamRef immediately on invocation_created leaves a window where late callbacks from the previous invocation can't find their replacement target, creating duplicate bubbles.

That said, we think the fix can be approached more cleanly at a different layer.

Why we're hesitant about the current approach

The PR adds four new coordination mechanisms (fencedAt, matchKind, isReplyTargetCompatible, callbackBridge.skipDedup) across both active and background paths. Each layer addresses a real edge case, but the cumulative complexity is high — the Codex reviews surfaced 15+ P1/P2 findings, many of which were edge cases introduced by the new guards themselves. This is the fundamental challenge with client-side heuristic stacking: each fix narrows one gap but can open others.

What we think is a better path

Option A — Protocol-level fix (preferred):

The backend callback routes already carry invocationId in most broadcast payloads. The root cause of the invocationless fallback complexity is that some callback paths don't propagate it to the WebSocket event. If we close that gap at the API layer — ensuring every origin: 'callback' message always carries invocationId — the findCallbackReplacementTarget exact-match path works universally, and the entire findInvocationlessStreamPlaceholder / finalized_fallback / fencing machinery becomes unnecessary.

This is a one-time backend change that eliminates a whole class of client-side race conditions rather than fencing them case by case.

Option B — Lightweight client-side alternative (if backend change is deferred):

Instead of fencing finalizedStreamRef with fencedAt + content/reply-target guards, use a simple TTL map on stream finalization:

// On done(isFinal): record invocationId → messageId with auto-expiry
const LATE_CALLBACK_GRACE_MS = 5_000;
finalizedStreamRef.current.set(catId, { messageId, invocationId });
setTimeout(() => {
  const entry = finalizedStreamRef.current.get(catId);
  if (entry?.messageId === messageId) finalizedStreamRef.current.delete(catId);
}, LATE_CALLBACK_GRACE_MS);

invocation_created no longer needs to touch finalizedStreamRef at all — the TTL handles cleanup. Late callbacks within the window find the ref; after the window, the ref is gone. No fencing state, no matchKind discrimination, no content/reply-target guards needed.

What we'd love to keep

Your regression test cases are excellent — they precisely describe real race conditions (late callback with SAME/DIFFERENT content across boundary, fenced finalized_fallback, reply-target mismatch, etc.). We'd like to absorb those test scenarios regardless of which implementation path we take.

Suggested next step

Could you open a dedicated issue describing the specific race condition (late callback after invocation_created clears finalizedStreamRef)? The original #266 is closed, and having a focused accepted issue would let us track this properly. We're happy to triage it immediately.

Thanks again for the thorough work — this is clearly a well-understood problem, and we appreciate the contribution. 🐾

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 12, 2026

Thanks — I agree the protocol-level fix is the cleaner long-term direction.

I opened a dedicated issue to track that root cause and invariant here: #454.

The main reason this PR went down the client-side path was to stop the user-visible duplicate-bubble race without waiting on a backend contract change. Regardless of which implementation path we choose, I think the regression scenarios captured here are still valuable and should be preserved.

At this point I see two reasonable options:

I'm fine with either direction; the key thing is that we now have a focused issue that tracks the actual race condition separately from #266.

@zts212653
Copy link
Copy Markdown
Owner

Thanks for the clean follow-up and for opening #454 — that's exactly the right anchor.

Our decision: we'll pursue the protocol-level fix via #454 and won't merge this PR.

Reasoning:

  • Since we both agree the backend invariant (invocationId on every callback payload) is the cleaner long-term answer, merging 2400 lines of client-side coordination state into main as a temporary bridge doesn't pay for itself — especially when the heuristic surface has already shown it generates new edge cases during review.
  • The regression test scenarios you wrote here are genuinely valuable. We'll preserve those cases (same/different content across boundary, reply-target mismatch, callback-first ordering, background-thread variants) as red-light fixtures when we implement fix(api,ws): always propagate callback invocationId to avoid late-callback duplicate bubbles #454.

We've triaged #454 as bug + accepted. If you'd like to take a crack at the backend side as well, we're happy to review — the callback broadcast paths in callbacks.ts already carry invocationId internally, so the delta should be small.

Thanks again for the thorough analysis — the issue quality and willingness to pivot to the right fix layer is exactly the kind of contribution we value. 🐾

@mindfn
Copy link
Copy Markdown
Collaborator Author

mindfn commented Apr 12, 2026

Understood. I won't push the client-side mitigation on this branch any further.

I'll treat #410 as superseded by #454, and the regression scenarios from this PR can be carried over there as red-light fixtures for the protocol-level implementation.

Closing this PR so the tracking state matches the agreed direction.

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