refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@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

refactor(server): settled state is now server-authored, ending client drift - #5462

Closed
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic
Closed

refactor(server): settled state is now server-authored, ending client drift#5462
t3dotgg wants to merge 1 commit into
mainfrom
t3code/server-side-settled-logic

Conversation

@t3dotgg

@t3dotggt3dotgg commented Aug 6, 2026

Copy link
Copy Markdown
Member

"Settled" was split across the stack: the server stored a user override while every client re-derived the actual classification from an inactivity window, per-row PR state, and clock heuristics. The copies had drifted — mobile hardcoded the 3-day window web made configurable, sorted the settled shelf by a different key, and inverted the capability-gate default — so the same thread could be settled on one device and active on another.

Now the server is the single author of settled state and clients just read settledOverride:

  • A new ThreadAutoSettleReactor sweeps once a minute and dispatches the existing thread.settle command for threads that qualify: quiet past the inactivity window, or on a merged/closed PR. All existing decider invariants and the activity-driven auto-unsettle apply unchanged, so a raced sweep can never hide live work.
  • The auto-settle window moved from per-device client settings (localStorage) to ServerSettings.threadAutoSettleAfterDays — one value per environment, same shelf on every device.
  • An open PR still blocks inactivity settling. The sweep reads cached VCS status, and verifies cold checkouts with a cooldown-limited, background-policy-gated live lookup so it never stampedes the forge.
  • Auto-settles backdate settledAt to the thread's last activity, and both platforms now sort the settled shelf by settledAt — fixing the ordering drift.
  • Snooze wakes count as activity, so a woken thread gets a fresh window instead of settling the moment it wakes.
  • The settle-on-merge toggle (feat: allow disabling auto-settle on merge #5880) moved server-side with the rest of the policy: ServerSettings.threadAutoSettleOnMerge, applied by the sweep. Clients keep a small changeRequestAutoSettles helper for display only (Woke-pill suppression). Mobile's device-local copy of the toggle is removed — settle policy has no per-device knobs.
  • Deleted from clients: the whole effectiveSettled derivation (window/PR/clock inputs, the "serverAdjudicated" clock-skew hack), the per-row PR-state lift-up machinery on web and mobile, web's useNowMinute hook, and mobile's hardcoded window. effectiveSettled is now a plain override read with a blocked-work guard.

Old servers never emit the override, so their threads simply stay active — same graceful degradation as before, minus a capability check per row.

Built by Claude Fable 5 via Claude Code.


Note

High Risk
Changes core thread-list behavior and settlement timing across server and all clients; incorrect sweep or settledAt derivation could hide active work or settle threads users still care about.

Overview
Thread settlement is now server-authored so web, mobile, and desktop no longer disagree on whether a thread is settled.

A new ThreadAutoSettleReactor runs periodic sweeps using pure policy in autoSettle.ts: inactivity (threadAutoSettleAfterDays), merged/closed PR rules (threadAutoSettleOnMerge), and cooldown-limited VCS/PR verification via VcsStatusBroadcaster.peekStatus / refreshStatus. Qualifying threads get thread.settle dispatched server-side; the decider derives settledAt from last activity (including latestUserMessageAt on the read model) instead of settle time.

Clients stop re-deriving settled state: effectiveSettled is a settledOverride read plus local guards (live session, pending input, user message newer than settledAt). Removed are client-side inactivity/PR/clock partitioning, per-row PR state lift-up on lists, useNowMinute for settle, and mobile/desktop sidebarAutoSettle* / autoSettleOnMerge preferences. Auto-settle knobs move to server settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge), gated by a threadAutoSettle capability; settled shelf ordering aligns on settledAt across platforms.

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

Note

Move thread settled state from client-side heuristics to server-authored settlement

  • Introduces ThreadAutoSettleReactor on the server that periodically sweeps threads and dispatches settle commands based on inactivity windows and PR merge state read from ServerSettings.
  • effectiveSettled in threadSettled.ts is rewritten to only classify a thread as settled when the server has set settledOverride = 'settled' and no newer user message exists; all client-side inactivity/PR-state paths are removed.
  • Auto-settle settings (threadAutoSettleAfterDays, threadAutoSettleOnMerge) move from ClientSettings to ServerSettings; the web settings panel now reads/writes server-scoped fields and only renders when the server advertises the threadAutoSettle capability.
  • thread.settled events now stamp settledAt with the thread's last recorded activity time rather than the command dispatch time, affecting shelf ordering.
  • Mobile and web clients stop tracking per-row PR change request state and no longer pass changeRequestStateByKey, autoSettleOnMerge, or wall-clock now into thread list partitioning.
  • Risk: effectiveSettled no longer accepts any options object; all callers must be updated, and threads will not appear settled until the server reactor marks them.

Macroscope summarized 8b8bec7.

Summary by CodeRabbit

  • New Features

    • Added server-wide automatic thread settlement for inactive threads.
    • Added settings to enable, disable, and configure automatic settlement from 1–90 days.
    • Added support for environments to indicate automatic settlement availability.
  • Improvements

    • Thread settlement status is now consistently determined by server state across web and mobile.
    • Settled threads are sorted using their settlement time, with last update time as a fallback.
    • Snooze behavior and settlement safeguards remain supported.

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 46.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description check✅ PassedThe description clearly explains the server-authored settled state refactor, its motivation, implementation, risks, and UI impact.
Title check✅ PassedThe title clearly and concisely summarizes the primary change: settled state is now authored by the server.

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:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026

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

Two Effect service convention violations in the new ThreadAutoSettleReactor service. The rest of apps/server/src already follows the canonical single-module make + layer shape (e.g. vcs/VcsStatusBroadcaster.ts, background/BackgroundPolicy.ts), so the new service is the outlier here. Everything else in the diff (namespace subpath imports, dependency acquisition via yield* Foo, pure-config options, test-only Layer.succeed/Layer.mock seams, VcsStatusBroadcaster.peekStatus addition, contracts/settings moves) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/decider.ts Outdated
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/client-runtime/src/state/threadSettled.ts
Comment threadapps/server/src/orchestration/autoSettle.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
@macroscopeapp

macroscopeappBot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR fundamentally changes settled-state handling from client-derived to server-authored, introducing a new periodic reactor and schema changes. The architectural scope—new server infrastructure, capability flags, and settings migration—plus an unresolved High severity finding about shell schema compatibility warrant human review.

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

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from bfa8139 to c2b77f2CompareAugust 6, 2026 22:01

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts`:
- Around line 169-185: The inactivity-candidate flow around
resolveAutoSettleVerdict must treat a cached "open" result from
peekChangeRequestState as "unknown" so it reaches the existing verification
path. Preserve the cooldown, background-policy gate, and verifyBudget checks,
then use verifyChangeRequestState to refresh and settle when the PR is merged or
closed. Add a focused test covering peekStatus returning open and refreshStatus
returning merged or closed.
In `@apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts`:
- Around line 19-35: Move ThreadAutoSettleReactor and its layer implementation
into the canonical orchestration/ThreadAutoSettleReactor module, inline
ThreadAutoSettleReactorShape in Context.Service, and export the service type,
make, and layer members there. Update all consumers to import
ThreadAutoSettleReactor from the canonical module instead of the Services/ or
Layers/ modules, removing the obsolete split definitions.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 112-114: Update the AutoSettleDaysInput usage in BetaSettingsPanel
so updateServerSettings is not called for every valid keystroke; commit the
fully validated draft threshold only on blur or Enter, preserving the existing
threadAutoSettleAfterDays setting update once editing completes.
In `@packages/contracts/src/orchestration.ts`:
- Around line 596-600: Keep auto-settle backdating server-only: in
packages/contracts/src/orchestration.ts:596-600, remove settledAt from the
client-callable thread.settle contract or provide a separate server-only
auto-settle command; in apps/server/src/orchestration/decider.ts:500-502, derive
the timestamp exclusively from trusted server projection data; in
apps/server/src/orchestration/decider.settled.test.ts:111-130, update coverage
to exercise the trusted server path without accepting a caller-provided
timestamp.
In `@packages/contracts/src/settings.ts`:
- Around line 536-541: The default for threadAutoSettleAfterDays must preserve
clients that previously persisted sidebarAutoSettleAfterDays: null instead of
enabling auto-settlement with 3 days. Add a one-time migration that carries the
explicit null forward, or change the server default to a disabled-safe value,
and add coverage verifying the persisted null case remains disabled after
decoding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 732a9a94-b034-4f54-a494-b6616cb226ed

📥 Commits

Reviewing files that changed from the base of the PR and between 7251f1a and bfa8139.

📒 Files selected for processing (33)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/home/HomeScreen.tsx
  • apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/mobile/src/features/threads/threadListV2.test.ts
  • apps/mobile/src/features/threads/threadListV2.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationReactor.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.test.ts
  • apps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts
  • apps/server/src/orchestration/autoSettle.test.ts
  • apps/server/src/orchestration/autoSettle.ts
  • apps/server/src/orchestration/decider.settled.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/server.ts
  • apps/server/src/vcs/VcsStatusBroadcaster.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/settings/BetaSettingsPanel.tsx
  • apps/web/src/hooks/useNowMinute.ts
  • packages/client-runtime/src/state/threadSettled.test.ts
  • packages/client-runtime/src/state/threadSettled.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.test.ts
  • packages/contracts/src/settings.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/settings/DesktopClientSettings.test.ts
  • apps/mobile/src/features/threads/thread-list-v2-items.tsx
  • apps/web/src/hooks/useNowMinute.ts

Comment threadapps/server/src/orchestration/Layers/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/Services/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/web/src/components/settings/BetaSettingsPanel.tsx Outdated
Comment threadpackages/contracts/src/orchestration.ts Outdated
Comment threadpackages/contracts/src/settings.ts
Comment threadpackages/contracts/src/settings.ts
Comment threadapps/mobile/src/features/home/HomeScreen.tsx
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/decider.ts
@github-actionsgithub-actionsBot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 6, 2026
Comment threadapps/server/src/orchestration/decider.ts
Comment threadapps/server/src/orchestration/projector.ts
Comment threadapps/server/src/orchestration/projector.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 12b1dc6 to 671e346CompareAugust 7, 2026 06:20
@github-actions

github-actionsBot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire11.3 KiB11.4 KiB+17 B (+0.1%)15.1 KiB
CodexThread snapshot wire5.5 KiB5.5 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire5.9 KiB5.9 KiB+6 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded49.7 KiB49.7 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages16160 (0.0%)21
ClaudeTotal thread wire11.3 KiB11.3 KiB+5 B (+0.0%)15.1 KiB
ClaudeThread snapshot wire5.5 KiB5.5 KiB+15 B (+0.3%)7.3 KiB
ClaudeLive turn WebSocket wire5.9 KiB5.9 KiB−10 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded50.6 KiB50.6 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages16160 (0.0%)21

Baseline: 57a299a · PR result: 8b8bec7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.7 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 671e346 to 407c63eCompareAugust 7, 2026 08:40
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Comment threadapps/server/src/orchestration/autoSettle.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 2 times, most recently from 09f046c to 5199acaCompareAugust 7, 2026 10:16
return yield* updateCachedStatus(cwd, local, remote);
});

const peekStatus: VcsStatusBroadcaster["Service"]["peekStatus"] = Effect.fn(

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.

🟠 Highvcs/VcsStatusBroadcaster.ts:346

peekStatus merges independently cached local and remote halves without verifying they describe the same checkout. When refreshLocalStatusCore updates only cached.local after a branch switch, the stale cached.remote from the previous branch remains in the cache. mergeGitStatusParts pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in peekStatus.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/vcs/VcsStatusBroadcaster.ts around line 346:
`peekStatus` merges independently cached local and remote halves without verifying they describe the same checkout. When `refreshLocalStatusCore` updates only `cached.local` after a branch switch, the stale `cached.remote` from the previous branch remains in the cache. `mergeGitStatusParts` pairs the new branch name from local with the old branch's PR data from remote, so the auto-settle reactor can see a new branch alongside a merged/closed PR from the prior branch and incorrectly settle an active thread. Consider invalidating the remote half when the local checkout identity changes, or adding a coherence check before merging in `peekStatus`.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in 72f1b2f at the consumer: peekChangeRequestState now requires the cached PR's headRef to equal the thread's branch, so a stale local/remote pairing after a branch switch (new refName + previous branch's PR) maps to "unknown" and live-verifies instead of settling. I kept the fix in the sweep rather than changing peekStatus/cache invalidation because the streaming path already tolerates the transient mismatch (rows re-render when the remote half refreshes) and the sweep is the only consumer that acts irreversibly on the merged view.

@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch 3 times, most recently from 2693eda to ffb4e20CompareAugust 8, 2026 11:17

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

One convention finding in the new server-side auto-settle code: the test harness references the service interface through Parameters<typeof Tag["of"]>[0] instead of the canonical Tag["Service"]. The reactor module itself now follows the canonical layout (single orchestration/ThreadAutoSettleReactor.ts, inline interface in Context.Service, real make, layer, all dependencies acquired via yield*), so the earlier layout/naming findings are resolved.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.test.ts Outdated
Comment threadapps/server/src/orchestration/projector.ts
Comment threadpackages/contracts/src/settings.ts
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from ffb4e20 to 9763b02CompareAugust 8, 2026 11:27
Comment threadapps/server/src/orchestration/ThreadAutoSettleReactor.ts Outdated
Settled classification used to be re-derived per client (inactivity window,
PR state, clock hacks), with real drift between web and mobile. The server
is now the single author of settled state: a ThreadAutoSettleReactor sweep
dispatches thread.settle for quiet threads and merged/closed PRs, the
auto-settle window moved to ServerSettings, and clients just read
settledOverride.
Includes the review-hardening rounds: settledAt derived in the decider from
read-model activity (unforgeable, restart-safe via a projected
latestUserMessageAt stamp, revert-consistent, clock-skew clamped), only
live-confirmed PR states act as settle authority (cached open/closed/no-PR
re-verify with cooldown), fail-safe settings reads, and capability-gated
settings UI with blur-committed input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotggforce-pushed the t3code/server-side-settled-logic branch from 5d03b32 to 8b8bec7CompareAugust 15, 2026 01:06

@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 8b8bec7. Configure here.

// array: the engine's command read model boots threads with no message
// bodies, and the decider needs this stamp for settle invariants and the
// settledAt derivation. Optional for pre-existing payloads.
latestUserMessageAt: Schema.optional(Schema.NullOr(IsoDateTime)),

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.

Shell stamp breaks old servers

High Severity

latestUserMessageAt is required on OrchestrationThreadShell, unlike neighboring lifecycle fields that use Schema.optional for old-server and cached-snapshot interop. The full OrchestrationThread marks the same field optional. New clients decoding shells from older servers or pre-upgrade persisted snapshots missing the key can fail the shell stream or cache hydrate, which breaks the thread list despite the PR’s graceful-degradation goal for older servers.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 8b8bec7. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive — the required latestUserMessageAt on OrchestrationThreadShell (L464) is pre-existing and byte-identical to main (main L459); this PR does not touch it, so old-server interop is unchanged. The field this PR ADDS is on the full OrchestrationThread detail model (L404), and that one is Schema.optional for exactly the interop reason you describe.

@t3dotgg

Copy link
Copy Markdown
MemberAuthor

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.

This branch covers the same server-owned settlement problem as #5402. We are keeping that PR as the active implementation, so this branch does not need a second review path.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

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

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@t3dotgg