Skip to content

Extract coordinator state mechanisms#800

Merged
FuJacob merged 1 commit into
mainfrom
codex/coordinator-mechanisms
Jul 18, 2026
Merged

Extract coordinator state mechanisms#800
FuJacob merged 1 commit into
mainfrom
codex/coordinator-mechanisms

Conversation

@FuJacob

@FuJacob FuJacob commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Extract the coordinator's two clearest independent state mechanisms: streamed-partial coalescing/monotonic rendering and the short post-exhaustion Tab-ownership window. The coordinator still owns generation, timers, input interception, Accessibility reads, session mutation, logging, and presentation; the new values own only deterministic state transitions.

This is the final PR in the repository-organization stack and is based on #799.

Validation

  • swiftlint lint --strict — 0 violations.
  • xcodegen generate — succeeded and the generated project is committed.
  • xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build-for-testing -derivedDataPath build/DerivedDataTEST BUILD SUCCEEDED.
  • After ad-hoc re-signing the local test host to avoid the repository's Team-ID mismatch, 26 focused streaming-policy, extracted-state, and coordinator-acceptance tests passed with 0 failures.
  • The 87 settings domain/model/store tests from the preceding stacked PR also passed with 0 failures on this branch.
  • A complete test-target run was attempted and made progress with no observed failures, but was interrupted after ScreenshotContextGeneratorTests blocked while enumerating the developer debug-screenshot directory on Desktop. That filesystem/debug-artifact path is unchanged by this PR.

Linked issues

None.

Risk / rollout notes

No coordinator side effects moved into the extracted values. Existing coordinator acceptance tests still cover input interception and insertion, while focused tests now cover backstop invalidation, bounded queued acceptance, latest-wins stream coalescing, scheduled-drain preservation, teardown, and monotonic rendering.

Greptile Summary

This PR extracts two independent coordinator state mechanisms — streamed-partial coalescing/monotonic rendering (SuggestionStreamingState) and the bounded post-exhaustion Tab-ownership window (PostExhaustionAcceptanceState) — into dedicated value types with pure state transitions, leaving all side effects (timers, input interception, overlay presentation) in the coordinator.

  • SuggestionStreamingState encapsulates three invariants: latest-wins coalescing, at-most-one scheduled drain per runloop turn, and monotonic text rendering. beginGeneration and clearSession intentionally share the same implementation to preserve an already-dispatched-but-uncancellable drain callback.
  • PostExhaustionAcceptanceState uses a monotonic generation token to ensure stale backstop timers always fire as no-ops, with consumeQueuedAccept() atomically reading and clearing the window to avoid TOCTOU between the queued-flag check and the clear.
  • All coordinator call-sites in +Acceptance.swift and +Prediction.swift are mechanical substitutions with no observable behavioral change, and focused unit tests cover every state-transition rule for both new types.

Confidence Score: 4/5

Clean, behavior-preserving refactoring with no side effects moved out of the coordinator; safe to merge.

The refactoring is correct end-to-end: generation token, coalescing, drain scheduling, and atomic consume all match the replaced code. The only observation is that SuggestionStreamingState documents itself as main-actor bookkeeping but carries no @MainActor annotation, leaving the concurrency contract implicit rather than enforced at compile time.

SuggestionStreamingState.swift — would benefit from an explicit @MainActor annotation to match its own documentation.

Important Files Changed

Filename Overview
Cotabby/Support/Suggestion/SuggestionStreamingState.swift New value type encapsulating latest-wins coalescing, single-drain-per-runloop scheduling, and monotonic rendering invariants. beginGeneration and clearSession intentionally share identical implementations to preserve a scheduled-but-uncancellable drain callback; the design is sound and documented. Missing @MainActor annotation despite the doc comment saying "Main-actor bookkeeping".
Cotabby/Support/Suggestion/PostExhaustionAcceptanceState.swift New pure state machine for the bounded Tab-ownership window. Generation token correctly invalidates stale backstop timers on both arm() and clear(). needsRelease using `isArmed
Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift Coordinator call-sites mechanically swapped to the extracted state API with no behavioral change. queueAcceptIfArmed() return value correctly gates the log call; consumeQueuedAccept() atomically clears and returns the queued flag, matching the old two-step pattern exactly.
Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift Streaming call-sites delegate to SuggestionStreamingState. beginGeneration() preserves isDrainScheduled (matching the old pattern), enqueue return value controls drain scheduling, and drain() atomically clears the flag and takes the partial — all equivalent to the replaced code.
Cotabby/App/Coordinators/SuggestionCoordinator.swift Three inline properties (pendingStreamPartial, isStreamDrainScheduled, streamRenderedText) and four post-exhaustion properties replaced by two value-type instances. Coordinator retains ownership of all side effects.
CotabbyTests/Support/Suggestion/SuggestionStreamingStateTests.swift Four focused tests cover coalescing, drain preservation across generation boundaries, session-teardown self-heal, and monotonic rendering. Coverage is thorough for the new type's public surface.
CotabbyTests/Support/Suggestion/PostExhaustionAcceptanceStateTests.swift Four tests cover arm-invalidates-previous-backstop, queue-only-while-armed, clear-invalidates-captured-generation, and atomic consume. All transition rules are exercised.
CotabbyTests/App/Suggestion/SuggestionCoordinatorAcceptanceTests.swift Updated to access state through the extracted type's public interface. Direct property assignments replaced by arm() / queueAcceptIfArmed() calls which preserve the same armed+queued state for the flush test.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Engine
    participant Coordinator as SuggestionCoordinator
    participant SSS as SuggestionStreamingState
    participant PEAS as PostExhaustionAcceptanceState
    participant InputMonitor

    Note over Coordinator,SSS: New generation starts
    Coordinator->>SSS: beginGeneration()
    Note right of SSS: clears renderedText, pendingPartial<br/>preserves isDrainScheduled

    loop Token-rate partial callbacks
        Engine->>Coordinator: onPartial(result, workID)
        Coordinator->>SSS: enqueue(result, workID)
        alt drain not yet scheduled
            SSS-->>Coordinator: true (schedule drain)
            Coordinator->>Coordinator: DispatchQueue.main.async drainStreamedPartial
        else drain already scheduled
            SSS-->>Coordinator: false (coalesce, skip)
        end
    end

    Note over Coordinator,SSS: Runloop drain fires
    Coordinator->>SSS: drain()
    SSS-->>Coordinator: PendingPartial? (clears flag atomically)
    Coordinator->>SSS: canRender(partial.text)
    SSS-->>Coordinator: Bool (monotonic check via StreamedGhostTextPolicy)
    Coordinator->>SSS: recordRendered(partial.text)

    Note over Coordinator,PEAS: Final-chunk accept — exhausted session
    Coordinator->>PEAS: arm()
    PEAS-->>Coordinator: generation (UInt64)
    Coordinator->>InputMonitor: setAcceptInterceptionActive(true)
    Coordinator->>Coordinator: asyncAfter(postExhaustionWindow)

    alt Fast-follow Tab arrives during regen window
        InputMonitor->>Coordinator: shouldConsumeAcceptKey?
        Coordinator->>PEAS: isArmed (read)
        PEAS-->>Coordinator: true
        Coordinator->>PEAS: queueAcceptIfArmed()
    end

    Note over Coordinator,PEAS: Regen completes — new suggestion visible
    Coordinator->>PEAS: consumeQueuedAccept()
    PEAS-->>Coordinator: shouldAccept Bool (clears window atomically)

    Note over Coordinator,PEAS: Backstop timer fires (if not superseded)
    Coordinator->>PEAS: ownsBackstop(generation)
    alt generation matches
        PEAS-->>Coordinator: true → releasePostExhaustionAcceptanceWindow
    else stale timer
        PEAS-->>Coordinator: false → no-op
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Engine
    participant Coordinator as SuggestionCoordinator
    participant SSS as SuggestionStreamingState
    participant PEAS as PostExhaustionAcceptanceState
    participant InputMonitor

    Note over Coordinator,SSS: New generation starts
    Coordinator->>SSS: beginGeneration()
    Note right of SSS: clears renderedText, pendingPartial<br/>preserves isDrainScheduled

    loop Token-rate partial callbacks
        Engine->>Coordinator: onPartial(result, workID)
        Coordinator->>SSS: enqueue(result, workID)
        alt drain not yet scheduled
            SSS-->>Coordinator: true (schedule drain)
            Coordinator->>Coordinator: DispatchQueue.main.async drainStreamedPartial
        else drain already scheduled
            SSS-->>Coordinator: false (coalesce, skip)
        end
    end

    Note over Coordinator,SSS: Runloop drain fires
    Coordinator->>SSS: drain()
    SSS-->>Coordinator: PendingPartial? (clears flag atomically)
    Coordinator->>SSS: canRender(partial.text)
    SSS-->>Coordinator: Bool (monotonic check via StreamedGhostTextPolicy)
    Coordinator->>SSS: recordRendered(partial.text)

    Note over Coordinator,PEAS: Final-chunk accept — exhausted session
    Coordinator->>PEAS: arm()
    PEAS-->>Coordinator: generation (UInt64)
    Coordinator->>InputMonitor: setAcceptInterceptionActive(true)
    Coordinator->>Coordinator: asyncAfter(postExhaustionWindow)

    alt Fast-follow Tab arrives during regen window
        InputMonitor->>Coordinator: shouldConsumeAcceptKey?
        Coordinator->>PEAS: isArmed (read)
        PEAS-->>Coordinator: true
        Coordinator->>PEAS: queueAcceptIfArmed()
    end

    Note over Coordinator,PEAS: Regen completes — new suggestion visible
    Coordinator->>PEAS: consumeQueuedAccept()
    PEAS-->>Coordinator: shouldAccept Bool (clears window atomically)

    Note over Coordinator,PEAS: Backstop timer fires (if not superseded)
    Coordinator->>PEAS: ownsBackstop(generation)
    alt generation matches
        PEAS-->>Coordinator: true → releasePostExhaustionAcceptanceWindow
    else stale timer
        PEAS-->>Coordinator: false → no-op
    end
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "refactor: extract coordinator state mech..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@FuJacob
FuJacob force-pushed the codex/settings-domains branch from 7a0bad7 to 9c15593 Compare July 18, 2026 03:06
@FuJacob
FuJacob deleted the branch main July 18, 2026 03:10
@FuJacob FuJacob closed this Jul 18, 2026
@FuJacob FuJacob reopened this Jul 18, 2026
@FuJacob
FuJacob changed the base branch from codex/settings-domains to main July 18, 2026 03:10
@FuJacob
FuJacob force-pushed the codex/coordinator-mechanisms branch from d68eeaa to 7b56688 Compare July 18, 2026 03:10
@FuJacob
FuJacob marked this pull request as ready for review July 18, 2026 03:13
@FuJacob
FuJacob merged commit ac67e30 into main Jul 18, 2026
4 checks passed
@FuJacob
FuJacob deleted the codex/coordinator-mechanisms branch July 18, 2026 03:13
/// latest-wins, at most one drain is scheduled per runloop turn, and visible text may only grow
/// monotonically. It does not schedule work or render UI; the coordinator remains responsible for
/// those side effects.
struct SuggestionStreamingState {

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.

P2 Missing @MainActor annotation

The doc comment opens with "Main-actor bookkeeping" but the struct itself carries no @MainActor isolation. The coordinator's own main-actor isolation provides the protection in practice, but an explicit annotation would make this a compile-time guarantee rather than an implicit contract — and would prevent a future caller from accidentally constructing and mutating a SuggestionStreamingState off the main actor. PostExhaustionAcceptanceState has only primitive fields so the omission there is less material, but SuggestionStreamingState stores a SuggestionResult value.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant