Skip to content

refactor(wework): centralize runtime task lifecycle - #2211

Merged
qdaxb merged 17 commits into
mainfrom
fix/wework-runtime-task-status
Jul 25, 2026
Merged

qdaxb merged 17 commits into
mainfrom
fix/wework-runtime-task-status

Conversation

@qdaxb

@qdaxb qdaxb commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

What

  • introduce one user-scoped RuntimeTaskLifecycleStore that owns all task lifecycle machines and routes executor, stream, Goal, optimistic-send, stop, read, rename, and archive events
  • make each RuntimeTaskMachine the aggregate root for execution, turn, Goal, and unread state; keep the reducer private to the machine implementation
  • keep RuntimeTaskLifecycleProvider as a strict React subscription adapter with no fallback Store or local execution cache
  • migrate the sidebar, desktop/mobile composer, message thinking state, tray, close guard, debug panel, and completion reminders to the same lifecycle snapshot
  • remove the old Workbench reducer running sets, pending-settled cache, pane sendPhase, reminder-owned ongoing set, and legacy runtime status helper
  • add a static architecture boundary check that rejects old state paths, raw task-running interpretation outside lifecycle ingestion, and reducer usage outside the state machine
  • document the executor/frontend authority contract in Chinese and English

Why

Runtime state previously existed in several frontend layers. The task list, pane, optimistic send path, stream callbacks, reminders, and tray could each retain or infer a slightly different answer. That made active-Goal gaps, stale snapshots, background completion, and renderer/executor restart disagree in user-visible ways.

The final authority chain is:

  1. The executor process active-task set is authoritative for actual execution.
  2. One frontend task state machine combines executor snapshots with ordered local lifecycle events.
  3. The Store owns all machines and publishes one derived multi-task snapshot.
  4. React only subscribes to that Store.

Execution state is memory-only. Optimistic starting is maintained by the same machine and converges on explicit executor state. Only unread completion reminders persist; they never reconstruct execution.

User-visible contract

  • Running turn: sidebar running indicator, pause control, and thinking feedback agree; no unread dot.
  • Active Goal between turns: task remains running while the executor still owns it, but the turn is idle, so no thinking feedback and no unread dot.
  • Active Goal automatic continuation: all running surfaces become active again from the same lifecycle state.
  • Background completion: running clears and unread appears.
  • Reopening a completed task: unread clears and the composer returns to send.
  • Wework/executor restart: a persisted active Goal remains visible, but running=false from the new executor process means idle; it does not auto-resume. Explicit user input starts a new run.

Cleanup evidence

  • pnpm --dir wework lint:task-lifecycle
    • scans 665 TypeScript/TSX source files
    • rejects every removed legacy identifier
    • rejects raw task/runtimeTask/summary/transcript.running interpretation outside the lifecycle aggregate and transport ingestion
    • rejects reducer calls outside reducer.ts and RuntimeTaskMachine.ts
  • deleted:
    • runtimeTaskStatus.ts
    • reminder-owned runtime transition tests and implementation
    • Workbench reducer runtime-running and pending-settled state
    • pane-local optimistic send phase
  • the public lifecycle index does not export the reducer or mutable machine state
  • the React context throws without the Provider instead of silently creating another Store

Validation

  • pnpm --dir wework lint
  • pnpm --dir wework typecheck
  • pnpm --dir wework test — 215 files / 2127 tests passed
  • node wework/e2e/desktop/task-flow.e2e.mjs --goal-idle-only
  • node wework/e2e/desktop/task-flow.e2e.mjs --goal-restart-only
  • cargo test --locked in executor — complete suite passed
  • pre-push: Wework ESLint, TypeScript, unit tests; Executor library tests, fmt, and Clippy passed

The two desktop E2E journeys use real backend/runtime requests and verify the sidebar, composer, message thinking state, Goal state, unread state, background completion, full Wework/executor restart, non-automatic recovery, and explicit user resume together.

Summary by CodeRabbit

  • New Features
    • Unified task “running” and unread behavior across sidebar, composer, message thinking indicator, system tray, and close protection.
    • Active Goal tasks stay in a visible running state during automatic continuation gaps.
    • Goal status is now surfaced more consistently after execution, recovery, and thread resume.
  • Bug Fixes
    • Prevents stale executor refreshes and older streaming events from incorrectly re-enabling running/thinking indicators.
    • Improved cancellation, restart, and attachment-only flows so lifecycle indicators remain accurate across desktop and mobile.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR centralizes runtime task execution, turn, goal, and unread state in RuntimeTaskLifecycleStore, updates workbench and UI consumers to use lifecycle snapshots, propagates Codex goal status through executor responses, and expands desktop, unit, integration, and documentation coverage.

Runtime lifecycle centralization

Layer / File(s) Summary
Lifecycle model and store
wework/src/features/workbench/runtimeTaskLifecycle/*
Adds lifecycle phases, reducer transitions, task machines, store snapshots, React accessors, unread persistence, and behavioral tests.
Workbench and UI integration
wework/src/features/workbench/*, wework/src/components/*, wework/src/tauri/*, wework/src/lib/*
Routes messaging, transcripts, pane status, reminders, task lists, tray groups, close protection, and debug state through lifecycle snapshots.
Executor goal propagation
executor/src/agents/*, executor/src/runtime_work/*
Tracks observed goal status through Codex turns, persists it on runtime tasks, exposes goalStatus, and validates archive unsubscribe behavior.
E2E, tooling, and documentation
wework/e2e/*, docs/*, wework/scripts/*, wework/package.json
Adds Goal-idle and Goal-restart coverage, process shutdown checks, lifecycle boundary linting, and updated lifecycle contracts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: centralizing WeWork runtime task lifecycle state.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/wework-runtime-task-status

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.

@qdaxb
qdaxb marked this pull request as ready for review July 24, 2026 21:26
@qdaxb

qdaxb commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 3

🧹 Nitpick comments (4)
wework/e2e/desktop/task-flow.e2e.mjs (2)

1163-1176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse into waitForLogPattern with an offset parameter.

This is waitForLogPattern (Line 1153) plus a slice offset; the two will drift. A default of 0 keeps every existing call site unchanged.

As per coding guidelines, "Before adding code, search for and reuse existing components, services, utilities, and patterns; extract shared logic instead of duplicating it."

♻️ Single helper
-async function waitForLogPattern(logPath, pattern, timeoutMs = UI_TIMEOUT_MS) {
+async function waitForLogPattern(
+  logPath,
+  pattern,
+  { fromOffset = 0, timeoutMs = UI_TIMEOUT_MS } = {}
+) {
   const startedAt = Date.now()
   while (Date.now() - startedAt < timeoutMs) {
     const content = await readFile(logPath, 'utf8').catch(() => '')
-    if (pattern.test(content)) return content
+    if (pattern.test(content.slice(fromOffset))) return content
     await new Promise(resolvePromise => setTimeout(resolvePromise, 100))
   }
-  throw new Error(`Timed out waiting for ${pattern} in ${logPath}`)
+  throw new Error(`Timed out waiting for ${pattern} in ${logPath} after offset ${fromOffset}`)
 }
-
-async function waitForNewLogPattern(
-  logPath,
-  pattern,
-  previousContentLength,
-  timeoutMs = UI_TIMEOUT_MS
-) {
-  const startedAt = Date.now()
-  while (Date.now() - startedAt < timeoutMs) {
-    const content = await readFile(logPath, 'utf8').catch(() => '')
-    if (pattern.test(content.slice(previousContentLength))) return content
-    await new Promise(resolvePromise => setTimeout(resolvePromise, 100))
-  }
-  throw new Error(`Timed out waiting for new ${pattern} in ${logPath}`)
-}

Then Line 1778 becomes:

-    await waitForNewLogPattern(tauriLogPath, /windowWillClose:/, tauriLogLengthBeforeClose)
+    await waitForLogPattern(tauriLogPath, /windowWillClose:/, {
+      fromOffset: tauriLogLengthBeforeClose,
+    })
🤖 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 `@wework/e2e/desktop/task-flow.e2e.mjs` around lines 1163 - 1176, Remove the
duplicate waitForNewLogPattern helper and extend waitForLogPattern with an
optional offset parameter defaulting to 0. Apply the offset when testing newly
read log content, preserving existing waitForLogPattern callers and updating the
offset-based call site to use the shared helper.

Source: Coding guidelines


2355-2380: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Capture stage screenshots for this new lifecycle.

Every sibling verifier (verifyBackgroundTaskWindowLifecycle, verifyAttachmentOnlySidebarLifecycle, verifyMemoryGrowth) writes captureVerificationScreenshot evidence at each critical stage. This flow has four distinct states (running → continuing → background-unread-free → settled-unread) and none are captured, so a CI failure here leaves only the assertion message.

As per coding guidelines, verification must produce "reproducible failure or critical-path evidence."

🤖 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 `@wework/e2e/desktop/task-flow.e2e.mjs` around lines 2355 - 2380, Update
verifyActiveGoalIdleUnreadLifecycle to call captureVerificationScreenshot at
each critical lifecycle stage: running, continuing, background-unread-free, and
settled-unread. Use the existing sibling verifier screenshot conventions and
include stage-specific identifiers or labels so CI failures preserve
reproducible evidence for each state.

Source: Coding guidelines

wework/src/features/workbench/workbenchReducer.test.ts (1)

1276-1312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a reducer-level stale-refresh case too.

This covers the identity change well. The new applyRuntimeTaskActivity path (settle → runtime_work_refreshed returns an older running: true snapshot → still false, then runtime_task_started → true again) is only exercised at the provider integration level; a direct reducer test would pin the contract cheaply.

🤖 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 `@wework/src/features/workbench/workbenchReducer.test.ts` around lines 1276 -
1312, Add a direct reducer test alongside the existing runtime task settlement
test that applies runtime_task_settled, then runtime_work_refreshed with an
older running:true snapshot and verifies running remains false, followed by
runtime_task_started and verifies it becomes true. Use the existing
workbenchReducer and runtime task state fixtures to pin this stale-refresh
contract.
wework/src/features/workbench/workbenchReducer.ts (1)

1249-1260: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

pendingSettledRuntimeTasks only ever shrinks on a matching runtime_task_started.

Every settled task accumulates for the session — including tasks later archived (runtime_tasks_archived, Line 1233) or removed with their project (runtime_project_removed, Line 1076), which can never match a task again. Since applyRuntimeTaskActivity re-walks the whole work-list tree once per pending entry on every bootstrapped / lists_refreshed / runtime_work_refreshed, refresh cost grows monotonically with the number of settled tasks.

Pruning entries that are no longer present in the refreshed work list keeps the settled-state guarantee (a stale snapshot still contains the task) while bounding the array.

♻️ Drop pending entries for tasks that are gone
 function applyRuntimeTaskActivity(
   runtimeWork: RuntimeWorkListResponse | null,
   pendingSettledRuntimeTasks: RuntimeTaskAddress[]
 ): RuntimeWorkListResponse | null {
   return pendingSettledRuntimeTasks.reduce(
     (current, address) => updateRuntimeTaskRunning(current, address, false),
     runtimeWork
   )
 }
+
+function retainPendingSettledRuntimeTasks(
+  runtimeWork: RuntimeWorkListResponse | null,
+  pendingSettledRuntimeTasks: RuntimeTaskAddress[]
+): RuntimeTaskAddress[] {
+  if (!runtimeWork || pendingSettledRuntimeTasks.length === 0) return pendingSettledRuntimeTasks
+  const next = pendingSettledRuntimeTasks.filter(address =>
+    Boolean(findRuntimeTask(runtimeWork, address))
+  )
+  return next.length === pendingSettledRuntimeTasks.length ? pendingSettledRuntimeTasks : next
+}

Then in each refresh case commit both, e.g. for runtime_work_refreshed:

       return {
         ...state,
         runtimeWork,
+        pendingSettledRuntimeTasks: retainPendingSettledRuntimeTasks(
+          runtimeWork,
+          state.pendingSettledRuntimeTasks
+        ),
🤖 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 `@wework/src/features/workbench/workbenchReducer.ts` around lines 1249 - 1260,
Prune pendingSettledRuntimeTasks during each work-list refresh case
(bootstrapped, lists_refreshed, and runtime_work_refreshed) to retain only tasks
still present in the refreshed work list. Apply the same filtered pending list
both when calling applyRuntimeTaskActivity and when storing the refreshed
reducer state, while preserving entries represented by the refreshed snapshot so
settled-state handling remains intact.
🤖 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 `@wework/src/components/chat/MessageTurnNavigation.tsx`:
- Around line 560-564: The live-turn deduplication in MessageTurnNavigation.tsx
at lines 560-564 mixes runtime and local messageIndex values; update it to use
stable turn IDs or normalize live turns to runtimeMessageIndex before
comparison. In ScrollableMessageArea.test.tsx at lines 715-756, add coverage
with differing local and runtime indexes and assert that every live marker
remains visible.

In `@wework/src/features/workbench/WorkbenchProvider.test.tsx`:
- Around line 1816-1821: Update the listRuntimeWork mock in the
staleTerminalRefresh test setup to provide a default resolved
RuntimeWorkListResponse after the two
mockResolvedValueOnce/mockImplementationOnce behaviors. Preserve both existing
one-time responses and ensure any third call returns a valid response instead of
undefined.

In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Around line 1195-1197: Update onRefreshWorkLists in WorkbenchProvider to
invoke the stable refresh callback reference and report any rejected
refreshWorkLists promise with the relevant device/task context instead of
swallowing it. Adjust the surrounding dependency array to use that stable
callback reference, while preserving the existing refresh behavior.

---

Nitpick comments:
In `@wework/e2e/desktop/task-flow.e2e.mjs`:
- Around line 1163-1176: Remove the duplicate waitForNewLogPattern helper and
extend waitForLogPattern with an optional offset parameter defaulting to 0.
Apply the offset when testing newly read log content, preserving existing
waitForLogPattern callers and updating the offset-based call site to use the
shared helper.
- Around line 2355-2380: Update verifyActiveGoalIdleUnreadLifecycle to call
captureVerificationScreenshot at each critical lifecycle stage: running,
continuing, background-unread-free, and settled-unread. Use the existing sibling
verifier screenshot conventions and include stage-specific identifiers or labels
so CI failures preserve reproducible evidence for each state.

In `@wework/src/features/workbench/workbenchReducer.test.ts`:
- Around line 1276-1312: Add a direct reducer test alongside the existing
runtime task settlement test that applies runtime_task_settled, then
runtime_work_refreshed with an older running:true snapshot and verifies running
remains false, followed by runtime_task_started and verifies it becomes true.
Use the existing workbenchReducer and runtime task state fixtures to pin this
stale-refresh contract.

In `@wework/src/features/workbench/workbenchReducer.ts`:
- Around line 1249-1260: Prune pendingSettledRuntimeTasks during each work-list
refresh case (bootstrapped, lists_refreshed, and runtime_work_refreshed) to
retain only tasks still present in the refreshed work list. Apply the same
filtered pending list both when calling applyRuntimeTaskActivity and when
storing the refreshed reducer state, while preserving entries represented by the
refreshed snapshot so settled-state handling remains intact.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3cfc6884-f529-4959-8ac9-1fabed931f29

📥 Commits

Reviewing files that changed from the base of the PR and between 80d4314 and a2f786d.

📒 Files selected for processing (15)
  • docs/en/wework/developer-guide/wework-chat-state-sources.md
  • docs/zh/wework/developer-guide/wework-chat-state-sources.md
  • executor/tests/app_runtime_work_contract.rs
  • executor/tests/app_runtime_work_workspace_contract.rs
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/src/components/chat/MessageTurnNavigation.tsx
  • wework/src/components/chat/ScrollableMessageArea.test.tsx
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/features/workbench/runtimeTaskReminders.test.ts
  • wework/src/features/workbench/runtimeTaskReminders.ts
  • wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/src/types/workbench.ts

@qdaxb
qdaxb force-pushed the fix/wework-runtime-task-status branch from bcbed34 to 857e929 Compare July 25, 2026 01:57

@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)
wework/src/features/workbench/runtimeTaskStatus.ts (1)

1-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a dedicated unit test for this shared helper.

isRuntimeTaskRunning is now the shared running/active-goal contract consumed across runtimePaneStatus.ts, runtimeTaskCloseGuard.ts, and other files in this cohort. It's currently only exercised indirectly through each consumer's tests. A small dedicated test file covering the running/goalStatus combinations would pin down this contract directly.

🤖 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 `@wework/src/features/workbench/runtimeTaskStatus.ts` around lines 1 - 6, Add a
dedicated unit test for isRuntimeTaskRunning in runtimeTaskStatus.ts, covering
true and false running values combined with active and non-active goalStatus
values. Assert that the helper returns true when either condition indicates
activity and false only when both indicate inactivity.
🤖 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 `@wework/src/components/layout/DesktopWorkbenchMain.tsx`:
- Around line 1192-1193: Update the active-task pause flow around
paneIsResponseStreaming so taskExecution.running is routed through ChatInput and
its embedded composer via the existing isStreaming/onPause handling, rather than
relying only on GoalStatusBar. Preserve response-streaming behavior and ensure a
visible pause control remains available when task execution is active without an
active response.

---

Nitpick comments:
In `@wework/src/features/workbench/runtimeTaskStatus.ts`:
- Around line 1-6: Add a dedicated unit test for isRuntimeTaskRunning in
runtimeTaskStatus.ts, covering true and false running values combined with
active and non-active goalStatus values. Assert that the helper returns true
when either condition indicates activity and false only when both indicate
inactivity.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b00a1181-7327-439c-9944-57ca05bd9065

📥 Commits

Reviewing files that changed from the base of the PR and between bcbed34 and 857e929.

📒 Files selected for processing (29)
  • docs/en/wework/developer-guide/wework-chat-state-sources.md
  • docs/zh/wework/developer-guide/wework-chat-state-sources.md
  • executor/tests/app_runtime_work_contract.rs
  • executor/tests/app_runtime_work_workspace_contract.rs
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/src/components/chat/MessageList.test.tsx
  • wework/src/components/chat/MessageList.tsx
  • wework/src/components/chat/MessageTurnNavigation.tsx
  • wework/src/components/chat/ScrollableMessageArea.test.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.test.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.tsx
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/features/workbench/runtimePaneStatus.test.ts
  • wework/src/features/workbench/runtimePaneStatus.ts
  • wework/src/features/workbench/runtimeTaskReminders.test.ts
  • wework/src/features/workbench/runtimeTaskReminders.ts
  • wework/src/features/workbench/runtimeTaskStatus.ts
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/src/lib/developerCommandMenu.test.ts
  • wework/src/tauri/runtimeTaskCloseGuard.test.ts
  • wework/src/tauri/runtimeTaskCloseGuard.ts
  • wework/src/tauri/trayMenuState.test.ts
  • wework/src/tauri/trayMenuState.ts
  • wework/src/types/workbench.ts
💤 Files with no reviewable changes (1)
  • wework/src/components/chat/MessageList.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • docs/zh/wework/developer-guide/wework-chat-state-sources.md
  • docs/en/wework/developer-guide/wework-chat-state-sources.md
  • wework/src/components/chat/ScrollableMessageArea.test.tsx
  • wework/src/features/workbench/runtimeTaskReminders.test.ts
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/components/chat/MessageTurnNavigation.tsx
  • executor/tests/app_runtime_work_contract.rs
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/e2e/desktop/task-flow.e2e.mjs

qdaxb added 4 commits July 25, 2026 12:32
…sk-status

# Conflicts:
#	wework/e2e/desktop/task-flow.e2e.mjs
#	wework/src/components/chat/MessageTurnNavigation.tsx
#	wework/src/components/chat/ScrollableMessageArea.test.tsx
#	wework/src/features/workbench/workbenchReducer.ts
…sk-status

# Conflicts:
#	wework/e2e/desktop/task-flow.e2e.mjs
@qdaxb qdaxb changed the title fix(wework): simplify runtime task status refactor(wework): centralize runtime task lifecycle Jul 25, 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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
wework/src/features/workbench/RuntimeTaskCloseGuard.tsx (1)

17-43: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the close listener installed across lifecycle changes.

Reinstalling this async listener on every count change creates a cleanup-to-subscribe gap. A close event in that window is lost, so the app can close while tasks are running. Install once and read the current running count through a ref or stable event callback.

🤖 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 `@wework/src/features/workbench/RuntimeTaskCloseGuard.tsx` around lines 17 -
43, The useEffect around installRuntimeTaskCloseGuard must install the close
listener only once rather than rerunning when lifecycle.runningTaskKeys.size
changes. Store the current running-task count in a ref or use a stable callback
so the listener always reads fresh state, while preserving cancellation,
cleanup, and dialog behavior.
wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts (1)

1301-1313: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mirror lifecycle store updates in pauseCurrentResponse.

pauseCurrentResponse cancels state.currentRuntimeTask, but unlike cancelRuntimePaneTask, it never calls lifecycleStore.stopRequested/stopRejected. Pause actions wired here can leave the sidebar/composer/tray with a stale running/stopping state until reconciliation.

🐛 Proposed fix: mirror cancelRuntimePaneTask's lifecycle wiring
 const pauseCurrentResponse = useCallback(async () => {
   if (!state.currentRuntimeTask) return

+  lifecycleStore.stopRequested(state.currentRuntimeTask)
   const ack = await executorClient.runtime.cancelRuntimeTask(state.currentRuntimeTask)
   if (!ack.accepted) {
+    lifecycleStore.stopRejected(state.currentRuntimeTask)
     dispatch({
       type: 'error_set',
       error: normalizeGuidanceError(ack.error ?? '取消当前回复失败'),
     })
     return
   }
   await refreshWorkLists()
-}, [dispatch, executorClient, refreshWorkLists, state.currentRuntimeTask])
+}, [dispatch, executorClient, lifecycleStore, refreshWorkLists, state.currentRuntimeTask])
🤖 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 `@wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts` around lines
1301 - 1313, Update pauseCurrentResponse to mirror cancelRuntimePaneTask’s
lifecycle handling: call lifecycleStore.stopRequested when cancellation begins
and lifecycleStore.stopRejected when the acknowledgement is not accepted, using
the same task or runtime identifiers and error context as the existing cancel
flow. Preserve the current error dispatch, early returns, and refreshWorkLists
behavior.
🧹 Nitpick comments (3)
wework/src/components/layout/MobileWorkbenchLayout.test.tsx (1)

208-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Harness forces every open task into a running state.

lifecycleTaskRunning = Boolean(state.currentRuntimeTask) plus the unconditional store.executorStarted(...) means any test that opens a task renders with isBusy/running true, so idle-task composer/message assertions can no longer be expressed. Consider deriving running from the task's running flag in state.runtimeWork (or an explicit prop) so both states remain testable. Also note the store mutation inside useMemo is impure; a useState initializer or useEffect would be safer if React discards the memo.

Also applies to: 259-259

🤖 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 `@wework/src/components/layout/MobileWorkbenchLayout.test.tsx` around lines 208
- 215, Update the MobileWorkbenchLayout test harness around lifecycleStore and
lifecycleTaskRunning so an open task is marked running only when its
corresponding state.runtimeWork entry has running=true, preserving idle-task
scenarios. Remove the unconditional executorStarted call based solely on
currentRuntimeTask, and avoid mutating the store inside useMemo by initializing
it through useState or synchronizing it in an appropriate effect.
wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts (1)

201-209: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache the persisted unread set instead of re-reading localStorage per machine.

ensureMachine performs a synchronous localStorage.getItem + JSON.parse for every task it creates, so the first syncRuntimeWork over a large work list does N reads on the layout-effect path in WorkbenchProvider. Read it once and keep it in a field (the store already owns writes via persistUnreadKeys).

♻️ Proposed fix
-    const machine = new RuntimeTaskMachine(address, this.readUnreadKeys().has(key))
+    this.persistedUnreadKeys ??= this.readUnreadKeys()
+    const machine = new RuntimeTaskMachine(address, this.persistedUnreadKeys.has(key))
🤖 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
`@wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts`
around lines 201 - 209, Cache the persisted unread-key set on
RuntimeTaskLifecycleStore and initialize it once from localStorage, then have
ensureMachine reuse that field instead of calling readUnreadKeys for each
machine. Update persistUnreadKeys to keep the cache synchronized whenever writes
occur, preserving existing unread-key behavior.
wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts (1)

182-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for unread rehydration from localStorage.

The store persists unread keys per user and seeds new machines from them (ensureMachine), but no test constructs a second store with the same userId to assert the unread state survives a reload. That path is easy to regress silently.

🤖 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
`@wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts`
around lines 182 - 200, Add a test covering unread-state rehydration by creating
one RuntimeTaskLifecycleStore for a user, producing and persisting an unread
task state, then creating a second store with the same userId and asserting the
task’s derived.shouldShowUnread remains true. Exercise the ensureMachine
initialization path and preserve the existing behavior that opening the task
clears unread state.
🤖 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 `@docs/en/wework/developer-guide/wework-chat-state-sources.md`:
- Line 24: Update the “Current runtime running snapshot” row in the developer
guide to replace the removed getRuntimePaneTaskExecution and
currentRuntimeTaskRunning references with the runtime task lifecycle snapshot,
specifically deriveRuntimePaneStatus({ lifecycle }) from runtimePaneStatus.ts.
Preserve the documented running semantics and update the source/consumer columns
to describe the lifecycle store as authoritative.

In `@docs/zh/wework/developer-guide/wework-chat-state-sources.md`:
- Line 24: 更新“当前 runtime 运行快照”表格行,改为记录 pane status consumers 使用的
RuntimeTaskLifecycleStoreSnapshot,并将 lifecycle state 标明为前端/UI 的权威来源;保留 executor
的 running 作为外部输入单独说明,移除过时的 getRuntimePaneTaskExecution 和
currentRuntimeTaskRunning 引用。

In `@wework/src/components/layout/DesktopWorkbenchLayout.test.tsx`:
- Around line 835-846: Derive the effective lifecycle-running value once, using
the explicit props.lifecycleTaskRunning value with the existing
currentRuntimeTask fallback, and reuse it in both the lifecycleStore
initialization and the provider’s lifecycleTaskRunning value. Update the
lifecycleStore useMemo dependencies to track this derived value and current
runtime state, ensuring executorStarted and the rendered status stay consistent.

In `@wework/src/components/layout/runtimeTaskSidebarHelpers.ts`:
- Around line 16-18: Update getRuntimeTaskSortTime to return the latest
available timestamp among completedAt, updatedAt, and createdAt rather than
prioritizing completedAt; preserve undefined when no timestamps exist and avoid
adding any running-state dependency.

In `@wework/src/components/layout/useWorkbenchPaneSession.ts`:
- Around line 1607-1617: In the currentRuntimeTask branch of the submission
flow, update the !response.accepted failure path after setRuntimeGoal to restore
the cleared input using submittedInput before setting the error and returning,
matching the sibling failure path’s behavior.

In `@wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts`:
- Around line 8-39: Update the goalStatus assignment in the
executor_snapshot_received reducer case to preserve state.goalStatus when
event.task.goalStatus is undefined, while still applying explicit values
including null when provided. Keep the existing snapshot handling and other
state updates unchanged.
- Around line 41-64: Update the send_rejected branch in the runtime task
lifecycle reducer so it only transitions to idle when no executor confirmation
has been observed; otherwise preserve the current active executionPhase,
turnPhase, and expectedExecutorRunning state. Use the existing
executor-confirmation state/symbols and keep send_requested and send_accepted
behavior unchanged.

In
`@wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts`:
- Around line 146-176: Update RuntimeTaskLifecycleStore.rename to migrate
previousState.goalStatus to nextMachine alongside the existing task, execution
phase, turn phase, and unread state; use the machine’s established goal-status
event or transition so reduceMachine preserves the same goal status after
renaming.
- Around line 242-245: Update persistUnreadKeys to skip writing when the
incoming key set matches the currently persisted snapshot, and wrap
localStorage.setItem in defensive error handling so quota or private-mode
failures cannot propagate into publish() or prevent listener notification.
Preserve the existing 200-key serialization and SSR guard.
- Around line 112-131: Update syncTranscript so historical settled assistant
messages cannot trigger turnSettled or dispatch turn_settled when
transcript.running is unknown. Restrict settlement to transcript.running ===
false, or ensure any settled assistant check is scoped to the current turn while
preserving streaming recovery behavior.

---

Outside diff comments:
In `@wework/src/features/workbench/RuntimeTaskCloseGuard.tsx`:
- Around line 17-43: The useEffect around installRuntimeTaskCloseGuard must
install the close listener only once rather than rerunning when
lifecycle.runningTaskKeys.size changes. Store the current running-task count in
a ref or use a stable callback so the listener always reads fresh state, while
preserving cancellation, cleanup, and dialog behavior.

In `@wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts`:
- Around line 1301-1313: Update pauseCurrentResponse to mirror
cancelRuntimePaneTask’s lifecycle handling: call lifecycleStore.stopRequested
when cancellation begins and lifecycleStore.stopRejected when the
acknowledgement is not accepted, using the same task or runtime identifiers and
error context as the existing cancel flow. Preserve the current error dispatch,
early returns, and refreshWorkLists behavior.

---

Nitpick comments:
In `@wework/src/components/layout/MobileWorkbenchLayout.test.tsx`:
- Around line 208-215: Update the MobileWorkbenchLayout test harness around
lifecycleStore and lifecycleTaskRunning so an open task is marked running only
when its corresponding state.runtimeWork entry has running=true, preserving
idle-task scenarios. Remove the unconditional executorStarted call based solely
on currentRuntimeTask, and avoid mutating the store inside useMemo by
initializing it through useState or synchronizing it in an appropriate effect.

In
`@wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts`:
- Around line 182-200: Add a test covering unread-state rehydration by creating
one RuntimeTaskLifecycleStore for a user, producing and persisting an unread
task state, then creating a second store with the same userId and asserting the
task’s derived.shouldShowUnread remains true. Exercise the ensureMachine
initialization path and preserve the existing behavior that opening the task
clears unread state.

In
`@wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts`:
- Around line 201-209: Cache the persisted unread-key set on
RuntimeTaskLifecycleStore and initialize it once from localStorage, then have
ensureMachine reuse that field instead of calling readUnreadKeys for each
machine. Update persistUnreadKeys to keep the cache synchronized whenever writes
occur, preserving existing unread-key behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 992bcc01-2a27-49da-a55e-714fc45ff877

📥 Commits

Reviewing files that changed from the base of the PR and between 857e929 and 6f091d8.

📒 Files selected for processing (55)
  • docs/en/wegent/developer-guide/local-device-architecture.md
  • docs/en/wework/developer-guide/wework-chat-state-sources.md
  • docs/zh/wegent/developer-guide/local-device-architecture.md
  • docs/zh/wework/developer-guide/wework-chat-state-sources.md
  • executor/src/agents/codex.rs
  • executor/src/agents/codex/run_state.rs
  • executor/src/runtime_work/handler/tests.rs
  • executor/src/runtime_work/handler/turns.rs
  • executor/src/runtime_work/response.rs
  • wework/e2e/desktop/process-lifecycle.mjs
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/package.json
  • wework/scripts/check-runtime-task-lifecycle.mjs
  • wework/src/App.plugins.test.tsx
  • wework/src/components/chat/MessageList.test.tsx
  • wework/src/components/chat/MessageList.tsx
  • wework/src/components/layout/DesktopSidebar.test.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/layout/MobileDrawer.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.test.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.tsx
  • wework/src/components/layout/runtimeTaskSidebarHelpers.ts
  • wework/src/components/layout/useWorkbenchPaneSession.ts
  • wework/src/e2e/desktop-process-lifecycle.test.ts
  • wework/src/features/workbench/RuntimeTaskCloseGuard.tsx
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/features/workbench/runtimePaneStatus.test.ts
  • wework/src/features/workbench/runtimePaneStatus.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleProvider.tsx
  • wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskMachine.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/context.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/index.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/internalContext.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts
  • wework/src/features/workbench/runtimeTaskLifecycle/types.ts
  • wework/src/features/workbench/runtimeTaskReminders.test.ts
  • wework/src/features/workbench/runtimeTaskReminders.ts
  • wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts
  • wework/src/features/workbench/useWorkbenchRuntimeTasks.ts
  • wework/src/features/workbench/workbenchContextTypes.ts
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/src/lib/debugPanel.ts
  • wework/src/lib/developerCommandMenu.test.ts
  • wework/src/pages/WorkbenchPage.tsx
  • wework/src/tauri/runtimeTaskCloseGuard.test.ts
  • wework/src/tauri/runtimeTaskCloseGuard.ts
  • wework/src/tauri/trayMenuState.test.ts
  • wework/src/tauri/trayMenuState.ts
  • wework/src/types/workbench.ts
💤 Files with no reviewable changes (2)
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/runtimeTaskReminders.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • wework/src/components/chat/MessageList.test.tsx
  • wework/src/components/chat/MessageList.tsx
  • wework/src/types/workbench.ts
  • wework/src/features/workbench/workbenchReducer.ts

@qdaxb
qdaxb merged commit 1278f5e into main Jul 25, 2026
32 of 33 checks passed
@qdaxb
qdaxb deleted the fix/wework-runtime-task-status branch July 25, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant