Skip to content

Session robustness: per-message timers, no stranded Working, cutover races closed - #35

Merged
wingleeio merged 3 commits into
mainfrom
comet/session-state-and-timer-fixes
Aug 10, 2026
Merged

wingleeio merged 3 commits into
mainfrom
comet/session-state-and-timer-fixes

Conversation

@wingleeio

@wingleeio wingleeio commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the two reported bugs plus a batch of robustness defects found by an adversarial audit of the ACP + session-DO rework, all verified against a real wrangler edge (real DOs) and the real claude harness on haiku.

The two reported bugs

Timer opens at ~30:00 on send. started_at survived a session settling: a steer into a parked run kept the previous turn's base, and the UI's pending-send overlay read the stale row directly. Now set_status restamps on any entry into Working from a settled state, clears the base on Idle/Errored, the steer boundary restamps (timers are per user message), and the status strip clamps to the in-flight send during the send→ack window. Verified live: second message into a parked haiku session stamped startedAt 5ms after send.

Session completes but is never marked completed; timer runs forever. The ACP adapter could emit Steered after the turn's Done when the _session/steering response raced the prompt response — the engine un-parks on Steered and then waits forever for a Done that never comes, with the liveness heartbeat keeping the phantom fresh. Steering calls now settle at the turn boundary, before the turn closes; Steered can never trail Done (regression test: steer_racing_the_turn_end_never_emits_steered_after_done).

Audit findings fixed (highlights)

  • chat2 seed was doubly broken: v0.1.32's self-pin (every seed aborted — cutover DOA) and, once un-blocked, a data-loss race (seeding from the pre-backfill local doc forked other devices' rows into the retired lineage). Seeds now wait for room join + 2s of frontier quiet, and a frontier seal aborts the flip on any doc movement since the rebuild.
  • Cutover converges live watchers: registry flip retires s2 handles immediately; watch streams end cleanly and clients resubscribe onto the chat2 adopt path (verified: 36 live s2 frames → flip → resubscribe → full converged transcript).
  • Routed-steer at-least-once ledger: a steer accepted by a dying run (idle-reaper race, parked child death, error discarding queued steers) is re-dispatched at run exit — a message that shows as sent can never silently not run.
  • Input-request stranding: resolvers drain at turn end; a question arriving while parked is auto-declined instead of un-parking into a dead AwaitingInput.
  • Live plan/todo chip: the stale-echo filter ate the stable acp-plan id after the first park/steer boundary — plans never rendered again for the rest of the run.
  • UI standing watches resubscribe (Sessions/Chats/Devices/Spaces): a daemon restart froze them for the app's life — remote Idle/Completed transitions never arrived.
  • Registry batch wedge: ops chunk at 400/batch (delete_space on a ≥250-chat space minted a forever-rejected batch that blocked every later write from the device, session-status rows included).
  • chat2 handles evictable again (sink holds a Weak doc ref), host reacts to ServerReset/PushRejected with compensating checkpoints, chat2 rooms join user-signal liveness probes, checkpoint Range-resume restarts when the blob changes mid-download.
  • e2e-smoke.sh build fix (--example filtered out the comet bin — the smoke never ran) and driver fixes (resubscribe on stream end; parse the framed WatchDocMessages protocol).

Verification

  • Full workspace test suite green (new regression tests: parked-steer timer restamp, ACP steering race).
  • Two-device e2e smoke passes end to end against a real wrangler edge: doc-queued cross-device run, transcript sync, working→idle status round-trip, chat2 seed+flip+adopt underneath.
  • Real claude harness (haiku): run→working(fresh start)→idle(cleared), steer into parked session (fresh timer, reply lands), interrupt (partial output stamped aborted, settles idle).

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

wing-anara and others added 3 commits August 10, 2026 10:43
Two user-visible bugs since the ACP/session-DO rework, one shared root:

- 'Timer opens at 30:00 on send': started_at survived a session settling,
  so the next turn's Working (steer into a parked run passes
  fresh_start=false, and the UI's pending-send overlay reads the row
  directly) displayed the PREVIOUS turn's elapsed. set_status now
  restamps started_at on any entry into Working from a settled state,
  drops it entirely on Idle/Errored, and the steer-boundary split
  restamps too (the timer is per user message, not per child process).
  The status strip additionally clamps to the in-flight send's own
  timestamp during the send->ack window.

- 'Completed session never marked completed, timer keeps counting': the
  ACP adapter could emit Steered AFTER the turn's Done when the
  _session/steering response raced the prompt response (unbiased
  select). The engine un-parks on Steered and waits for a Done that
  never comes -- Working forever, heartbeat keeping it fresh. The
  adapter now settles an in-flight steering call at the turn boundary,
  BEFORE closing the turn: a landed injection emits its Steered ahead of
  the Done, a rejected/unsettled call redelivers as the next turn.

Also hoists the unknown-id InputRequested drop ahead of the parked gate
(un-parking on a dropped event disarmed the idle reaper with no turn
running - leaked child, no reap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… plan chip

Fixes from a two-agent adversarial audit of the ACP + session-DO rework,
plus what the real-edge smoke run surfaced:

- chat2 seed (cutover) was doubly broken: v0.1.32's self-pin (the seed's
  own doc Arc tripped the pinned re-check -- every seed aborted) and,
  once un-blocked, a data-loss race: seeding at open() rebuilt from the
  host's LOCAL doc before the s2 room backfill landed, forking other
  devices' rows (a queued command, a phone steer) into the retired
  lineage -- the e2e smoke's cross-device run never executed. Seeds now
  defer until the room has joined and the doc frontier has been quiet
  for 2s, and a frontier seal aborts the flip if ANY doc movement
  happened since the rebuild.
- Routed-steer at-least-once ledger: a steer accepted into a dying run's
  mailbox (idle-reaper race, parked child death, mid-turn error
  discarding queued boundary steers) is re-dispatched as a fresh turn at
  run exit -- a message that shows as sent can never silently not run.
  steer() also gained the liveness re-check + Working-before-note
  causal ordering the dispatch route already had.
- Input-request stranding: resolvers drain at turn end (a late answer
  un-parked the session into Working with no turn -- timer forever,
  reaper disarmed); a question arriving while parked is auto-declined
  instead of un-parking into a dead AwaitingInput.
- Live plan/todo chip: the stale-echo filter ate the stable 'acp-plan'
  id after the first park/steer boundary -- plans never rendered again
  for the rest of the run. Shared LIVE_PLAN_TOOL_ID const, exempted.
- UI standing watches (Sessions/Chats/Devices/Spaces) resubscribe on
  stream end like the transcript watch -- a daemon restart froze them
  for the app's life (remote Idle/Completed transitions never arrived:
  'the session never completes').
- Reader devices now retire a cached s2 handle when the registry says
  the chat cut over to chat2 (was: frozen fat lineage forever).
- Adopt-path requeued commands commit AFTER the local-update
  subscription (were invisible to the room -- host never saw them).
- Registry batches chunk at 400 ops (delete_space on a big space minted
  a >cap batch the server rejects forever, wedging ALL later writes
  from that device, session-status rows included).
- EngineChatSink holds a Weak doc ref (a strong one made every chat2
  handle perma-pinned -- LRU eviction was dead, warm docs leaked).
- chat2: host reacts to ServerReset/PushRejected with a compensating
  checkpoint; rooms are included in user-signal liveness probes;
  checkpoint Range-resume restarts when the blob changed mid-download.
- ACP: mid-turn Steered drains queued pre-injection updates first
  (text landed on the wrong side of the split); interrupt escalation
  timer aborts BEFORE the child is reaped (pid-reuse signal hazard).
- e2e-smoke.sh: split cargo build invocations (--example filtered out
  the comet bin -- the smoke never ran).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DocHost watches registry chat rows: an s2-mode handle whose chat
  flipped to roomGen 2 is retired ON the flip (not on the next lucky
  reopen) and dropped unless a live run holds the doc -- watch streams
  end cleanly and clients resubscribe onto the chat2 adopt path.
  Verified live: a viewer streaming a run over s2 (36 delta frames
  through the real DO) has its stream end at the flip and its
  resubscribe deliver the full converged transcript.
- e2e_driver: wait_stream resubscribes on stream end (the cutover
  contract every client follows) and parses WatchDocMessages' framed
  reset/upsert protocol -- the old array-shaped predicate could never
  match, so the smoke silently never verified 5a.
- Full two-device smoke now PASSES end to end against a real wrangler
  edge: doc-queued cross-device run, transcript sync, working->idle
  status round-trip, chat2 seed+flip+adopt underneath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wingleeio
wingleeio merged commit b21ce00 into main Aug 10, 2026
1 check passed
guilhermexp pushed a commit to guilhermexp/comet that referenced this pull request Aug 30, 2026
…mer-fixes

Session robustness: per-message timers, no stranded Working, cutover races closed
hoangvu12 referenced this pull request in hoangvu12/roboco Oct 13, 2026
… edge-to-edge, process lifecycle hooks (ui-parity #35)

- shell/RootKind, RootCrossfade (CATransition composite: old-on-top fade,
  Motion.rootSwap 350 ms, no input during the swap, key() identity),
  RootPresentations (+RegisterRootPresentation), AppRoot + AppRootState +
  CoreUnavailableScreen (replaces #13's PlaceholderScreen), RootHosts
  (ContextMenu > Alert > Toast > ActionSheet > GlassMenu > Sheet, all
  registered for instant dismissal before a swap), SystemBarsEffect,
  FirstFrameHold (splash hold, 2 s cap), RobocoAppContent
- core/AppLifecycleHooks: ProcessLifecycleOwner ON_START/ON_STOP ->
  onForeground/onBackground, one registerDefaultNetworkCallback ->
  setNetworkOnline (NetworkOnlineTracker dedupe), single roboco-lifecycle
  thread, install-once guard, clientReady() re-send
- RobocoApplication: routerHooks, appearance, lifecycleHooks install
  before coreStartup.start(), rootInput from 71's state+engines
- MainActivity: applyRobocoEdgeToEdge before super, RobocoAppContent,
  holdFirstFrame, GalleryLaunch kept; manifest configChanges; accent
  color + android:colorAccent; gallery 'App root' section
- tests (written, not run): RootKindTest, RootPresentationsTest,
  NetworkOnlineTrackerTest (plain JVM); RootCrossfadeTest, AppRootTest,
  AppLifecycleHooksTest, MainActivityWindowTest, AppRootScreenshotTest
  (Robolectric/Roborazzi)
hoangvu12 referenced this pull request in hoangvu12/roboco Oct 13, 2026
…FF), gallery fake-sheet registry

- CoreUnavailableScreen had the ink branches swapped (black-on-black in
  dark); the placeholder's inks are restored verbatim
- the gallery's fake sheet registered on the default LocalRootPresentations
  while AppRoot dismissed the section's own registry — provide the same
  registry around the frame so 'Paired' toggling removes the panel
  instantly before the cross-fade, as the ticket's step 12 requires
hoangvu12 referenced this pull request in hoangvu12/roboco Oct 13, 2026
… redeclaration)

- SelectTextSheetScreenshotTest and SelectTextSheetTest both declared a
  private top-level LinearDensity class in package sh.roboco.android.sheets
  (a JVM redeclaration); the screenshot test's copy is ShotLinearDensity.
- SelectTextSheetTest, DetailWidgetTest: assertExists/assertDoesNotExist are
  SemanticsNodeInteraction members, not importable top-level functions.
- RootCrossfadeTest: missing androidx.compose.ui.test.onRoot import.
hoangvu12 referenced this pull request in hoangvu12/roboco Oct 13, 2026
… sheets, transcript tests

Production files read clean (FolderRoute, FolderScreen, SessionList,
AboutGallery, SelectTextSheet, TextSheetController, DetailWidget,
RowWidget, the debug galleries). Test fixes:
- FolderRouteTest: missing uniffi.roboco_core.WorkspaceSnapshot import.
- FolderScreenTest: the loading case shows a live StatusGlyph spinner, which
  never lets Compose idle; wrap in ProvideGlyphClock(frozenAtNanos = 0L).
- SelectTextSheetTest: setContent once per test (configurations swap through
  keyed state); frozen-clock showDetail/selectText writes are delivered with
  Snapshot.sendApplyNotifications().
- DetailWidgetTest: config[Text] throws when absent - use getOrNull; one
  setContent (the widget toggles through state); the scroll-from-widget drag
  went down at offset 0, which cannot scroll - it now drags up the same
  240 px from the same start.
hoangvu12 referenced this pull request in hoangvu12/roboco Oct 13, 2026
Main and debug sources read clean (enableEdgeToEdge/SystemBarStyle,
LocalActivity, ProcessLifecycleOwner, LifecycleRegistry checked with javap;
the @JvmName on TabletShellState's setter is a real clash fix). Tests:
- AppLifecycleHooksTest: lifecycle 2.11's LifecycleOwner is a Kotlin
  interface with `val lifecycle`; `override fun getLifecycle()` overrides
  nothing (compile error). Now `override val lifecycle`.
- AppRootTest: Robolectric's default window is 320 dp wide, so the 900/840 dp
  split cases were clamped; @config qualifiers w1280dp-h900dp.
- RootCrossfadeTest, AppRootScreenshotTest#crossfadeMidLight: frozen-clock
  swaps are delivered (Snapshot.sendApplyNotifications) and given their
  frame before asserting; post-click SideEffect reads get one frame.
- RootPresentationsTest#dismissingTwiceIsSafe: the registry is persistent by
  design (ticket 35), so the test's handler is now idempotent like a real
  presenter's dismiss; the expected count stays 1.
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.

2 participants