Skip to content

fix(clients): derive the steer-pending marker from thread state - #186

Merged
pandec merged 4 commits into
devfrom
feat/steer-pending-server-derived
Sep 9, 2026
Merged

pandec merged 4 commits into
devfrom
feat/steer-pending-server-derived

Conversation

@pandec

@pandec pandec commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Problem

The "Waiting for the agent to pick this up" marker was tracked in an in-memory store that each thread view leased on mount and purged on unmount. Navigating to another thread and back lost the marker, and it never showed on another device or after a reload. A steer that waits long enough to matter is exactly the one you walk away from, so in practice the marker was almost never seen.

Fix

Derive the marker from thread state. A user message with a null turn id, created after the running turn was requested and after the last main-agent tool start or assistant message, is an unread steer. Both timestamps are server-stamped (the server replaces the client's createdAt on receipt, adapters stamp activities with the server clock), so no client clock is involved.

  • packages/client-runtime: unreadSteerMessageIds(snapshot) replaces the dispatch store, lease refcount, and watermark record.
  • Web and mobile: the hook takes the snapshot only and keeps just the 1.5s reveal delay as local state. The outbox drain no longer notes dispatches.
  • Removed the fork-only readThreadDetail helper in web, which had no other caller.
  • README and docs/user/message-queueing.md updated to say the marker follows the conversation, not the device.

Review follow-up (second commit)

The marker resolves against the running turn's newest main-agent tool start. A long subagent fan-out can push that row past the 500-activity window on both the server snapshot and the client's live retention, and every steer newer than the turn would then come back as unread. That row is now pinned the same way the latest context-window update already is: the server's pinned-activity CTE carries it into detail reads while the turn is running, and the client reducer keeps it through the cap. Plus a null-byte key separator, a sync pointer between the two identical hooks, and two more derivation tests.

Two review findings were considered and not changed:

  • Output after a steer clearing the marker does not prove the provider read it (a steer sent while the model is already mid-request clears on that request's first tool start). This is the same imprecision the previous design had, documented in the module comment, and fixing it needs a provider-side drain boundary that does not exist today.
  • Imported sessions keep their native timestamps. A source clock ahead of the server could mark an imported user message as pending, but only while a turn is running on that thread, and only for messages newer than the running turn's request time.

Verification

  • vp test run on the steer-pending, thread-reducer, and mobile outbox-drain tests, and the server ProjectionSnapshotQuery tests: all pass.
  • Typecheck clean for client-runtime, web, mobile, server. Lint and format clean on touched files.
  • Ran the real rows for a thread whose steer sat unread for two hours through the new function; it returned the steer's message id while the parent was blocked and an empty list once the parent's next tool start landed.

Claude Fable 5.1 via Claude Code, with Sol (gpt-5.6) and Opus review passes.

Summary by CodeRabbit

  • New Features
    • Steer-pending indicators now follow the conversation across devices and survive reloads.
    • Indicators remain visible when returning to a thread until the main agent progresses.
  • Bug Fixes
    • Pending steer indicators now clear promptly when the steer is processed, interrupted, or the turn ends.
    • Improved activity retention keeps relevant main-agent progress visible during long-running turns and subagent activity.
  • Documentation
    • Updated message queueing guidance to reflect persistent, conversation-based steer indicators.

The "Waiting for the agent to pick this up" marker lived in an in-memory
store that the thread view purged on unmount, so navigating away and back
lost it, and it never showed on another device or after a reload. A steer
that waits long enough to matter is exactly the one you walk away from.

Derive it instead: a user message with a null turn id, created after the
running turn was requested and after the last main-agent tool start or
assistant message, is an unread steer. Both timestamps are server-stamped
(the server replaces the client's createdAt on receipt), so no client
clock is involved. The outbox drain no longer records dispatches, and the
lease/refcount store is gone. The reveal delay stays as the only local
state.
…ity cap

Review follow-ups for the derived steer-pending marker.

The marker resolves against the running turn's newest main-agent tool
start. A long subagent fan-out can push that row past the 500-activity
window on both the server snapshot and the client's live retention, at
which point every steer newer than the turn would come back as unread.
Pin that one row the same way the latest context-window update is
pinned: the server's pinned-activity CTE carries it into detail reads
while the turn is running, and the client reducer keeps it through the
cap.

Also from review: null-byte key separator, sync pointer between the two
identical hooks, tighter comments, and two more derivation tests.
@pandec
pandec merged commit adb5be5 into dev Sep 9, 2026
4 of 5 checks passed
@pandec
pandec deleted the feat/steer-pending-server-derived branch September 9, 2026 11:53
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 86061ee8-3444-4d00-907a-04ad538d8ec9

📥 Commits

Reviewing files that changed from the base of the PR and between 5929fa5 and f5a5ec2.

📒 Files selected for processing (16)
  • README.md
  • apps/mobile/src/state/thread-steer-pending.ts
  • apps/mobile/src/state/use-thread-composer-state.ts
  • apps/mobile/src/state/use-thread-outbox-drain.test.ts
  • apps/mobile/src/state/use-thread-outbox-drain.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/state/entities.ts
  • apps/web/src/state/threadSteerPending.ts
  • apps/web/src/state/use-thread-outbox-drain.ts
  • docs/user/message-queueing.md
  • packages/client-runtime/src/state/threadReducer.test.ts
  • packages/client-runtime/src/state/threadReducer.ts
  • packages/client-runtime/src/state/threadSteerPending.test.ts
  • packages/client-runtime/src/state/threadSteerPending.ts

📝 Walkthrough

Walkthrough

The change replaces client-local steer dispatch tracking with server-derived thread state. It updates activity retention and server projections so parent-agent progress remains available, then adapts web and mobile hooks and documentation to the new behavior.

Changes

Steer pending state

Layer / File(s) Summary
Runtime steer derivation
packages/client-runtime/src/state/threadSteerPending.ts, packages/client-runtime/src/state/threadReducer.ts, packages/client-runtime/src/state/*test.ts
Unread steer IDs now derive from running-turn messages and parent-agent progress. Activity retention preserves the latest parent-agent progress. Tests cover resolution and retention cases.
Running-turn activity projection
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts, apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
The server pins the newest main-agent tool start for a running turn and validates behavior when the activity window reaches 500 rows.
Web and mobile hook integration
apps/web/src/state/*, apps/web/src/components/ChatView.tsx, apps/mobile/src/state/*
Web and mobile hooks consume snapshots without thread-key arguments or outbox dispatch tracking. Delivery code removes dispatch notifications.
Behavior documentation
README.md, docs/user/message-queueing.md
Documentation describes steer markers as thread-derived, cross-device, and reload-persistent.

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

Sequence Diagram(s)

sequenceDiagram
  participant ServerProjection
  participant ClientRuntime
  participant WebOrMobileHook
  ServerProjection->>ClientRuntime: provide running-turn messages and parent progress
  ClientRuntime->>WebOrMobileHook: derive unreadSteerMessageIds(snapshot)
  WebOrMobileHook-->>ClientRuntime: reveal pending IDs after delay
Loading

Suggested reviewers: juliusmarminge, t3dotgg

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/steer-pending-server-derived

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.

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