Skip to content

fix(todo): prevent stuck status from stale metadata and parts shadowing backend - #637

Merged
Astro-Han merged 12 commits into
Astro-Han:devfrom
Spongeacer:fix/todo-stuck-status
May 15, 2026
Merged

fix(todo): prevent stuck status from stale metadata and parts shadowing backend#637
Astro-Han merged 12 commits into
Astro-Han:devfrom
Spongeacer:fix/todo-stuck-status

Conversation

@Spongeacer

@Spongeacer Spongeacer commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #636: the UI could keep showing stale in_progress todos from old todowrite parts after backend todo state had already reached completed / terminal, or after a live backend todo update explicitly cleared the todo list.

Changes

  • Share the same todo source resolver between composer dock data and Status Summary data.
  • Let matching backend terminal todos override stale active todowrite parts for both primary and fallback sources.
  • Treat live empty backend todo updates as authoritative over stale active parts while preserving historical terminal parts.
  • Preserve that live clear state across ordinary empty backend refreshes, without letting ordinary empty API cache create a new clear state.
  • Allow newer todowrite parts to override an older live empty clear while backend non-empty state is still catching up.
  • Keep active parts when the backend terminal todos describe a different todo list.
  • Keep active parts over ordinary empty backend API cache until a live clear event arrives.
  • Wire Status Summary to globalSync.data.session_todo[sessionID] so it sees backend todo updates instead of only message parts.
  • Restore strict todo metadata validation: malformed metadata falls back to tool input instead of inventing a missing status.
  • Remove the unrelated ScrollView rAF throttle from this PR.
  • Gate the E2E-only todo update route before JSON validation when E2E routes are disabled.
  • Add an E2E-only todo update hook and user-path regression tests covering dock + Status Summary.

Test Coverage

  • bun --cwd packages/app test src/context/global-sync.test.ts src/pages/session/todos/todo-source.test.ts src/pages/session/session-todos.test.ts
  • bun --cwd packages/opencode test test/server/session-e2e-routes.test.ts
  • bun --cwd packages/app typecheck
  • bun --cwd packages/opencode typecheck
  • PLAYWRIGHT_PORT=3317 bun --cwd packages/app test:e2e --project=chromium --grep "backend (terminal update|todo update is empty)" --reporter=line --workers=1
  • git diff --check

wuhongji and others added 6 commits May 14, 2026 11:34
The websearch tool was using Effect.orDie at the tool execution level,
which converted recoverable errors (like quota exceeded, invalid API key,
network issues) into unrecoverable defects. This caused the generic
'Tool execution aborted' message instead of meaningful error messages.

The outer Tool.define already wraps execution with Effect.orDie (tool.ts:124),
so the inner orDie was redundant and harmful - it prevented the McpExa error
catch handler from properly propagating user-friendly error messages.

Fixes Astro-Han#612
## Problem
Session UI experienced typing lag and scroll stuttering, especially with
50+ messages. Root causes identified in Astro-Han#615:

1. No virtualization - all messages rendered in DOM
2. Excessive createMemo calls per message (276+ reactive calls)
3. Unthrottled scroll events updating thumb position every frame
4. Inefficient content-visibility without proper sizing

## Changes

### Virtual Scrolling (message-timeline.tsx)
- Replace <For> with virtua/solid <VList>
- Only render visible messages, reducing DOM nodes from O(n) to O(1)
- Remove redundant content-visibility CSS (handled by virtualizer)
- Add getKey for stable item identity

### Throttled Scroll Events (scroll-view.tsx)
- Throttle onScroll handler with requestAnimationFrame
- Reduce CPU usage during scrolling by batching updates
- Prevent redundant thumb position calculations

### Memo Optimization (message-part.tsx)
- Add custom equals functions to createMemo for Map comparisons
- Avoid unnecessary re-renders when Map contents haven't changed
- Reduce reactive computation overhead

## Testing
- All 1063 unit tests pass
- No breaking changes to public API

Fixes Astro-Han#615
…adowing backend

- Replace strict isValidTodo batch rejection with normalizeTodo that
  fills missing fields (status/priority) with sensible defaults.
  Prevents entire metadata arrays from being discarded when a single
  todo is partially malformed, which previously caused fallback to
  stale tool input.

- Let backend terminal state override stale active parts in
  selectSessionTodoDockSnapshot. When backend todos are all terminal
  but parts still show active, backend is fresher and should win.

- Add tests covering:
  - partial metadata normalization
  - multi-part competition with invalid metadata
  - backend terminal overriding stale active parts
  - empty/non-array metadata fallback

Fixes #TBD
@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface labels May 15, 2026
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR fixes the bug where todo status gets stuck on stale parts when the backend has completed terminal updates. It introduces a backendClearActiveParts flag through global-sync state to signal when active parts should be cleared, extends snapshot selection with a backend-terminal precedence rule, and threads these controls through UI components with comprehensive test coverage.

Changes

Backend Terminal Precedence & Clear Active Parts

Layer / File(s) Summary
Snapshot selection helpers and precedence rule
packages/app/src/pages/session/todos/todo-source.ts
Introduces sameTodoList equality check and sourceTodoSnapshot helper that centralizes backend-vs-parts precedence logic. When backend is terminal and parts are active with matching todos, returns backend-sourced snapshot marked dockEligible: false and historicalTerminal: true. Refactors selectSessionTodoDataSnapshot and selectSessionTodoDockSnapshot to use the shared helper.
Global sync state for clear active parts flag
packages/app/src/context/global-sync.tsx, packages/app/src/context/global-sync/bootstrap.ts, packages/app/src/context/global-sync/event-reducer.ts
Adds session_todo_clear map to global-sync store to track which sessions should clear active parts. Extends setSessionTodo with optional options: { clearActiveParts?: boolean } parameter. When todo.updated events carry empty todo arrays, passes clearActiveParts: true to set the flag in state.
Global sync clear flag integration tests
packages/app/src/context/global-sync/event-reducer.test.ts
Validates that setSessionTodo receives options: { clearActiveParts: true } for empty todo.updated events and options: undefined for non-empty updates. Asserts directory store state is set to [] when clearing.
Component integration of backend and clear flag
packages/app/src/components/session/session-status-panel.tsx, packages/app/src/components/session/session-status-summary.tsx, packages/app/src/pages/session/todos/use-session-todos.ts
SessionStatusPanel derives backend and backendClearActiveParts from global-sync and passes to SessionStatusSummary. Summary component accepts these optional props and passes to selectSessionTodos. use-session-todos threads backendClearActiveParts (derived from session-todo-clear map) into snapshot inputs for both primary and fallback branches.
Comprehensive todo snapshot selection test coverage
packages/app/src/pages/session/todos/todo-source.test.ts
Extensive validation of backend terminal precedence rule and backendClearActiveParts behavior across data/dock/items selectors. Tests cover backend terminal vs active parts matching/non-matching cases, known-empty backend clearing stale parts, and ordinary empty backend retaining active parts for both primary and fallback sources.
selectSessionTodos selection logic test coverage
packages/app/src/pages/session/session-todos.test.ts
Validates top-level selector handling of backend terminal precedence, fallback terminal override, empty-result behavior when backendClearActiveParts: true, and retention of active parts for ordinary empty caches.
Status extractor fallback test cases
packages/app/src/pages/session/session-status-extractors.test.ts
Refines extractTodos test coverage with granular fallback scenarios: metadata with missing status does not reject batch, empty metadata falls back to input, non-array metadata falls back to input, both invalid yields empty array.
End-to-end testing infrastructure for todo updates
packages/opencode/src/server/instance/middleware.ts, packages/opencode/src/server/instance/session.ts, packages/app/e2e/session/session-composer-dock.spec.ts
middleware.ts excludes E2E paths from session ID parsing. session.ts adds POST /__e2e/update-todos endpoint gated by environment variables that updates todos via Todo.Service.update and responds with 204. E2E test suite adds e2eUpdateTodos helper and tests dock + status-summary state transitions on backend terminal updates and clearing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Astro-Han/pawwork#394: Adjusts session todo selection layer and precedence between primary parts and backend sources in todo-source.ts and snapshot selectors.
  • Astro-Han/pawwork#484: Extends global-sync session-todo pipeline with detached todo.updated caching and event handling, overlapping on setSessionTodo callback logic.
  • Astro-Han/pawwork#381: Switches status panel UI logic to derive todos via selectSessionTodos, directly tied to the same selection flow extended by this PR.

Suggested labels

bug, P1

Poem

🐇 Stale parts no more, the rabbits cheer,
Backend truth now crystal clear!
Terminal wins when parts are grey,
Active parts know when to sway—
Empty updates sweep them clean,
Fresh todo docks in between!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% 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 name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main fix: preventing stale metadata and parts from shadowing backend todo state.
Linked Issues check ✅ Passed The PR successfully addresses all requirements from #636: per-item metadata normalization is restored [session-status-extractors.test.ts, event-reducer.ts], backend terminal todos override stale active parts [todo-source.ts], known-empty backend updates clear stale parts while preserving terminal history [todo-source.ts, global-sync.tsx], and E2E regression tests cover dock and Status Summary behavior [session-composer-dock.spec.ts].
Out of Scope Changes check ✅ Passed All file changes are directly related to fixing the stale todo status issue: test expansions for metadata/source resolution, todo-source refactoring, global-sync tracking of backend clears, component wiring to observe backend updates, E2E helpers, and session middleware/server for E2E routes.
Description check ✅ Passed The PR description is comprehensive and well-structured. It clearly explains the problem being fixed, lists all changes made, provides test coverage details, and includes verification steps. All major template sections are covered with substantive content.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

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

❤️ Share

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

@github-actions github-actions Bot 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.

Suggested priority: P2 (includes user-path files (packages/app/src/pages/session/session-status-extractors.test.ts, packages/app/src/pages/session/session-status-extractors.ts, packages/app/src/pages/session/todos/todo-source.test.ts, packages/app/src/pages/session/todos/todo-source.ts)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces todo metadata normalization to handle malformed data with sensible defaults and updates the todo source logic to prefer terminal backend states over stale active parts, preventing the UI from getting stuck. It also optimizes the ScrollView component by throttling scroll events. Feedback suggests extending the backend-preference logic to selectSessionTodoDataSnapshot for UI consistency and implementing cleanup and untrack() for the scroll throttling logic to align with SolidJS best practices.

Comment thread packages/app/src/pages/session/todos/todo-source.ts Outdated
Comment thread packages/ui/src/components/scroll-view.tsx Outdated
@github-actions github-actions Bot added the harness Model harness, prompts, tool descriptions, and session mechanics label May 15, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/app/src/pages/session/todos/todo-source.ts (1)

19-27: ⚡ Quick win

Clarify type signature to match actual usage.

The function signature declares both parameters as SessionTodoItem[], but it's called at line 39 with sourceBackend (which is Todo[] from input.backend) and sourceParts (which is SessionTodoItem[]). While TypeScript permits this due to structural compatibility, the signature is misleading. Consider either:

  1. Updating the signature to (backend: Todo[], parts: SessionTodoItem[]), or
  2. Making it generic: <T extends { id?: string; content: string }>(backend: T[], parts: T[]).

This improves clarity for future maintainers.

🤖 Prompt for 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.

In `@packages/app/src/pages/session/todos/todo-source.ts` around lines 19 - 27,
The signature of sameTodoList is misleading because it currently types both
parameters as SessionTodoItem[] but is invoked with sourceBackend (Todo[] from
input.backend) and sourceParts (SessionTodoItem[]); update the signature to
reflect real usage—for example change sameTodoList(backend: Todo[], parts:
SessionTodoItem[]) or make it generic like function sameTodoList<T extends {
id?: string; content: string }>(backend: T[], parts: SessionTodoItem[] |
T[])—and adjust any imports/types (Todo, SessionTodoItem) referenced where
sameTodoList is defined/used (e.g., calls from input.backend, sourceBackend,
sourceParts) so the types align and intent is clear.
🤖 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 `@packages/opencode/src/server/instance/session.ts`:
- Around line 143-151: The route currently runs validator(...) before the E2E
feature gate so malformed requests can leak the route; move the
e2eSessionRoutesEnabled() check to run before validation (i.e., as a
pre-validation middleware) so it returns c.notFound() for all requests when
disabled. Concretely, ensure the e2eSessionRoutesEnabled() guard executes before
the validator(...) middleware (reference validator, z.object({ sessionID:
SessionID.zod, todos: z.array(Todo.Input) }) and the async handler using
c.notFound()), or add a small pre-validator wrapper that performs the
e2eSessionRoutesEnabled() check and calls c.notFound() when false.

---

Nitpick comments:
In `@packages/app/src/pages/session/todos/todo-source.ts`:
- Around line 19-27: The signature of sameTodoList is misleading because it
currently types both parameters as SessionTodoItem[] but is invoked with
sourceBackend (Todo[] from input.backend) and sourceParts (SessionTodoItem[]);
update the signature to reflect real usage—for example change
sameTodoList(backend: Todo[], parts: SessionTodoItem[]) or make it generic like
function sameTodoList<T extends { id?: string; content: string }>(backend: T[],
parts: SessionTodoItem[] | T[])—and adjust any imports/types (Todo,
SessionTodoItem) referenced where sameTodoList is defined/used (e.g., calls from
input.backend, sourceBackend, sourceParts) so the types align and intent is
clear.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2871d8d5-0a24-4c4c-89d3-e8c341507cc8

📥 Commits

Reviewing files that changed from the base of the PR and between e94bc1c and f8dd280.

📒 Files selected for processing (14)
  • packages/app/e2e/session/session-composer-dock.spec.ts
  • packages/app/src/components/session/session-status-panel.tsx
  • packages/app/src/components/session/session-status-summary.tsx
  • packages/app/src/context/global-sync.tsx
  • packages/app/src/context/global-sync/bootstrap.ts
  • packages/app/src/context/global-sync/event-reducer.test.ts
  • packages/app/src/context/global-sync/event-reducer.ts
  • packages/app/src/pages/session/session-status-extractors.test.ts
  • packages/app/src/pages/session/session-todos.test.ts
  • packages/app/src/pages/session/todos/todo-source.test.ts
  • packages/app/src/pages/session/todos/todo-source.ts
  • packages/app/src/pages/session/todos/use-session-todos.ts
  • packages/opencode/src/server/instance/middleware.ts
  • packages/opencode/src/server/instance/session.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/app/src/context/global-sync/bootstrap.ts

Comment thread packages/opencode/src/server/instance/session.ts Outdated
@Astro-Han
Astro-Han merged commit 0df4511 into Astro-Han:dev May 15, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows harness Model harness, prompts, tool descriptions, and session mechanics ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Todo list frontend status gets stuck — stale parts shadow backend updates

2 participants