Skip to content

fix(gui): stop the fork dialog waiting forever, and block a doomed submit - #1230

Merged
hdkshingala merged 2 commits into
mainfrom
traycer/lucid-ocelot-cold-review
Aug 16, 2026
Merged

hdkshingala merged 2 commits into
mainfrom
traycer/lucid-ocelot-cold-review

Conversation

@hdkshingala

Copy link
Copy Markdown
Member

Client half of a cold review of the cross-host chat fork feature. The host half is internal #5014; this PR carries the protocol field it answers with and the two client behaviours that read it.

Follows #1227.

definitive — a way to say "never"

epic.chatPublicationState gains definitive, set when waiting cannot change the answer. Three host states needed it and none could be expressed before:

Reason State
chat-deleted the source chat is a tombstone on its own host
lineage-superseded the chat lost an arbitrated fork; its publications land under a different cloud identity
backup-halted publication stopped for a reason the sweep does not retry within the process lifetime

All three previously reported byte-identically to "the sweep has not reached it yet", so the condition poll re-asked every 30s forever while the dialog said "It backs up automatically — try again shortly." and kept submit blocked. The poll now terminates, and each reason gets copy that is true for it.

Two cases that look symmetric and are not:

  • an unrecognised reason is treated as terminal-but-unexplained, so a newer host cannot reintroduce the infinite wait;
  • an absent one is not. The field was added to an unreleased method in place, so a host built before it takes the un-parsed same-version path and the key genuinely arrives missing. Reading that as a reason would mark every older host permanently halted — the same hang, inverted.

Submit no longer stays enabled on an uncovered boundary

verdictAllowsSubmit previously allowed submit while boundaryCovered === false, documented as safe because "Layer 2 — the host's typed refusal — is the authority".

That premise was false. The host's check was presence-only, so a boundary turn published mid-stream is present in the head at its partial version: the fork returned 200 OK and seeded a silently truncated turn.

The host-side currency check lands alongside this in #5014. Both halves are wanted rather than either alone — the publication hook reports UNKNOWN during background refetches, so the gate blinks open for one RTT per poll cycle, and the host refusal is what makes that blink a wasted round trip instead of a hole.

Preserved deliberately

  • boundaryCovered: null is still never collapsed to falsenull means the question was not answered, and blocking on it would refuse a fork on a question nobody asked.
  • The permissive unknown path for hosts that predate the method stays permissive; an unsupported or unreachable host does not start blocking.

Tests

84 tests across three suites, including four mutation probes — disabling the definitive-first check, treating undefined as a reason, disabling the invalidates-published-head gate, and reverting the submit block — each confirmed caught, with production restored and diff-verified byte-identical afterward.

🤖 Generated with Claude Code

…bmit

Two client-side halves of a cross-host fork review, plus the protocol field
they read.

`epic.chatPublicationState` gains `definitive`, set when waiting CANNOT change
the answer. Three host states needed it and none could be expressed before: a
deleted source chat, a chat whose lineage lost an arbitrated fork, and a
publication halted for a reason the sweep does not retry within the process
lifetime. All three reported byte-identically to "the sweep has not reached it
yet", so the condition poll re-asked every 30s forever while the dialog told
the user "it backs up automatically - try again shortly" and kept submit
blocked. The poll now terminates and each reason gets copy that is true for it.

An UNRECOGNISED reason is treated as terminal-but-unexplained rather than as
null, so a newer host cannot reintroduce the infinite wait. An ABSENT one is
NOT: the field was added to an unreleased method in place, so a host built
before it takes the un-parsed same-version path and the key genuinely arrives
missing - reading that as a reason would mark every older host permanently
halted, which is the same hang inverted.

Separately, submit no longer stays enabled while `boundaryCovered === false`.
That behaviour was documented as safe because "Layer 2 - the host's typed
refusal - is the authority", and that premise was false: the host's check was
presence-only, so a boundary published mid-stream is present at its partial
version, the fork returns 200 OK, and the user gets a silently truncated turn.
The host-side currency check lands alongside this; both halves are wanted,
because the hook reports UNKNOWN during background refetches and the gate
therefore blinks open for one RTT per poll - the host refusal is what makes
that blink harmless rather than a hole.

`boundaryCovered: null` is still never collapsed to `false`, and the permissive
unknown path for hosts that predate the method stays permissive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Hardik Shingala <hardik@traycer.ai>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 36 minutes

Limit details: You’ve used all 1 included review currently available under your plan. You completed 122 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a5af72cd-3644-4c14-b32e-a4bff6a06552

📥 Commits

Reviewing files that changed from the base of the PR and between 2ce226d and f320388.

📒 Files selected for processing (2)
  • clients/gui-app/src/components/chat/__tests__/chat-fork-dialog-cross-host.test.tsx
  • protocol/src/host/epic/__tests__/chat-publication-state-compat.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4c4e74eb-c789-4af4-b338-b0f75485f0c0

📥 Commits

Reviewing files that changed from the base of the PR and between ae62f0f and 2ce226d.

📒 Files selected for processing (9)
  • clients/gui-app/src/components/chat/__tests__/chat-fork-target.test.ts
  • clients/gui-app/src/components/chat/chat-fork-dialog.tsx
  • clients/gui-app/src/components/chat/chat-fork-target.ts
  • clients/gui-app/src/hooks/chats/use-chat-publication-state-query.ts
  • clients/gui-app/src/lib/chats/__tests__/chat-publication-definitive.test.ts
  • clients/gui-app/src/lib/chats/chat-publication-definitive.ts
  • clients/gui-app/src/lib/host-rpc-policy/__tests__/host-method-policy-table.test.ts
  • clients/gui-app/src/lib/host-rpc-policy/host-method-policy-table.ts
  • protocol/src/host/epic/unary-schemas.ts

Included review availability: 0 reviews are currently available. Based on recent review activity, included reviews refill at 1 per hour.


Summary by CodeRabbit

  • New Features
    • Added clear notices when chats are deleted, superseded, or unavailable for backup.
    • Added reason-specific status handling for cross-host chat forks.
  • Bug Fixes
    • Prevented fork submission while publication status is still synchronizing.
    • Preserved host-row access during temporary synchronization issues.
    • Correctly blocks unavailable chats while allowing valid, covered chats to proceed.
  • Tests
    • Expanded coverage for publication states, fork eligibility, status precedence, and polling behavior.

Walkthrough

The PR adds definitive chat publication outcomes to the protocol, shared parsing, polling policy, and chat-fork eligibility. Definitive failures make rows unreachable; transient syncing blocks submission while remaining selectable.

Changes

Chat publication and forking

Layer / File(s) Summary
Publication response and definitive reason contract
protocol/src/host/epic/unary-schemas.ts, clients/gui-app/src/lib/chats/*
The publication response adds an optional definitive reason. Shared helpers classify known and unknown terminal reasons and identify published-head invalidation.
Publication polling termination
clients/gui-app/src/lib/host-rpc-policy/*
The polling policy stops after any definitive reason and continues for absent or null reasons when publication remains unresolved.
Fork publication verdicts and submission gates
clients/gui-app/src/components/chat/chat-fork-target.ts, clients/gui-app/src/components/chat/__tests__/chat-fork-target.test.ts
Fork targets now distinguish syncing from durable unavailability. Definitive notices take precedence, and only the allowed verdict permits submission.
Dialog and query eligibility contracts
clients/gui-app/src/components/chat/chat-fork-dialog.tsx, clients/gui-app/src/hooks/chats/use-chat-publication-state-query.ts
The dialog uses the combined host and publication verdict. Durable failures disable remote rows, while transient syncing keeps rows selectable but blocks submission.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 2ce22

The PR is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant PublicationQuery
  participant ForkTarget
  participant ForkDialog
  Host->>PublicationQuery: return publication state and definitive reason
  PublicationQuery->>ForkTarget: classify publication status
  ForkTarget->>ForkDialog: provide verdict and notice
  ForkDialog->>ForkDialog: enable submission only for allowed
Loading

Possibly related PRs

Suggested labels: protocol-compat-override

Suggested reviewers: tanveergill

Poem

I’m a rabbit guarding the forked-chat trail,
Terminal reasons now ring their bell.
Syncing waits, but rows stay near,
Deleted paths disappear clear.
Only allowed gets a carrot hop! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary changes: terminating indefinite fork-dialog polling and blocking invalid submissions.
Description check ✅ Passed The description directly explains the definitive publication state, compatibility behavior, submit gating, and associated tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch traycer/lucid-ocelot-cold-review

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 16, 2026
…usals

Three CI jobs on #1230 failed on tests that still describe the behaviour
this branch deliberately changed.

`definitive` is `.default(null)`, so a wire payload that omits it - which
is exactly what a host predating the field sends - no longer parses back
to itself. Both round-trips now assert the defaulted shape and say why
the two sides differ. The field also had no coverage at all, so this adds
it: each reason round-trips, an explicit null stays null, and an
unrecognised reason is rejected rather than silently read as "no terminal
cause", which would put the infinite wait back.

The two `boundarySyncing` dialog tests were stale in OPPOSITE directions,
and updating them the same way would have hidden the rule. The one that
selects a remote host is cross-host, so it now asserts the submit is
BLOCKED - the reversal this branch made after finding the host's coverage
check is presence-only. The first-paint one selects nothing, so it is a
same-host fork that `chatForkTargetVerdict` short-circuits to `allowed`
before it ever reads publication state; its button correctly stays
enabled. The pair is what pins that exemption, so both now say so.

Signed-off-by: Hardik Shingala <hardik@traycer.ai>
@hdkshingala

Copy link
Copy Markdown
Member Author

@coderabbitai approve

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@hdkshingala
hdkshingala merged commit 0b61c2d into main Aug 16, 2026
20 checks passed
@hdkshingala
hdkshingala deleted the traycer/lucid-ocelot-cold-review branch August 16, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant