fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(web): reconcile client state when a settle does not land - #4593

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile
Closed

fix(web): reconcile client state when a settle does not land#4593
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/settle-state-reconcile

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What Changed

Settle can now recover a client whose view has drifted from the server, instead of requiring a client restart.

  • New resync-requestedConnectionWakeup. Snapshot-backed subscriptions (shell, threads) already reconcile on a foreground wake — reload the HTTP snapshot, resume the stream from its sequence — but nothing could ask for that on demand. They now resubscribe on this reason too, via a shared wakeupResubscribes predicate.
  • New ResyncRequests emitter in client-runtime, merged into the web app's wakeups layer.
  • settleThread requests a reconcile when it observes a disagreement:
    • when the local canSettle guard blocks the send, and
    • when a settle the server accepted never lands in the local view (checked at 150/500/1200 ms, then escalated).
  • Requests are coalesced behind a 5 s window, so bulk settle asks once rather than once per thread.

Why

Settling acts on the client's local view, and when that view is stale the action becomes unrecoverable in both directions:

  • canSettle (threadSettled.ts) refuses to send while the client's copy of session.status is starting | running, even if the server has since moved the session to ready.
  • A settle the server accepts against an already-settled thread is a deliberate idempotent no-op — it re-emits thread.settled with the original settledAt and the existing updatedAt (decider.ts) so bulk-settle and double-click stay quiet. If the local view never applies the resulting thread-upserted, every subsequent click succeeds while changing nothing visible.

Either way the control reads as dead and the thread cannot be moved to settled at all. The only known fix was restarting the client — which works precisely because it reloads the snapshot and resubscribes. This makes that same reconciliation reachable in place.

The server side already publishes what a client needs to converge: threadUpsertOrRemove re-reads the current projected row and emits thread-upserted after any thread event, including the idempotent re-emit. The gap is that nothing on the client notices when its view has stopped tracking that.

Reported in #4589.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — no visual change; this only affects when a reconcile is requested
  • I included a video for animation/interaction changes — n/a

Validation (scoped per AGENTS.md, which reserves repo-wide vp check / vp run typecheck for CI):

vitest run apps/web/src/lib/ packages/client-runtime/src/state/ # 429 passed
tsgo --noEmit # apps/web, packages/client-runtime — clean
vp lint --report-unused-disable-directives # no findings in changed files

Note on verification honesty: the underlying desync has no deterministic repro yet, so the new escalation path is covered by unit tests rather than an end-to-end reproduction. The happy path is unchanged — the convergence check reads once and exits.


Note

Medium Risk
Touches live subscription reconciliation and thread lifecycle UX; behavior is guarded by throttling and unit tests but the underlying desync has no deterministic E2E repro.

Overview
Fixes settle appearing broken when the local thread view is stale: the server may accept the command (or the client may block on canSettle) while the UI never updates until a restart.

Adds an on-demand resync-requested connection wakeup (via ResyncRequests in client-runtime, merged into the web wakeups layer). Shell and thread snapshot subscriptions now resubscribe on that reason as well as foreground activation, using shared wakeupResubscribes.

settleThread requests a coalesced reconcile (5s trailing-edge throttle in resyncRequests.ts) when the local canSettle guard blocks, and after a successful settle runs confirmSettleConverged polling; if settled state still does not appear, it escalates to another resync.

Reviewed by Cursor Bugbot for commit 5fc3aef. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Reconcile client state when a thread settle does not land locally

  • Adds a resync-requested wakeup type to ConnectionWakeup and wires it into the wakeups layer via a new ResyncRequests.resyncRequestStream in platform.ts.
  • Adds settleConvergence.ts, which polls the local thread shell after a successful settle and calls requestResync if the settled state never converges.
  • Adds resyncRequests.ts, a trailing-edge coalescing wrapper that fires at most one immediate and one trailing resync per 5-second window.
  • Updates settleThread in useThreadActions.ts to request a resync when a local canSettle check fails, and to run convergence polling after a successful mutation.
  • foregroundResubscriptions in shell.ts and threads.ts now resubscribe on resync-requested in addition to application-active.

Macroscope summarized 5fc3aef.

@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aafa55e-231f-4def-b5dd-92cf8e16bd20

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
Comment threadapps/web/src/lib/resyncRequests.ts Outdated
Comment threadapps/web/src/lib/resyncRequests.ts
@macroscopeapp

macroscopeappBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new client-side state reconciliation logic including a new wakeup type, throttled resync requests, and convergence polling after settle operations. While well-tested, these changes significantly affect runtime behavior around client/server state synchronization and warrant human review.

You can customize Macroscope's approvability policy. Learn more.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95aa505. Configure here.

Comment threadapps/web/src/lib/resyncRequests.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 26, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 6980a8c to 86bcba0CompareJuly 27, 2026 11:15
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from 86bcba0 to c4d02afCompareJuly 28, 2026 02:08
colonelpanic8and others added 3 commits July 27, 2026 19:47
Settling a thread acts on the client's local view. When that view has
drifted from the server the action becomes unrecoverable: `canSettle`
refuses to send against a stale `running` session, and a settle the server
accepts is a deliberate idempotent no-op that changes nothing the client
can see. Either way the control looks dead, and the only known fix is
restarting the client.
Snapshot-backed subscriptions already know how to reconcile — they reload
the HTTP snapshot and resume the stream from its sequence on a foreground
wake. Nothing could ask for that on demand. Add a `resync-requested`
wakeup so the same reconciliation is reachable in place, and have the
settle action request it when it observes a disagreement: when the local
guard blocks, and when an accepted settle never lands in the local view.
The convergence check polls only after an explicit user action and only
until the view agrees, so the healthy path costs one read. Requests are
coalesced, since one reconcile covers the whole environment and bulk
settle would otherwise ask once per thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found two ways the coalescing window could suppress the very
reconcile this change exists to perform.
Leading-edge-only coalescing dropped any request arriving within the
window. But a request exists because something observed the client
disagreeing with the server, so dropping it leaves that disagreement
standing until the user happens to act again — a settle blocked by a
stale guard, or another thread's earlier resync, could strand a later
accepted settle. Suppressed requests now collapse into one reconcile at
the end of the window instead of vanishing.
A backward clock jump also wedged the throttle shut: a negative elapsed
compares as inside the window, so every reconcile was suppressed until
wall time caught back up to the old timestamp. Treat it as a fresh
window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An immediate reconcile — an open window, or a backward clock jump — left
any pending trailing timer scheduled, so one logical request could produce
two reconciles and defeat the coalescing.
Track whether a request is still outstanding rather than cancelling the
timer. The obsolete timer becomes a no-op, while a request raised after
the superseding reconcile still gets its own trailing fire instead of
being dropped along with the obsolete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/settle-state-reconcile branch from c4d02af to 5fc3aefCompareJuly 28, 2026 02:51
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Closing the settle-specific polling workaround. It adds another recovery loop and global resync signal around one client action. The underlying subscription failure should be repaired in shared recovery. The related recovery PR is still open, so this closure does not mean the desynchronization bug is already fixed. Related work: #4602.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed. If GitHub does not let you reopen it, leave a comment here and we'll take another look.

@t3dotggt3dotgg closed this Aug 27, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg