Skip to content

refactor(web): migrate to TanStack Query + add WS event accounting (Phase 1)#1130

Open
carlosflorencio wants to merge 56 commits into
mainfrom
feature/ws-event-accounting
Open

refactor(web): migrate to TanStack Query + add WS event accounting (Phase 1)#1130
carlosflorencio wants to merge 56 commits into
mainfrom
feature/ws-event-accounting

Conversation

@carlosflorencio
Copy link
Copy Markdown
Member

@carlosflorencio carlosflorencio commented May 28, 2026

Summary

Supersedes #1052. Bundles the TanStack Query server-state migration (waves 0–5) with the first phase of WebSocket event accounting so the two ship as one coherent reliability unit. The migration alone introduces a parallel write path that can silently desync from Zustand during the transition; the accounting alone leaves the FE on a Zustand model we're trying to retire. Together they deliver "every server-state event the backend emits is observably received, parsed, and applied to the FE cache."

Important Changes

  • TanStack Query migration (parent commit b95b168b) — server state for features, comments, integrations-shell, automations, workspace, settings, jira, linear, gitlab, github, kanban, office, session, session-runtime, and session-runtime-streams now flows through lib/query/{keys,query-options,bridge} with a WS → query bridge so partially-migrated screens keep working during the transition.
  • WS event accounting (Phase 1) — per-connection monotonic seq + connection_id on every outbound WS envelope, bounded backend ring buffer (wsSentLog, 5000 entries), FE receive-side WsAccount ring buffer (5000), a new /api/v1/e2e/ws-sent endpoint, and a Playwright fixture hook that diffs "what the backend sent" vs "what the FE processed" at end-of-test. Strict mode via KANDEV_E2E_WS_ASSERT=1 turns any drop into a test failure that names the missing event.
  • Phase 2 plandocs/specs/ws-event-accounting/plan.md (force-added since docs/specs/*/plan.md is gitignored) describes the per-session sequencing, ack channel + production metrics, bridge-handler audit (wrapBridgeHandler + BRIDGE_SKIPPED_ACTIONS), and audit/migration workstreams that finish the reliability guarantee.

Validation

  • pnpm --filter @kandev/web typecheck — pending re-run in CI (local pnpm install hit a half-broken state with the rebased lockfile; CI runs in a clean container).
  • pnpm --filter @kandev/web lint --max-warnings 0 — pending CI.
  • pnpm --filter @kandev/web test — Phase 1 adds 23 unit tests for WsAccount / detectGaps; bridge tests from b95b168b cover the WS → query mirroring.
  • Backend: go test ./internal/gateway/websocket/... ./cmd/kandev/... — covers seq monotonicity, broadcast clone isolation, ring-buffer eviction, /api/v1/e2e/ws-sent happy + error paths.
  • E2E: KANDEV_E2E_WS_ASSERT=1 left off by default in CI; Workstream 3 of the Phase 2 plan flips it on after the bridge instrumentation and per-session work land.

Possible Improvements

The accounting layer currently records at the parse seam (handleParsedMessage), which catches "envelope never arrived" but not "envelope arrived and didn't mutate state." Phase 2's wrapBridgeHandler closes that gap by asserting that every observed WS event with a session_id corresponds to a bridge audit entry with cacheChanged === true (or is on the documented Zustand-only allowlist). Without that follow-up, a bridge wiring bug — wrong query key, unmounted slice — can still silently break a UI screen during the migration window. Phase 2 is scoped in docs/specs/ws-event-accounting/plan.md.

Checklist

  • Tests added / updated
  • Docs updated
  • Changelog entry (if user-facing)

Related issues

Supersedes #1052.

Preview Environment

URL https://kandev-pr-1130-bwo7.sprites.app
Commit 50ee027
Agent Mock agent

Updates automatically on each push. Destroyed when the PR is closed.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Important

Review skipped

Too many files!

This PR contains 299 files, which is 149 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10e7415d-d8a1-4ba0-89bd-1f175769a3d9

📥 Commits

Reviewing files that changed from the base of the PR and between 1fdeda9 and 50ee027.

⛔ Files ignored due to path filters (1)
  • apps/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (299)
  • .github/workflows/e2e-tests.yml
  • AGENTS.md
  • apps/backend/cmd/kandev/e2e_reset.go
  • apps/backend/cmd/kandev/e2e_ws_sent_test.go
  • apps/backend/cmd/kandev/helpers.go
  • apps/backend/internal/gateway/websocket/client.go
  • apps/backend/internal/gateway/websocket/client_seq_test.go
  • apps/backend/internal/gateway/websocket/hub.go
  • apps/backend/internal/gateway/websocket/hub_session_mode_test.go
  • apps/backend/internal/gateway/websocket/hub_session_seq.go
  • apps/backend/internal/gateway/websocket/hub_session_seq_test.go
  • apps/backend/internal/gateway/websocket/hub_ws_sent_test.go
  • apps/backend/internal/gateway/websocket/ws_sent_log.go
  • apps/backend/pkg/websocket/message.go
  • apps/web/AGENTS.md
  • apps/web/app/actions/features.ts
  • apps/web/app/github/page.tsx
  • apps/web/app/gitlab/page.tsx
  • apps/web/app/jira/page.tsx
  • apps/web/app/layout.tsx
  • apps/web/app/linear/page.tsx
  • apps/web/app/office/agents/[id]/channels/page.tsx
  • apps/web/app/office/agents/[id]/components/agent-config-cli-card.tsx
  • apps/web/app/office/agents/[id]/components/agent-configuration-tab.test.tsx
  • apps/web/app/office/agents/[id]/components/agent-configuration-tab.tsx
  • apps/web/app/office/agents/[id]/components/agent-overview-tab.tsx
  • apps/web/app/office/agents/[id]/components/agent-permissions-tab.tsx
  • apps/web/app/office/agents/[id]/components/agent-runs-tab.test.tsx
  • apps/web/app/office/agents/[id]/components/agent-runs-tab.tsx
  • apps/web/app/office/agents/[id]/components/agent-skills-tab.tsx
  • apps/web/app/office/agents/[id]/configuration/page.tsx
  • apps/web/app/office/agents/[id]/dashboard/dashboard-view.tsx
  • apps/web/app/office/agents/[id]/instructions/page.tsx
  • apps/web/app/office/agents/[id]/layout.tsx
  • apps/web/app/office/agents/[id]/memory/page.tsx
  • apps/web/app/office/agents/[id]/permissions/page.tsx
  • apps/web/app/office/agents/[id]/runs/runs-list-view.tsx
  • apps/web/app/office/agents/[id]/skills/page.tsx
  • apps/web/app/office/agents/agents-page-client.tsx
  • apps/web/app/office/agents/components/agent-card.tsx
  • apps/web/app/office/agents/components/agent-role-badge.tsx
  • apps/web/app/office/agents/components/agent-status-dot.tsx
  • apps/web/app/office/agents/components/create-agent-dialog.tsx
  • apps/web/app/office/agents/page.tsx
  • apps/web/app/office/components/new-task-bottom-bar.tsx
  • apps/web/app/office/components/new-task-selector-row.tsx
  • apps/web/app/office/components/new-task-stages.tsx
  • apps/web/app/office/components/office-sidebar.tsx
  • apps/web/app/office/components/office-tq-seeder.tsx
  • apps/web/app/office/components/sidebar-agents-list.tsx
  • apps/web/app/office/components/sidebar-projects-list.tsx
  • apps/web/app/office/components/workspace-rail.tsx
  • apps/web/app/office/inbox/inbox-item-row.tsx
  • apps/web/app/office/inbox/inbox-page-client.tsx
  • apps/web/app/office/layout.tsx
  • apps/web/app/office/page-client.tsx
  • apps/web/app/office/projects/[id]/page.tsx
  • apps/web/app/office/projects/[id]/project-executor-section.tsx
  • apps/web/app/office/projects/[id]/project-header.tsx
  • apps/web/app/office/projects/[id]/project-repos-section.tsx
  • apps/web/app/office/projects/[id]/project-tasks-section.tsx
  • apps/web/app/office/projects/create-project-dialog.tsx
  • apps/web/app/office/projects/page.tsx
  • apps/web/app/office/projects/project-card.tsx
  • apps/web/app/office/projects/projects-page-client.tsx
  • apps/web/app/office/routines/[id]/routine-detail-view.tsx
  • apps/web/app/office/routines/routines-content.tsx
  • apps/web/app/office/routines/routines-page-client.tsx
  • apps/web/app/office/routines/run-row.tsx
  • apps/web/app/office/setup/page.tsx
  • apps/web/app/office/setup/setup-wizard.tsx
  • apps/web/app/office/setup/step-agent.tsx
  • apps/web/app/office/setup/step-review.tsx
  • apps/web/app/office/tasks/[id]/advanced-panels/chat-panel.tsx
  • apps/web/app/office/tasks/[id]/office-dockview-layout.tsx
  • apps/web/app/office/tasks/[id]/page.tsx
  • apps/web/app/office/tasks/[id]/use-advanced-session.ts
  • apps/web/app/office/tasks/page.tsx
  • apps/web/app/office/tasks/task-board.tsx
  • apps/web/app/office/tasks/task-filters.tsx
  • apps/web/app/office/tasks/task-row.tsx
  • apps/web/app/office/tasks/tasks-list.tsx
  • apps/web/app/office/tasks/tasks-page-client.tsx
  • apps/web/app/office/tasks/use-paginated-tasks.ts
  • apps/web/app/office/tasks/use-tasks-tree.ts
  • apps/web/app/office/workspace/activity/activity-page-client.tsx
  • apps/web/app/office/workspace/activity/page.tsx
  • apps/web/app/office/workspace/costs/costs-page-client.tsx
  • apps/web/app/office/workspace/costs/page.tsx
  • apps/web/app/office/workspace/org/page.tsx
  • apps/web/app/office/workspace/settings/components/danger-zone-section.tsx
  • apps/web/app/office/workspace/settings/components/settings-content.tsx
  • apps/web/app/office/workspace/settings/export/export-preview.tsx
  • apps/web/app/office/workspace/skills/create-skill-form.tsx
  • apps/web/app/office/workspace/skills/page.tsx
  • apps/web/app/office/workspace/skills/skill-detail.tsx
  • apps/web/app/office/workspace/skills/skills-page-client.tsx
  • apps/web/app/page.tsx
  • apps/web/app/settings/agents/[agentId]/page.tsx
  • apps/web/app/settings/agents/[agentId]/profiles/[profileId]/use-agent-profile-settings.ts
  • apps/web/app/settings/agents/page.tsx
  • apps/web/app/settings/executor/[id]/page.tsx
  • apps/web/app/settings/executor/[id]/profile/[profileId]/page.tsx
  • apps/web/app/settings/executor/new/page.tsx
  • apps/web/app/settings/executors/[profileId]/page.tsx
  • apps/web/app/settings/executors/new/[type]/page.tsx
  • apps/web/app/settings/executors/new/[type]/ssh-create-page.tsx
  • apps/web/app/settings/executors/page.tsx
  • apps/web/app/settings/executors/ssh/[executorId]/page.tsx
  • apps/web/app/settings/integrations/github/page.tsx
  • apps/web/app/settings/layout.tsx
  • apps/web/app/settings/workspace/workspace-edit-client.tsx
  • apps/web/app/settings/workspace/workspace-repositories-client.tsx
  • apps/web/app/settings/workspace/workspaces-page-client.tsx
  • apps/web/app/tasks/page.tsx
  • apps/web/components/agent/cli-profile-editor.test.tsx
  • apps/web/components/agent/cli-profile-editor.tsx
  • apps/web/components/automations/config-section.tsx
  • apps/web/components/command-panel-footer.tsx
  • apps/web/components/command-panel.tsx
  • apps/web/components/config-chat/config-chat-panel.tsx
  • apps/web/components/config-chat/use-config-chat.ts
  • apps/web/components/editors/file-actions-dropdown.tsx
  • apps/web/components/github/github-status-seed.tsx
  • apps/web/components/github/github-status.tsx
  • apps/web/components/github/issue-watch-dialog.tsx
  • apps/web/components/github/issue-watch-table.tsx
  • apps/web/components/github/my-github/pr-row-task-indicator.test.tsx
  • apps/web/components/github/my-github/pr-row-task-indicator.tsx
  • apps/web/components/github/pr-ci-popover.tsx
  • apps/web/components/github/pr-detail-panel.tsx
  • apps/web/components/github/pr-status-chip.test.tsx
  • apps/web/components/github/pr-task-icon.render.test.tsx
  • apps/web/components/github/pr-task-icon.tsx
  • apps/web/components/github/review-watch-dialog.tsx
  • apps/web/components/github/review-watch-table.tsx
  • apps/web/components/global-commands.tsx
  • apps/web/components/homepage-commands.tsx
  • apps/web/components/improve-kandev-dialog.tsx
  • apps/web/components/integrations/workspace-scoped-section.tsx
  • apps/web/components/jira/jira-issue-watch-dialog.tsx
  • apps/web/components/jira/jira-issue-watch-table.tsx
  • apps/web/components/kanban-board-grid.tsx
  • apps/web/components/kanban-board.tsx
  • apps/web/components/kanban-card-content.tsx
  • apps/web/components/kanban-card-menu-items.tsx
  • apps/web/components/kanban-card-preview.tsx
  • apps/web/components/kanban-column.tsx
  • apps/web/components/kanban-display-dropdown.tsx
  • apps/web/components/kanban-with-preview.tsx
  • apps/web/components/kanban/graph2-task-pipeline.tsx
  • apps/web/components/kanban/mobile-menu-sheet.tsx
  • apps/web/components/kanban/swimlane-container.tsx
  • apps/web/components/kanban/swimlane-graph-content.tsx
  • apps/web/components/kanban/swimlane-kanban-content.tsx
  • apps/web/components/kanban/task-multi-select-toolbar.tsx
  • apps/web/components/linear/linear-issue-watch-dialog.tsx
  • apps/web/components/linear/linear-issue-watch-table.tsx
  • apps/web/components/quick-chat/quick-chat-dialog.tsx
  • apps/web/components/quick-chat/quick-chat-modal.tsx
  • apps/web/components/quick-chat/use-quick-chat-modal.test.ts
  • apps/web/components/quick-chat/use-quick-chat-modal.ts
  • apps/web/components/review/review-dialog.tsx
  • apps/web/components/review/review-top-bar.tsx
  • apps/web/components/session-commands.tsx
  • apps/web/components/session/prepare-progress.test.tsx
  • apps/web/components/session/prepare-progress.tsx
  • apps/web/components/settings/agent-profile-page.tsx
  • apps/web/components/settings/changelog-notification-card.tsx
  • apps/web/components/settings/config-chat-agent-section.tsx
  • apps/web/components/settings/editors-settings-state.tsx
  • apps/web/components/settings/executor-profiles-card.tsx
  • apps/web/components/settings/general-settings.tsx
  • apps/web/components/settings/keyboard-shortcuts-card.tsx
  • apps/web/components/settings/profile-edit/inline-secret-select.tsx
  • apps/web/components/settings/profile-status-panels.tsx
  • apps/web/components/settings/prompts-settings.tsx
  • apps/web/components/settings/secrets-settings.tsx
  • apps/web/components/settings/settings-app-sidebar.tsx
  • apps/web/components/settings/shell-settings-card.tsx
  • apps/web/components/settings/sprites-settings.tsx
  • apps/web/components/settings/voice-mode-settings.tsx
  • apps/web/components/state-hydrator.tsx
  • apps/web/components/state-provider.tsx
  • apps/web/components/task-create-dialog-autopick.ts
  • apps/web/components/task-create-dialog-create-mode-selectors.tsx
  • apps/web/components/task-create-dialog-effects.test.ts
  • apps/web/components/task-create-dialog-form-body.tsx
  • apps/web/components/task-create-dialog-helpers.ts
  • apps/web/components/task-create-dialog-options.tsx
  • apps/web/components/task-create-dialog-state.ts
  • apps/web/components/task-create-dialog-types.ts
  • apps/web/components/task/changes-panel-data.tsx
  • apps/web/components/task/changes-panel-timeline-grouping.test.tsx
  • apps/web/components/task/changes-panel-timeline.tsx
  • apps/web/components/task/chat/chat-input-area.tsx
  • apps/web/components/task/chat/chat-input-container.tsx
  • apps/web/components/task/chat/chat-input-toolbar.tsx
  • apps/web/components/task/chat/context-items/lazy-plan-preview.tsx
  • apps/web/components/task/chat/context-items/prompt-preview.tsx
  • apps/web/components/task/chat/message-renderer.tsx
  • apps/web/components/task/chat/messages/action-message.test.tsx
  • apps/web/components/task/chat/messages/agent-status.tsx
  • apps/web/components/task/chat/messages/chat-message.test.tsx
  • apps/web/components/task/chat/messages/message-actions.tsx
  • apps/web/components/task/chat/task-mention-items.test.ts
  • apps/web/components/task/chat/task-mention-items.ts
  • apps/web/components/task/chat/tiptap-input.tsx
  • apps/web/components/task/chat/use-chat-panel-state.ts
  • apps/web/components/task/chat/use-tiptap-editor.ts
  • apps/web/components/task/chat/voice-input-button.tsx
  • apps/web/components/task/dockview-desktop-layout.tsx
  • apps/web/components/task/dockview-header-actions.tsx
  • apps/web/components/task/dockview-session-tabs.ts
  • apps/web/components/task/dockview-shared.tsx
  • apps/web/components/task/dockview-watermark.tsx
  • apps/web/components/task/editors-menu.tsx
  • apps/web/components/task/executor-settings-button.test.tsx
  • apps/web/components/task/file-editor-panel.tsx
  • apps/web/components/task/files-panel.tsx
  • apps/web/components/task/layout-preset-selector.tsx
  • apps/web/components/task/mobile/mobile-file-viewer-panel.tsx
  • apps/web/components/task/mobile/mobile-repo-pill.tsx
  • apps/web/components/task/mobile/mobile-repos-section.tsx
  • apps/web/components/task/mobile/mobile-sessions-section.tsx
  • apps/web/components/task/mobile/session-task-switcher-sheet-hooks.ts
  • apps/web/components/task/mode-selector.tsx
  • apps/web/components/task/model-selector.tsx
  • apps/web/components/task/new-session-dialog.tsx
  • apps/web/components/task/new-subtask-dialog.tsx
  • apps/web/components/task/new-task-button.tsx
  • apps/web/components/task/passthrough-terminal.tsx
  • apps/web/components/task/passthrough-toolbar.test.tsx
  • apps/web/components/task/passthrough-toolbar.tsx
  • apps/web/components/task/plan-tab.test.tsx
  • apps/web/components/task/plan-tab.tsx
  • apps/web/components/task/preview-session-tabs.tsx
  • apps/web/components/task/quick-chat-button.tsx
  • apps/web/components/task/recent-task-switcher-hooks.ts
  • apps/web/components/task/recent-task-switcher-model.test.ts
  • apps/web/components/task/recent-task-switcher-model.ts
  • apps/web/components/task/repository-scripts-menu.tsx
  • apps/web/components/task/session-reopen-menu.tsx
  • apps/web/components/task/session-sort.ts
  • apps/web/components/task/session-tab.tsx
  • apps/web/components/task/sessions-dropdown.tsx
  • apps/web/components/task/shell-terminal.tsx
  • apps/web/components/task/sidebar-filter/use-filter-value-options.ts
  • apps/web/components/task/simple/OfficeSimplePane.tsx
  • apps/web/components/task/simple/chat-activity-tabs.test.tsx
  • apps/web/components/task/simple/chat-activity-tabs.tsx
  • apps/web/components/task/simple/components/agents-multi-picker.test.tsx
  • apps/web/components/task/simple/components/agents-multi-picker.tsx
  • apps/web/components/task/simple/components/approval-action-bar.test.tsx
  • apps/web/components/task/simple/components/assignee-picker.tsx
  • apps/web/components/task/simple/components/blockers-picker.test.tsx
  • apps/web/components/task/simple/components/blockers-picker.tsx
  • apps/web/components/task/simple/components/parent-picker.tsx
  • apps/web/components/task/simple/components/pending-approval-badge.test.tsx
  • apps/web/components/task/simple/components/pending-approval-badge.tsx
  • apps/web/components/task/simple/components/priority-picker.test.tsx
  • apps/web/components/task/simple/components/project-picker.tsx
  • apps/web/components/task/simple/components/run-error-entry.tsx
  • apps/web/components/task/simple/components/session-timeline-entry.test.tsx
  • apps/web/components/task/simple/components/session-timeline-entry.tsx
  • apps/web/components/task/simple/components/status-picker.test.tsx
  • apps/web/components/task/simple/components/topbar-working-indicator.test.tsx
  • apps/web/components/task/simple/components/topbar-working-indicator.tsx
  • apps/web/components/task/simple/task-activity.tsx
  • apps/web/components/task/simple/task-chat.test.tsx
  • apps/web/components/task/simple/task-chat.tsx
  • apps/web/components/task/task-center-panel.tsx
  • apps/web/components/task/task-changes-panel.tsx
  • apps/web/components/task/task-chat-panel.tsx
  • apps/web/components/task/task-item.test.tsx
  • apps/web/components/task/task-item.tsx
  • apps/web/components/task/task-page-content.tsx
  • apps/web/components/task/task-plan-panel.tsx
  • apps/web/components/task/task-plan-revisions.tsx
  • apps/web/components/task/task-select-helpers.ts
  • apps/web/components/task/task-session-sidebar-aggregate.test.ts
  • apps/web/components/task/task-session-sidebar-aggregate.ts
  • apps/web/components/task/task-session-sidebar.tsx
  • apps/web/components/task/task-switcher.test.tsx
  • apps/web/components/task/use-review-dialog.ts
  • apps/web/components/workflow-selector-row.tsx
  • apps/web/e2e/fixtures/test-base.ts
  • apps/web/e2e/helpers/api-client.ts
  • apps/web/e2e/helpers/session-store.ts
  • apps/web/e2e/helpers/ws-account.ts
  • apps/web/e2e/scripts/run-e2e.sh
  • apps/web/e2e/tests/kanban/preview-primary-session.spec.ts
  • apps/web/e2e/tests/system/ws-event-accounting.spec.ts
  • apps/web/eslint.config.mjs
  • apps/web/hooks/domains/comments/use-run-comment.test.ts
  • apps/web/hooks/domains/comments/use-run-comment.ts
  • apps/web/hooks/domains/features/use-feature.test.ts
  • apps/web/hooks/domains/features/use-feature.ts
  • apps/web/hooks/domains/github/use-active-task-pr-files.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ws-event-accounting

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.

@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @carlosflorencio's task in 5m 43s —— View job


Findings

Suggestion (recommended, doesn't block)

  1. computeWsDrops comparison window misaligned with clearWsAccount()apps/web/e2e/helpers/ws-account.ts:136

    • Issue: computeWsDrops fetches all backend events since connection start (no sinceSeq). After clearWsAccount(page), the FE ring is empty but connectionId is preserved. feSnapshot.minSeq is null, processedSeqs is []. Every pre-clear event in the backend ring buffer is flagged as "dropped" — a false positive.
    • Why: When KANDEV_E2E_WS_ASSERT=1 is enabled (Phase 2 plan), any spec that calls clearWsAccount() before the test body will immediately produce spurious failures, undermining the reliability guarantee the feature was built for.
    • Fix: Scope the backend query to the FE's current window — pass sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined to apiClient.getWsSent(). Fix this →
  2. time.Sleep in hub tests — inconsistent with project conventionapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102

    • Issue: Three time.Sleep calls synchronise goroutines in e2e_ws_sent_test.go. The adjacent hub_session_mode_test.go (same package) uses testing/synctest for the exact same hub — AGENTS.md documents this as the required pattern ("Prefer testing/synctest over time.Sleep; reserve sleeps for integration tests with real subprocess execution time").
    • Why: Sleep-based synchronisation is fragile under CI load and can introduce non-deterministic failures.
    • Fix: Wrap the test body in synctest.Test(t, func(t *testing.T) {...}), call synctest.Wait() after go hub.Run(ctx) and again after BroadcastToUser to replace all three sleeps with deterministic goroutine-idle checks.

Summary

Severity Count
Blocker 0
Suggestion 2

Verdict: Ready with suggestions.

The accounting architecture is solid — monotonic seq stamping, per-connection ring buffers, a clean diffWsAccount pure function, and good Go test coverage for the backend ring. The two suggestions are a pre-emptive fix for a false-positive path that will bite when WS_ASSERT_ENABLED=1 gets turned on globally, and a convention alignment for the new test file.

@carlosflorencio carlosflorencio changed the title refactor(web): TanStack Query migration + WS event accounting (Phase 1) refactor(web): migrate to TanStack Query + add WS event accounting (Phase 1) May 28, 2026
Comment thread apps/web/e2e/helpers/ws-account.ts Outdated
Comment thread apps/backend/cmd/kandev/e2e_ws_sent_test.go Outdated
@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @carlosflorencio's task in 5m 15s —— View job


Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments)
  • Read skill file and key changed files
  • Review backend changes (WS accounting: ws_sent_log.go, client.go, hub.go, message.go)
  • Review backend tests (e2e_ws_sent_test.go, hub_ws_sent_test.go, client_seq_test.go)
  • Review frontend E2E helpers (ws-account.ts, test-base.ts, api-client.ts)
  • Review bridge audit layer (bridge/index.ts, bridge-audit-diff.ts, session-runtime-streams.ts)
  • Post inline findings (2 new inline comments)
  • Post findings report

Findings

Blocker

None.

Suggestion (4 total — 2 from prior review still open, 2 new)

Prior review (still outstanding):

  1. computeWsDrops false-positive after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:181 (no sinceSeq scoping)
  2. time.Sleep in hub tests — apps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102 (use synctest.Wait() per convention)

New from this pass:
3. auditBuffer.shift() O(N) eviction — apps/web/lib/query/bridge/index.ts:120. Under heavy agent stream output, every eviction from the 5000-entry buffer is O(N). Fix: use a Map-based ring like WsAccount.
4. Stream bridge handlers guaranteed cacheChanged: falseapps/web/lib/query/bridge/session-runtime-streams.ts:62,83,104. session.shell.output, session.process.output, and terminal.output are wrapped by wrapBridgeHandler but only write to ring buffers (not TQ cache). Every chunk produces a cache-unchanged audit entry. These three actions are absent from BRIDGE_SKIPPED_ACTIONS, so Phase 2's global WS_ASSERT_ENABLED=1 flip will immediately fail any test that runs an agent session. Fix: add them to BRIDGE_SKIPPED_ACTIONS (in both bridge/index.ts and bridge-audit-diff.ts), or stop wrapping ring-buffer-only handlers with wrapBridgeHandler.

Verdict: Ready with suggestions. Findings 3 and 4 are inexpensive to fix now and will block the Phase 2 WS_ASSERT_ENABLED flip if left open.

Comment thread apps/web/lib/query/bridge/index.ts Outdated
Comment thread apps/web/lib/query/bridge/session-runtime-streams.ts Outdated
@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Findings

Blocker (must fix before merge)

None.

Suggestion (recommended, doesn't block)

Previous review (still open — not yet addressed):

  1. computeWsDrops comparison window misaligned with clearWsAccount()apps/web/e2e/helpers/ws-account.ts:181

    • Issue: computeWsDrops fetches all backend events since connection start (no sinceSeq). After clearWsAccount(page), feSnapshot.processedSeqs is empty but connectionId is preserved. Every pre-clear event in the ring buffer is flagged as "dropped."
    • Why: Any spec that calls clearWsAccount() before the test body will produce spurious failures the moment KANDEV_E2E_WS_ASSERT=1 is enabled globally in Phase 2.
    • Fix: Scope the backend query: sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined, then apiClient.getWsSent(feSnapshot.connectionId, sinceSeq). Fix this →
  2. time.Sleep in hub testsapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102

    • Issue: Three time.Sleep calls synchronise goroutines. The adjacent hub_session_mode_test.go uses testing/synctest for the same hub — per AGENTS.md convention.
    • Fix: Wrap in synctest.Test(t, func(t *testing.T) {...}) and replace each sleep with synctest.Wait().

New findings from this review pass:

  1. auditBuffer.shift() is O(N) evictionapps/web/lib/query/bridge/index.ts:120

    • Issue: Array.shift() is O(N). The auditBuffer is bounded at 5000 entries. Once full, every new entry (including high-frequency session.shell.output chunks) triggers an O(5000) copy. WsAccount (the FE-side accounting ring) already solves this with a Map for O(1) FIFO eviction.
    • Fix: Replace auditBuffer: BridgeAuditEntry[] with Map<number, BridgeAuditEntry> plus a monotonic counter — same pattern as WsAccount. See inline comment for the full replacement. Fix this →
  2. Stream bridge handlers guaranteed cacheChanged: false in Phase 2 auditapps/web/lib/query/bridge/session-runtime-streams.ts:62,83,104

    • Issue: session.shell.output, session.process.output, and terminal.output are wrapped by wrapBridgeHandler but call appendToRing, not queryClient.setQueryData. The cache subscription counter stays 0, producing audit entries with cacheChanged: false. None of these three actions are in BRIDGE_SKIPPED_ACTIONS. When Phase 2 globally enables bridge-audit assertion, every test with an active agent session will immediately fail with cache-unchanged drops for every stream chunk — potentially thousands per test.
    • Fix: Either add all three to BRIDGE_SKIPPED_ACTIONS (in both bridge/index.ts and bridge-audit-diff.ts), or don't wrap ring-buffer-only handlers with wrapBridgeHandler at all. See inline comment on line 62 for both options. Fix this →

Summary

Severity Count
Blocker 0
Suggestion 4 (2 from prior review + 2 new)

Verdict: Ready with suggestions.

The accounting architecture is sound — monotonic seq on every outbound envelope, broadcast-clone isolation verified by test, bounded ring buffers on both sides, and a clean pure-function diff. Findings 3 and 4 are latent blockers for Phase 2's global WS_ASSERT_ENABLED flip: stream bridge handlers that write to ring buffers (not TQ cache) will always appear as cache-unchanged drops, and the O(N) eviction will slow under heavy agent stream activity. Both are inexpensive to fix before Phase 2 lands.

@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @carlosflorencio's task in 6m 47s —— View job


Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments)
  • Read code-review skill and key changed files
  • Review new commits since last review pass (18c85a0, 0a89e72, 7566ce7)
  • Post inline findings (2 new inline comments)
  • Post findings report

Findings

Blocker

None.

Suggestion (5 total — 4 carried from prior passes, 1 new)

Carried from prior reviews (still not addressed):

  1. computeWsDrops false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:280

    • computeWsDropsAllStreams calls apiClient.getWsSent(feSnapshot.connectionId) with no sinceSeq. After clearWsAccount(), processedSeqs is [] but connectionId is preserved, so all pre-clear events return from the backend and diffWsAccount flags every one as dropped.
    • Fix: const sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined; then pass it to getWsSent. Fix this →
  2. time.Sleep in hub tests — now 5 calls instead of 3apps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

    • TestE2EWsSent_SessionFilter_ReturnsOnlyMatchingEvents (the new session-filter test) adds two more sleeps (lines 166, 178). All five are goroutine-sync sleeps that should be synctest.Wait(). (New inline comment at lines 166–178.)
  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts:120

    • Once the 5000-entry audit buffer fills (guaranteed under any agent shell session), every new entry triggers an O(5000) array shift. WsAccountBucket (added this PR) already solves this with a Map-keyed ring; the same pattern should be applied here.
  4. session.shell.output / session.process.output not in BRIDGE_SKIPPED_ACTIONSapps/web/lib/query/bridge/session-runtime-streams.ts:62,83

    • Both handlers are wrapped with wrapBridgeHandler but write to ring buffers (not the TQ cache), so every invocation records cacheChanged: false. session_id is non-null in both payloads, so diffBridgeAudit will flag every stream chunk as a cache-unchanged drop the moment WS_ASSERT_ENABLED=1 is enabled globally. Add both actions to BRIDGE_SKIPPED_ACTIONS in bridge/index.ts and bridge-audit-diff.ts (or don't wrap ring-buffer-only handlers with wrapBridgeHandler). terminal.output is fine — its payload has no session_id so the diff already skips it.

New from this review pass:

  1. Duplicated prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66
    • prependIntoQueryCache and prependIntoCache are identical in logic (dedupe by id, prepend older messages, update hasMore/oldestCursor). Any future change to merge strategy must be applied twice. Extract to a shared helper in lib/query/query-options/session.ts. (Inline comment posted.)

Architecture notes on new commits

The per-session sequencing logic (hub_session_seq.go, WsAccountBucket) is well-structured: sync.Map + atomic.Int64 for lock-free stamping, eager counter creation on subscribe, clean teardown at zero-subscriber count. The lifecycle regression test in hub_session_seq_test.go covers the subscribe → disconnect → drain path correctly.

use-session-messages.ts migration to useQuery is clean: SSR hydration feeds the initial cache, WS bridge writes message.added/message.updated into the same key, and the visibility/terminal-state invalidations are correct.


Summary

Severity Count
Blocker 0
Suggestion 5 (4 from prior passes + 1 new)

Verdict: Ready with suggestions. Findings 1 and 4 are latent blockers for the Phase 2 WS_ASSERT_ENABLED global flip — both will produce false-positive or false-negative failures in every agent-session E2E test the moment the flag is turned on.

Comment thread apps/backend/cmd/kandev/e2e_ws_sent_test.go Outdated
Comment thread apps/web/hooks/use-lazy-load-messages.ts Outdated
@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @carlosflorencio's task in 5m 50s —— View job


Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read code-review skill
  • Review new commit f1714ee (kanban consumers → useQuery)
  • Check status of prior open findings (5 still open from earlier passes)
  • Post inline findings (2 new inline comments)
  • Post findings report

Findings

Blocker

None.

Suggestion (7 total — 5 carried, 2 new)

Carried from prior reviews (all still unaddressed):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:280

    • getWsSent(feSnapshot.connectionId) with no sinceSeq returns all pre-clear events; after clearWsAccount(), processedSeqs is empty, so every pre-clear event is flagged as dropped.
    • Fix: sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined, pass it to getWsSent. Fix this →
  2. time.Sleep in hub tests — now 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

    • All five synchronise hub goroutines; adjacent hub_session_mode_test.go uses testing/synctest for the same hub. Fix: wrap in synctest.Test + replace sleeps with synctest.Wait(). Fix this →
  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts:120

    • Array.shift() is O(5000) once the buffer fills. WsAccountBucket already solves this with a Map-keyed ring. Fix: replace with a Map<number, BridgeAuditEntry> + monotonic counter identical to the WsAccount pattern. Fix this →
  4. Stream bridge handlers guaranteed cacheChanged: false — not in BRIDGE_SKIPPED_ACTIONSapps/web/lib/query/bridge/session-runtime-streams.ts:62,83

    • session.shell.output and session.process.output write to ring buffers (not TQ cache), so wrapBridgeHandler always records cacheChanged: false. Neither action is in BRIDGE_SKIPPED_ACTIONS. Phase 2's global WS_ASSERT_ENABLED=1 flip will immediately fail any agent-session test. Fix: add both to BRIDGE_SKIPPED_ACTIONS in bridge/index.ts and bridge-audit-diff.ts, or don't wrap ring-buffer-only handlers with wrapBridgeHandler. Fix this →
  5. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66

    • Identical merge-prepend logic in two hooks. Extract to a shared helper in lib/query/query-options/session.ts. Fix this →

New from this review pass (f1714ee):

  1. Missing isLoading = true testapps/web/hooks/use-tasks.test.ts:106

    • The 3 old isLoading = true tests were removed along with the Zustand-based loading logic. The new isLoading = !!workflowId && isFetching && !snapshot path has zero coverage. (Inline comment posted.)
  2. Migration docstrings that will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:16-19

    • useAllKanbanTasks's JSDoc explains the Zustand-to-TQ migration rationale ("Replaces direct reads of the Zustand slice…"), which is stale after the migration completes. Keep only the durable contract (what it returns, which cache key). (Inline comment posted.)

Architecture notes on f1714ee

The kanban consumer migration is clean. The three new hooks (useAllKanbanTasks, useActiveWorkflowSteps, useKanbanSnapshots) all subscribe to the same qk.kanban.multi() TQ entry — one fetch, shared by all subscribers. EMPTY_SNAPSHOTS as a module-level constant prevents reference churn on consumers. useTaskById's TQ-first-then-Zustand-fallback pattern is the right approach for tests that still seed Zustand directly.

One note: useRecentTaskBuildContext now calls all three hooks simultaneously (3 independent useQuery subscriptions to the same key). TQ deduplicates the fetch, but the three subscriber callbacks fire separately on each cache update. In practice React's batching absorbs this, and the data shape is correct.


Summary

Severity Count
Blocker 0
Suggestion 7 (5 carried + 2 new)

Verdict: Ready with suggestions. Findings 1 and 4 are latent blockers for Phase 2's WS_ASSERT_ENABLED flip.

Comment thread apps/web/hooks/use-tasks.test.ts
Comment thread apps/web/hooks/domains/kanban/use-kanban-tasks.ts Outdated
@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @carlosflorencio's task in 5m 9s —— View job


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read code-review skill and key changed files
  • Identify new commits since last review pass (c6b202a — office tasks list → useInfiniteQuery)
  • Review new changes
  • Post inline findings (3 new inline comments)
  • Post findings report

Findings

Blocker (must fix before merge)

New from c6b202a:

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:302

    • New exported utility in lib/ with no test. Every other lib utility in lib/query/ has a sibling test. The p.tasks ?? [] null guard is the one correctness-critical path worth pinning. Fix this →
  2. New tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

    • patchTasksKey now also invalidates tasksPaginated but the existing office.test.ts only asserts the flat tasks key is patched — the new behavior is uncovered. Fix this →

Suggestion (recommended, doesn't block)

  1. query.refetch() re-fetches every loaded pageapps/web/app/office/tasks/use-paginated-tasks.ts:126
    • TQ v5 refetches all pages sequentially on refetch(). With 5+ pages loaded and high WS event frequency, consider scoping to refetchPage: (_page, i) => i === 0 and letting the bridge's stale marking cover subsequent pages on next scroll.

Carried from prior passes (7 total — none yet addressed):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()ws-account.ts:280
  2. time.Sleep in hub tests (5 calls) — e2e_ws_sent_test.go:29,94,102,166,178
  3. auditBuffer.shift() O(N) eviction — bridge/index.ts:120
  4. session.shell.output/session.process.output not in BRIDGE_SKIPPED_ACTIONSsession-runtime-streams.ts:62,83
  5. Duplicate prependInto* cache helper — use-lazy-load-messages.ts:90 / use-session-messages.ts:66
  6. Missing isLoading = true test — use-tasks.test.ts:106
  7. Migration-context docstrings will rot — use-kanban-tasks.ts:19

Summary

Severity Count
Blocker 2 (new)
Suggestion 8 (1 new + 7 carried)

Verdict: Blocked — fix the two missing-test blockers. The useInfiniteQuery migration itself is clean; both blockers are inexpensive test-coverage gaps introduced in this commit.

Comment thread apps/web/lib/query/query-options/office.ts
Comment thread apps/web/lib/query/bridge/office.ts
Comment thread apps/web/app/office/tasks/use-paginated-tasks.ts
@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read code-review skill and key changed files
  • Identify new commits since last review pass (c6b202a — office tasks list → useInfiniteQuery)
  • Review new changes
  • Post inline findings (3 new inline comments)
  • Post findings report

Findings

Blocker (must fix before merge)

New from c6b202a:

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:302

    • flattenTasksPaginated is a new exported utility function in lib/query/query-options/office.ts with no corresponding test. Every other lib utility in lib/query/ has a sibling test (e.g. ring.test.ts, bridge-audit-diff.test.ts). Per convention, "new utility functions … must have *.test.ts tests". The p.tasks ?? [] null guard is the one correctness-critical path worth pinning. Fix this →
  2. New tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

    • patchTasksKey now also calls invalidateQueries({ queryKey: qk.office.tasksPaginated(wsId) }), but the existing bridge/__tests__/office.test.ts tests only assert that qk.office.tasks(WS_ID) is patched — none verify the paginated cache is marked stale. A future refactor that drops line 280 goes undetected. Fix this →

Suggestion (recommended, doesn't block)

New from c6b202a:

  1. query.refetch() re-fetches every loaded page — potentially chatty under high WS event volumeapps/web/app/office/tasks/use-paginated-tasks.ts:126
    • TQ v5: "When infinite queries are refetched, all pages are refetched sequentially." A user who loaded 5 pages and then receives a WS task event via useOfficeRefetch triggers 5 sequential API calls. The bridge's invalidateQueries also fires for the same event, which marks all pages stale and triggers its own background refetch. Under high event frequency with many loaded pages, consider scoping the useOfficeRefetch-driven refetch to only page 0 (refetchPage: (_page, i) => i === 0) and letting the bridge's stale marking handle subsequent pages on next scroll-to-load.

Carried from prior review passes (all still unaddressed — 7 total):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:280
  2. time.Sleep in hub tests — now 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178
  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts:120
  4. session.shell.output / session.process.output not in BRIDGE_SKIPPED_ACTIONSapps/web/lib/query/bridge/session-runtime-streams.ts:62,83
  5. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:90 and apps/web/hooks/domains/session/use-session-messages.ts:66
  6. Missing isLoading = true test in use-tasks.test.tsapps/web/hooks/use-tasks.test.ts:106
  7. Migration-context docstrings will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:19

Summary

Severity Count
Blocker 2 (new)
Suggestion 8 (1 new + 7 carried)

Verdict: Blocked — fix the two missing-test blockers first.

The useInfiniteQuery migration itself is clean: the tasksPaginated key is correctly isolated from the single-page tasks key, the flattenTasksPaginated helper is straightforward, and the Zustand transitional mirror is properly scoped with a removal note. Blockers 1 and 2 are both test-coverage gaps introduced in this commit and are inexpensive to close.

@claude
Copy link
Copy Markdown

claude Bot commented May 28, 2026

Claude finished @carlosflorencio's task in 3m 18s —— View job


Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Identify new commits since last review pass (84843ff — fix infinite render loop in useLazyLoadMessages)
  • Review new changes
  • Check status of prior open findings (9 still open from earlier passes)
  • Post inline findings (1 new inline comment)
  • Post findings report

Findings

Blocker (must fix before merge)

Carried from prior reviews (both still unaddressed):

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:307

    • flattenTasksPaginated is exported from lib/query/query-options/office.ts but has no corresponding test. Per convention, utility functions in lib/query/ must have *.test.ts sibling tests.
  2. New tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

    • patchTasksKey now invalidates qk.office.tasksPaginated(wsId) but the bridge test only verifies the flat qk.office.tasks key — the paginated invalidation is undetected by the test suite.

Suggestion (recommended, doesn't block)

New from 84843ff:

  1. No regression test for the infinite render loop fixapps/web/hooks/use-lazy-load-messages.test.ts:130 (inline comment posted)
    • The fix (event.type !== "updated" guard) is correct and well-commented, but no test pins it. A future removal of the guard would regress silently. Fix this →

Carried from prior reviews (all still unaddressed — 8 total):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:281

    • getWsSent(feSnapshot.connectionId) with no sinceSeq returns all pre-clear events; after clearWsAccount(), processedSeqs is empty, so every pre-clear event is flagged as dropped. Fix: sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined, pass to getWsSent.
  2. time.Sleep in hub tests — 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

    • All five synchronise hub goroutines; hub_session_mode_test.go (same package) uses testing/synctest for the same hub per AGENTS.md convention. Fix: wrap in synctest.Test + replace each sleep with synctest.Wait().
  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts:120

    • Array.shift() is O(5000) once the buffer fills. WsAccountBucket already uses a Map-keyed ring for O(1) FIFO eviction — the same pattern should replace auditBuffer.
  4. session.shell.output/session.process.output not in BRIDGE_SKIPPED_ACTIONSapps/web/lib/query/bridge/session-runtime-streams.ts:62,83

    • Both write to ring buffers (not TQ cache), so wrapBridgeHandler always records cacheChanged: false. Not in BRIDGE_SKIPPED_ACTIONS. Phase 2's WS_ASSERT_ENABLED=1 flip will immediately fail any agent-session test. Fix: add to BRIDGE_SKIPPED_ACTIONS in both bridge/index.ts and bridge-audit-diff.ts, or don't wrap ring-buffer-only handlers with wrapBridgeHandler.
  5. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66

    • Identical merge-prepend logic in two hooks. Extract to a shared helper in lib/query/query-options/session.ts.
  6. Missing isLoading = true testapps/web/hooks/use-tasks.test.ts:106

    • The old Zustand-based isLoading = true tests were removed; the new isFetching && !snapshot path has zero coverage.
  7. Migration-context docstrings will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:19

    • JSDoc describes the migration state, not the permanent contract. Per CLAUDE.md, drop migration-context prose and keep only durable facts.
  8. query.refetch() re-fetches every loaded pageapps/web/app/office/tasks/use-paginated-tasks.ts:126

    • TQ v5 refetches all pages sequentially on refetch(). Consider scoping to refetchPage: (_page, i) => i === 0 for WS-triggered refreshes.

Architecture note on 84843ff

The fix is correct and minimal. Filtering event.type !== "updated" in the getQueryCache().subscribe() callback prevents observerOptionsUpdated events (fired on every render when a new options object is passed to useQuery) from triggering setTick and looping. The comment on lines 101–104 explains the root cause clearly. The only gap is the missing regression test.


Summary

Severity Count
Blocker 2 (carried)
Suggestion 9 (1 new + 8 carried)

Verdict: Blocked — the two missing-test blockers from c6b202a are still open. The new commit is a correct one-line fix; the only new finding is a suggestion to pin it with a regression test.

Comment thread apps/web/hooks/use-lazy-load-messages.test.ts
@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @carlosflorencio's task in 8m 50s —— View job


Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read key changed files for both new commits (f82741b, 8f8d849)
  • Review f82741b — strip session_id from office.task.moved rebroadcast
  • Review 8f8d849 — close WS bridge-audit gaps for strict mode
  • Check status of all prior open findings
  • Post inline findings (2 new inline comments)
  • Post findings report

Resolved since last review pass

session.shell.output, session.process.output, terminal.output, session.process.status — now in BRIDGE_SKIPPED_ACTIONS in both files (prior finding #4).

✅ Control-plane ack false-positives — closed by both the structural type !== "notification" skip in diffBridgeAudit and the expanded static allowlist.

office.task.moved session_id leakage — stripPayloadKeys + subscribeWithout drops session-scoped fields before workspace-scoped rebroadcast (f82741b), with clone-isolation tests.

applyIfNewer primary-session bug — mergePrimaryFields now folds primary-session fields even on same-timestamp events; regression test in kanban.test.ts.

turn.completed handler — explicit if (prev) { setQueryData } else { invalidateQueries } replaces the silent no-op on empty cache.


Findings

Blocker (2 carried — still unaddressed)

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:307
  2. tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

Suggestion (2 new + 8 carried)

  1. BRIDGE_SKIPPED_ACTIONS duplicated across two 40+ entry sets with no equality testapps/web/lib/query/bridge-audit-diff.ts:31 (inline comment posted)
  2. No test for the type !== "notification" structural skip in diffBridgeAuditapps/web/lib/query/bridge-audit-diff.ts:233 (inline comment posted)
    5–12. Carried: computeWsDropsAllStreams sinceSeq gap, time.Sleep in hub tests (×5), auditBuffer.shift() O(N), duplicate prependInto*, missing isLoading=true test, rotting docstrings, refetch() all pages, no render-loop regression test.

Summary

Severity Count
Blocker 2 (carried)
Suggestion 10 (2 new + 8 carried)

Verdict: Blocked — the two missing-test blockers from c6b202a remain open. 8f8d849 is architecturally sound; findings 3–4 are pre-emptive gaps worth closing before WS_ASSERT_ENABLED=1 is flipped globally.

Comment thread apps/web/lib/query/bridge-audit-diff.ts
Comment thread apps/web/lib/query/bridge-audit-diff.ts
@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read key changed files for both new commits
  • Review f82741b — strip session_id from office.task.moved rebroadcast
  • Review 8f8d849 — close WS bridge-audit gaps for strict mode
  • Check status of all prior open findings
  • Post inline findings (2 new inline comments)
  • Post findings report

Resolved since last review pass

Finding #4 (blocker-level for Phase 2): session.shell.output, session.process.output, terminal.output, and session.process.status are now in BRIDGE_SKIPPED_ACTIONS in both bridge/index.ts and bridge-audit-diff.ts.

Control-plane ack false-positives: The 8f8d849 commit adds two complementary fixes: (a) a structural if (event.type && event.type !== "notification") continue skip in diffBridgeAudit — responses are dispatched via pendingRequests, never ws.on(), and the type field in WsAccountReceivedEvent now lets the diff skip them structurally; and (b) an exhaustive static allowlist for all request/response action families, as belt-and-suspenders for envelopes where type is empty (pre-Phase 2 backend).

office.task.moved session_id leakage (f82741b): stripPayloadKeys + subscribeWithout cleanly drops the session_id field before re-broadcasting workspace-scoped notifications. Tests cover clone isolation, no-keys pass-through, and non-map pass-through.

applyIfNewer primary-session bug (8f8d849): SetSessionPrimary doesn't bump the task row's updated_at, so a same-timestamp task.updated was silently dropped. The new mergePrimaryFields function folds the authoritative primary fields even on stale events. Regression test added in kanban.test.ts.

turn.completed handler fallback (8f8d849): The callback form setQueryData(key, (prev) => ...) that returned undefined when the cache was empty is replaced with an explicit if (prev) { setQueryData } else { invalidateQueries } pattern. Empty-cache session wakeups now trigger a refetch instead of silently no-oping.


Findings

Blocker (must fix before merge — both carried from c6b202a, still unaddressed)

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:307

  2. New tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

Suggestion (recommended, doesn't block)

New from this review pass (2 inline comments posted):

  1. BRIDGE_SKIPPED_ACTIONS is now a 40+ entry set duplicated across two files with no cross-file testapps/web/lib/query/bridge-audit-diff.ts:31

    • bridge/index.ts is the declared source-of-truth but is exported-and-never-imported; bridge-audit-diff.ts has the set that actually runs. Manual sync across 40+ entries is fragile. The comment claims "e2e will surface a divergence" — only in one direction (index.ts → audit-diff.ts). Fix: add a cross-file equality test in bridge-audit-diff.test.ts, or import from bridge/index.ts directly and remove the copy.
  2. No test for the type !== "notification" structural skip in diffBridgeAuditapps/web/lib/query/bridge-audit-diff.ts:233

    • The snapshot() test helper never sets type, so all test events hit the back-compat path. Removing the event.type check in a future refactor would be invisible to the test suite. Fix: add one test with type: "response" / type: "error" events that verifies they're skipped without being in BRIDGE_SKIPPED_ACTIONS.

Carried from prior reviews (all still unaddressed — 8 total):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:281

  2. time.Sleep in hub tests — 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts:251

  4. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66

  5. Missing isLoading = true testapps/web/hooks/use-tasks.test.ts

  6. Migration-context docstrings will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:19

  7. query.refetch() re-fetches every loaded pageapps/web/app/office/tasks/use-paginated-tasks.ts:126

  8. No regression test for infinite render loop fixapps/web/hooks/use-lazy-load-messages.test.ts


Summary

Severity Count
Blocker 2 (carried)
Suggestion 10 (2 new + 8 carried)

Verdict: Blocked — the two missing-test blockers from c6b202a remain open.

The 8f8d849 commit is architecturally sound. The dual approach (structural type skip + exhaustive static allowlist) is a solid Phase 2 foundation, and the applyIfNewer primary-session fix is a meaningful correctness improvement. New findings 3 and 4 are pre-emptive gaps that are inexpensive to close before WS_ASSERT_ENABLED=1 is flipped globally.

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @carlosflorencio's task in 3m 33s —— View job


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read code-review skill and key changed files
  • Review new commit f429612 (settle WS drop checks to absorb in-flight events)
  • Check status of prior open findings (2 blockers + 9 suggestions carried)
  • Post inline findings (0 new — no independent issues found in f429612)
  • Post findings report

Findings

Blocker (must fix before merge — both carried from c6b202a, still unaddressed)

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:307

    • flattenTasksPaginated is exported from lib/query/query-options/office.ts but has no corresponding test. Every other lib utility in lib/query/ has a sibling test. Per convention, utility functions in lib/query/ must have *.test.ts tests.
  2. New tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

    • patchTasksKey now invalidates qk.office.tasksPaginated(wsId) but bridge/__tests__/office.test.ts only verifies the flat qk.office.tasks key — the paginated invalidation path is not covered.

Suggestion (recommended, doesn't block)

Note on f429612:

settleDrops is conceptually sound — a transient in-flight drop clears on the next read; a real drop persists for all 12 retries. The "clean first read = immediate return" property means zero added latency in the passing case. One interaction concern: the unresolved sinceSeq gap (prior finding #1 below) means that any test using clearWsAccount() will have drops that never clear (pre-clear events can never re-enter the FE ring), causing settleDrops to exhaust all 12 × 200ms = 2.4s retries before reporting what are still false positives. This amplifies the latency cost of the existing issue but is not a new independent finding.

Carried from prior reviews (all still unaddressed — 9 total):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:281

    • getWsSent(feSnapshot.connectionId) with no sinceSeq returns all pre-clear events. After clearWsAccount(), processedSeqs is empty but connectionId is preserved, so every pre-clear event is flagged as dropped. With settleDrops now wrapping the check, this causes a 2.4s teardown penalty before reporting the false positive.
    • Fix: sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined, pass to getWsSent.
  2. time.Sleep in hub tests — 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

    • All five synchronise hub goroutines; hub_session_mode_test.go (same package) uses testing/synctest for the same hub per AGENTS.md convention. Fix: wrap in synctest.Test + replace each sleep with synctest.Wait().
  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts:251

    • Array.shift() is O(5000) once the buffer fills. WsAccountBucket already uses a Map-keyed ring for O(1) FIFO eviction — the same pattern should replace auditBuffer.
  4. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66

    • Identical merge-prepend logic in two hooks. Extract to a shared helper in lib/query/query-options/session.ts.
  5. Missing isLoading = true testapps/web/hooks/use-tasks.test.ts

    • The old Zustand-based isLoading = true tests were removed; the new isFetching && !snapshot path has zero coverage.
  6. Migration-context docstrings will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:19

    • JSDoc describes the migration state, not the permanent contract. Drop migration-context prose; keep only durable facts.
  7. query.refetch() re-fetches every loaded pageapps/web/app/office/tasks/use-paginated-tasks.ts:126

    • TQ v5 refetches all pages sequentially on refetch(). Consider scoping to refetchPage: (_page, i) => i === 0 for WS-triggered refreshes.
  8. No regression test for infinite render loop fixapps/web/hooks/use-lazy-load-messages.test.ts

    • The event.type !== "updated" guard is unverified — a future removal would regress silently.
  9. BRIDGE_SKIPPED_ACTIONS duplicated across two files with no equality testapps/web/lib/query/bridge-audit-diff.ts:31

    • Both bridge/index.ts and bridge-audit-diff.ts now carry 40+ entry sets manually synced. Fix: add a cross-file equality test in bridge-audit-diff.test.ts, or import from bridge/index.ts directly.
  10. No test for the type !== "notification" structural skip in diffBridgeAuditapps/web/lib/query/bridge-audit-diff.ts:233

    • snapshot() helper never sets type, so all test events hit the back-compat path. Removing the event.type check in a future refactor would be invisible to the test suite.

Summary

Severity Count
Blocker 2 (carried)
Suggestion 10 (9 carried + settleDrops/sinceSeq amplification noted)

Verdict: Blocked — the two missing-test blockers from c6b202a remain open.

f429612 itself is correct and well-commented. The settleDrops polling approach is sound for absorbing genuinely in-flight events — the only new concern is that it amplifies teardown latency for the existing sinceSeq false-positive case (suggestion #1). Closing the two blockers is the prerequisite for merge.

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @carlosflorencio's task in 3m 9s —— View job


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read key changed files for new commit (d86e613)
  • Review d86e613 — count invalidation as bridge handling; allowlist file.changes
  • Check status of all prior open findings (2 blockers + 10 suggestions)
  • Post inline findings (0 new — no independent issues found in d86e613)
  • Post findings report

Claude finished reviewing PR #1130


Resolved in d86e613

False-negative for invalidation-only handlers — The old wrapBridgeHandler subscribed to getQueryCache() and counted TQ mutation notifications. invalidateQueries on a key with no cached entry produces zero notifications → the handler was classified as cacheChanged: false even though it correctly handled the event. The new approach spies on qc.invalidateQueries, qc.setQueryData, setQueriesData, removeQueries, and clear directly, counting call invocations regardless of whether matching cache entries exist. This is correct: the void qc.invalidateQueries(...) pattern fires the spy counter synchronously at call time, before async work starts, and the finally restoration runs after the synchronous handler completes. Regression test added.

session.workspace.file.changes allowlisted — Added to BRIDGE_SKIPPED_ACTIONS in both bridge/index.ts and bridge-audit-diff.ts with an accurate comment explaining it routes through a component-level client.on() subscription in file-browser-hooks.ts, not a bridge handler.


Findings

Blocker (must fix before merge — both carried from c6b202a, still unaddressed)

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:302

    • flattenTasksPaginated is exported with no sibling test file. Every other lib utility in lib/query/ has a *.test.ts counterpart.
  2. tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

    • patchTasksKey invalidates qk.office.tasksPaginated(wsId) but bridge/__tests__/office.test.ts asserts only the flat qk.office.tasks key. The paginated invalidation path is invisible to the test suite.

Suggestion (recommended, doesn't block)

Carried from prior reviews (all still unaddressed — 10 total):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:281

    • getWsSent(feSnapshot.connectionId) with no sinceSeq returns all pre-clear events. After clearWsAccount(), processedSeqs is empty but connectionId is preserved, so every pre-clear event is flagged as dropped. With settleDrops wrapping the check, this causes a 2.4s teardown penalty before reporting the false positive.
  2. time.Sleep in hub tests — 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

    • All five synchronise hub goroutines; hub_session_mode_test.go uses testing/synctest for the same hub per AGENTS.md convention.
  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts

    • Array.shift() is O(5000) once the buffer fills. WsAccountBucket already uses a Map-keyed ring for O(1) FIFO eviction — same pattern should replace auditBuffer.
  4. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66

  5. Missing isLoading = true testapps/web/hooks/use-tasks.test.ts

  6. Migration-context docstrings will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:19

  7. query.refetch() re-fetches every loaded pageapps/web/app/office/tasks/use-paginated-tasks.ts:126

  8. No regression test for infinite render loop fixapps/web/hooks/use-lazy-load-messages.test.ts

  9. BRIDGE_SKIPPED_ACTIONS duplicated across two files with no equality testapps/web/lib/query/bridge-audit-diff.ts:31

    • d86e613 adds the new entry consistently to both files, but the dual-maintenance problem persists. The "e2e will surface a divergence" comment still only catches divergence in one direction.
  10. No test for the type !== "notification" structural skip in diffBridgeAuditapps/web/lib/query/bridge-audit-diff.ts:237


Summary

Severity Count
Blocker 2 (carried)
Suggestion 10 (all carried)

Verdict: Blocked — the two missing-test blockers from c6b202a remain open.

d86e613 is a correct and well-targeted fix. The method-spy approach for wrapBridgeHandler is the right solution to the false-negative problem: it captures handler intent (the call to invalidateQueries) independently of whether matching cache entries exist. The implementation is safe because all bridge handlers are synchronous — the spy count is captured before async work starts, and the method restoration in finally runs after the synchronous call completes.

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read key changed files for new commits
  • Review new commits since last review pass
  • Check status of all prior open findings
  • Post inline findings
  • Post findings report

View job run

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @carlosflorencio's task in 4m 38s —— View job


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read key changed files for new commits (3b43cff, c982389, 683133f)
  • Review new commits since last review pass
  • Check status of all prior open findings (2 blockers + 10 suggestions)
  • Post inline findings (1 new inline comment)
  • Post findings report

Claude finished reviewing PR #1130


Resolved in 683133f

applyIfNewer workflow step/state drop — A workflow transition (e.g. In Progress → Review on turn-complete) can arrive in the same wall-clock second as the prior cached updated_at. The <= guard read this as stale and silently dropped the new step/state, stranding the kanban stepper on the old step. The fix adds workflowStepId and state to the server-authoritative fold-through path in mergeAuthoritativeFields alongside the existing primary-session fields. Tests added: same-timestamp step change, strictly-older step change, empty-step guard, same-timestamp state change, bridge-level regression test.

Findings

Blocker (must fix before merge — both carried from c6b202a, still unaddressed)

  1. flattenTasksPaginated is an untested lib utilityapps/web/lib/query/query-options/office.ts:302

    • flattenTasksPaginated is exported with no sibling test file. Every other lib utility in lib/query/ has a *.test.ts counterpart. Per convention, utility functions in lib/query/ must have *.test.ts tests.
  2. tasksPaginated invalidation in patchTasksKey has no testapps/web/lib/query/bridge/office.ts:280

    • patchTasksKey invalidates qk.office.tasksPaginated(wsId) but bridge/__tests__/office.test.ts asserts only the flat qk.office.tasks key. The paginated invalidation path is invisible to the test suite.

Suggestion (recommended, doesn't block)

New from 683133f (1 inline comment posted):

  1. Missing analog guard test for state: undefinedapps/web/lib/query/bridge/__tests__/kanban.test.ts:136
    • workflowStepId has a dedicated "does not let a stale event with empty value clear a real step" test. state has no equivalent — a stale event with state: undefined not clobbering a real state is untested. The next.state !== undefined guard handles it correctly, but without a pinning test a future refactor would be invisible.

Carried from prior reviews (all still unaddressed — 10 total):

  1. computeWsDropsAllStreams false-positives after clearWsAccount()apps/web/e2e/helpers/ws-account.ts:281

    • getWsSent(feSnapshot.connectionId) with no sinceSeq returns all pre-clear events. After clearWsAccount(), processedSeqs is empty but connectionId is preserved, so every pre-clear event is flagged as dropped. Now that KANDEV_E2E_WS_ASSERT=1 is on globally, the first spec that uses clearWsAccount() will hit a 2.4s teardown penalty before reporting false positives.
    • Fix: sinceSeq = feSnapshot.minSeq !== null ? feSnapshot.minSeq - 1 : undefined, pass to getWsSent.
  2. time.Sleep in hub tests — 5 calls totalapps/backend/cmd/kandev/e2e_ws_sent_test.go:29,94,102,166,178

  3. auditBuffer.shift() O(N) evictionapps/web/lib/query/bridge/index.ts

  4. Duplicate prependInto* cache helperapps/web/hooks/use-lazy-load-messages.ts:74 and apps/web/hooks/domains/session/use-session-messages.ts:66

  5. Missing isLoading = true testapps/web/hooks/use-tasks.test.ts

  6. Migration-context docstrings will rotapps/web/hooks/domains/kanban/use-kanban-tasks.ts:19

  7. query.refetch() re-fetches every loaded pageapps/web/app/office/tasks/use-paginated-tasks.ts:126

  8. No regression test for infinite render loop fixapps/web/hooks/use-lazy-load-messages.test.ts

  9. BRIDGE_SKIPPED_ACTIONS duplicated across two files with no equality testapps/web/lib/query/bridge-audit-diff.ts:31

  10. No test for type !== "notification" structural skip in diffBridgeAuditapps/web/lib/query/bridge-audit-diff.ts:237


Architecture notes on new commits

683133f (workflow step/state fold-through): The fix is correct. The authoritative-field set now covers the three independently-triggered server broadcasts that can arrive without a timestamp bump: SetSessionPrimary (no updated_at bump), same-second workflow transition on turn-complete, and direct state change. The guards are correctly asymmetric — workflowStepId uses a truthy check (next.workflowStepId && ...) to block both undefined and "" (which toKanbanTask maps for a missing step); state uses !== undefined (correct because TaskState is a typed union that excludes "").

c982389 (per-session WS spec rewrite): The new strategy (navigate first → drive live sendMessage → assert bucket) is cleaner and more deterministic than polling two completed sessions. The distinct liveReply marker string avoids the nth(1) race that made the old approach fragile under worker contention.

3b43cff (enable KANDEV_E2E_WS_ASSERT=1 in CI): Milestone — strict mode is now on for all chromium/mobile shards. The plan.md update confirms a 1022-test local run passed with zero drops after the recent fixes. The containers project caveat is correctly documented. This makes the two test-coverage blockers more urgent — they should land before this PR merges.


Summary

Severity Count
Blocker 2 (carried)
Suggestion 11 (1 new + 10 carried)

Verdict: Blocked — the two missing-test blockers from c6b202a remain open. The three new commits are all correct: the mergeAuthoritativeFields expansion is well-reasoned and tested, the E2E spec rewrite is sound, and enabling strict-mode accounting in CI is the right milestone to land with this PR.

Additive D4+D6 Stage 1 infra: qk.taskSession.byId key, sessionByIdQueryOptions
(observe-only), bridge/session-state.ts mirroring session.state_changed /
agentctl_starting / agentctl_ready / agentctl_error into TQ by-id + by-task
caches, SSR seed in StateHydrator, and D6 Zustand side-effects (env mapping +
prepare-progress) kept populated in parallel during the staged migration.
Zustand slices are not deleted yet (Phase A additive).
…nd mirror

Remove `worktrees` / `sessionWorktreesBySessionId` Zustand slices and their
setters (`setWorktree` / `setSessionWorktrees`). Rewrite `useSessionWorktrees`
and `useWorktree` to derive worktree view-models from `qk.taskSession.byId`
(worktree_id / worktree_path / worktree_branch fields), eliminating the
agentctl_ready WS handler that kept the mirror in sync.
@carlosflorencio carlosflorencio force-pushed the feature/ws-event-accounting branch from a6595c3 to 50ee027 Compare June 1, 2026 15:26
@claude
Copy link
Copy Markdown

claude Bot commented Jun 1, 2026

Claude finished @carlosflorencio's task in 7m 26s —— View job


Claude finished reviewing PR #1130


Tasks

  • Gather context (PR description, prior review comments, new commits)
  • Read code-review skill and key changed files
  • Identify new commits since last review pass (50ee027, a2eca95)
  • Review new changes
  • Check status of prior open findings (3 suggestions carried)
  • Post inline findings (4 new inline comments)
  • Post findings report

Resolved in this batch

Voice-mode TQ migration (50ee027): All reads of userSettings.voiceMode in voice-mode-settings.tsx, voice-input-button.tsx, and use-voice-input.ts now use useUserSettings().data?.voiceMode ?? DEFAULT_VOICE_MODE_STATE from the TQ cache. VoiceModeState + DEFAULT_VOICE_MODE_STATE moved to lib/types/settings.ts. The settings bridge's applyUserSettingsUpdate now calls parseVoiceMode(p.voice_mode). The deleted settings-slice.ts and settings/types.ts have zero remaining imports. Two new test blocks in settings.test.ts cover the full voice_mode payload mapping and the null-payload default path.

Idle-input + terminal-busy guards (a2eca95): Rebased version of previously reviewed a6595c3 — same isStaleSessionStateEvent export and bridge guard, no new content.


Findings

Blocker (must fix before merge)

  1. useTaskCandidates() is wrong data source for LinkedEntity — tasks outside top 50 silently show "Task"apps/web/app/office/agents/[id]/runs/runs-list-view.tsx:160 (inline comment posted)
    • useTaskCandidates() calls searchTasks(wsId, "", 50) — a picker endpoint capped at 50 results. LinkedEntity is a display cell resolving ANY historical run's linked task. Any run linked to a task outside the top 50 search results will silently show "Task" (no title, no identifier). The original code used s.office.tasks.items (the full listTasks result).
    • Fix: Use useQuery({ ...officeQueryOptions.tasks(workspaceId ?? ""), enabled: !!workspaceId }) — this reads the same qk.office.tasks cache the tasks list page already populates, zero extra fetch. Fix this →

Suggestion (recommended, doesn't block)

  1. Seq counter deleted when subscriber count → 0 but focus clients still receive session eventsapps/backend/internal/gateway/websocket/hub_session_seq.go:66 (inline comment posted)

    • decSessionSubscribers deletes sessionSeqs when ref-count → 0. But BroadcastToSession routes to focus clients AND subscribers (getSessionRecipientsLocked). When subscriber count transiently drops to 0 during task-switch (the scenario documented in the getSessionRecipientsLocked comment) while focus is still held, the counter is deleted then re-created at 0 on the next event. The FE's WsAccountBucket had seen seq N and now gets seq 1 → false gap, potential WS_ASSERT failure.
    • Fix: Only delete sessionSeqs when BOTH subscriber count AND focus-client count are zero. Check from recomputeSessionMode (already holds the hub lock) rather than decSessionSubscribers.
  2. isPending on a disabled query causes infinite spinner when workspaceId is emptyapps/web/app/office/agents/[id]/layout.tsx:53 (inline comment with code suggestion posted)

    • When workspaceId is null/"", enabled: false and TanStack Query sets isPending: true for never-fetched disabled queries. Fix: if (workspaceId && isPending) or replace with isLoading.
  3. Missing retry: false for SSR feature-flag prefetchapps/web/app/layout.tsx:54 (inline comment with code suggestion posted)

    • TQ's default retry: 3 applies here. If the backend is unreachable during SSR, layout rendering stalls through retries before falling back to defaults. Add retry: false.

Carried from prior reviews (3 suggestions, all still unaddressed):

  1. invalidateCumulativeDiffCache unconditional in Zustand handlerapps/web/lib/ws/handlers/git-status.ts:51 (inline comment already on diff)

  2. mergeSyntheticMessages appends at end — wrong position for multi-turn windowsapps/web/lib/query/query-options/session.ts:103 (inline comment already on diff)

  3. useWorktree has no call sites — dead exportapps/web/hooks/use-worktree.ts:14 (inline comment already on diff)


Summary

Severity Count
Blocker 1 (new)
Suggestion 6 (3 new + 3 carried)

Verdict: Blocked — finding #1 (useTaskCandidates in LinkedEntity) is a data-correctness regression that silently degrades the agent runs view for workspaces with >50 tasks. It's a one-line hook swap. The 6 suggestions are pre-Phase-2 hardening items.

const routine = useAppStore((s) =>
run.routine_id ? s.office.routines.find((r) => r.id === run.routine_id) : undefined,
);
const candidates = useTaskCandidates();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useTaskCandidates() is the wrong data source for a display component — tasks outside the 50-result window silently show "Task"

useTaskCandidates() calls searchTasks(wsId, "", 50) — a picker endpoint capped at 50 results. The hook's own docstring says: "The pickers don't need the full paginated/filtered list." LinkedEntity is not a picker — it's a display cell that resolves any historical run's linked task. Any run referencing a task ranked >50 in the search will silently fall back to displaying "Task" (line 194 in the current file) instead of the task's identifier and title.

The original code used s.office.tasks.items which was populated from listTasks (the full workspace task list). Replace with the equivalent TQ read:

const workspaceId = useAppStore((s) => s.workspaces.activeId);
const { data: allTasks = [] } = useQuery({
  ...officeQueryOptions.tasks(workspaceId ?? ""),
  enabled: !!workspaceId,
});
const task = run.task_id ? allTasks.find((t) => t.id === run.task_id) : undefined;

officeQueryOptions.tasks(wsId) is already subscribed by the tasks list page, so this is a zero-cost cache read on any page that mounts after the office layout has loaded. Drop the useTaskCandidates import from this file.

n := v.(*atomic.Int64).Add(-1)
if n <= 0 {
h.sessionSubscriberCounts.Delete(sessionID)
h.sessionSeqs.Delete(sessionID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seq counter deleted when subscriber count → 0 but focus clients still receive session events

decSessionSubscribers deletes sessionSeqs when the ref-count reaches zero. But BroadcastToSession routes events to focus clients AND subscribed clients (see getSessionRecipientsLocked). If a client is focused but has unsubscribed (the "subscriber count churns to 0 during task-switch/resume" case documented in the getSessionRecipientsLocked comment), the seq counter is deleted here, then lazily re-created at 0 in nextSessionSeq. The first post-deletion event arrives with session_seq: 1, but the FE's WsAccountBucket already holds entries with higher seqs — producing a false gap that KANDEV_E2E_WS_ASSERT=1 will flag.

Fix: only delete the seq counter when no focus clients remain for the session. Since decSessionSubscribers is called outside the hub lock, the simplest approach is to skip the sessionSeqs.Delete here and instead delete it from recomputeSessionMode once both subscriber count and focus client count are zero:

if n <= 0 {
    h.sessionSubscriberCounts.Delete(sessionID)
    // sessionSeqs is deleted from recomputeSessionMode once both
    // subscriber count AND focus-client count are zero — deleting here
    // would reset the seq for focus-only recipients, producing false gaps.
}

Then in recomputeSessionMode (where you already hold h.mu), after confirming both maps are empty for sessionID, call h.sessionSeqs.Delete(sessionID).

const agent = agents.find((a) => a.id === id);
const activeSlug = activeSlugFromPath(pathname, id);

if (isPending) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isPending on a disabled query shows an infinite spinner when workspaceId is empty

When workspaceId is null/"", the query is enabled: false. TanStack Query sets isPending: true for queries that are disabled and have never fetched (the query has no cached data). The if (isPending) guard therefore renders the spinner forever for any user whose workspaceId is not yet available.

Use isLoading instead (isPending && fetchStatus === "fetching") or gate on workspaceId:

Suggested change
if (isPending) {
if (workspaceId && isPending) {

isLoading is equivalent and safe here because it's false when the query is disabled.

Comment thread apps/web/app/layout.tsx
// to all-off when the backend is unreachable. See
// docs/decisions/0007-runtime-feature-flags.md.
const ssrQueryClient = makeQueryClient();
await ssrQueryClient.prefetchQuery(featuresQueryOptions());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Add retry: false for the SSR feature-flag prefetch

TanStack Query's default retry: 3 applies to SSR prefetchQuery calls. If the backend is unreachable, this blocks layout rendering for multiple retry attempts before surfacing the default feature flags. Feature flags should fail fast to defaults during SSR so a backend blip doesn't degrade page load time.

Suggested change
await ssrQueryClient.prefetchQuery(featuresQueryOptions());
await ssrQueryClient.prefetchQuery({ ...featuresQueryOptions(), retry: false });

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