Wework Chat State Sources
This document records the state sources for the Wework chat path. The goal is to make UI code read one explicit derived status instead of letting the send button, message stream, queue, and runtime task list override each other.
Core Principles
RuntimeTaskMachineis the aggregate root for one task's execution, turn, Goal, and unread lifecycle; its reducer is an internal transition implementation.RuntimeTaskLifecycleStoreowns all task machines, routes executor/UI/transcript events, and is the only frontend source for task lifecycle state.RuntimeTaskLifecycleProvideronly exposes Store subscriptions to React. It does not own, cache, or derive lifecycle state.useWorkbenchPaneSessionowns pane content such as messages and queued input, but reads all task and turn lifecycle facts from the Store.runtimePaneStatus.tsis a read-only projection from a lifecycle snapshot to existing pane presentation fields. It must not infer execution or turn state independently.
Sidebar priority filtering also reads explicit interactionStatus through the
lifecycle Store projection. A stale task-list snapshot must not override an
accepted user reply in the view layer.
Pending Interaction Membership and Error Cleanup
After successful archival, remove the task through the shared lifecycle Store before updating
the initiating view's task list. This confirmed user action is independent of background snapshot
ownership. Removing through a non-owner's read-only view leaves embedded inbox conversations
able to restore the old question from shared lifecycle state. Background write guards remain intact.
After persisting archival, the executor emits the existing runtime.work.changed event so other
subscribed views refresh their own task inventories. Updating only the initiating view cannot
invalidate an embedded inbox conversation's stale inventory.
Pending scans use current task sources and device pending inventories. Cached questions must not reintroduce archived or removed tasks. After a successful inventory read, remove questions, successful-read markers, and related errors for tasks outside the candidate set. Failed or offline devices retain their previous inventory; a connection failure is not an empty list. Tasks hidden from the sidebar remain discoverable through the device pending inventory. Confirmed archive markers in the existing conversation cache override offline inventory retention, even after the shared lifecycle entry is removed. Unarchival uses the existing marker cleanup.
Failures during local identity discovery, inventory reads, transcript reads, and answered-question
archival use the existing diagnostic channel in runtime-launch.log. Search for
pending-interaction-load-failed. Entries include the operation, device and task identifiers,
exception message, stack, error code, and status. Requests and conversation contents are not
serialized; the log writer applies its existing credential redaction. An offline device that
makes pending work unavailable is logged once per state transition.
The desktop human-interactions checkpoint archives a real task after caching an unanswered
question. It checks that background polling and renderer reload leave no pending item or load
error and never restore the archived task.
Needs Your Input Answer Drafts
RuntimeInteractionsProvider directly owns and synchronously exposes account-scoped
answer drafts. Every controlled input edit must update that state directly, rather
than updating child state and publishing a snapshot through an effect. That extra
publication step writes the previous draft back into the DOM at the end of the
input event. Even if the new value returns immediately afterward, the write
interrupts Chinese IME composition. Persistence may use effects; input values must
not wait for persistence or snapshot publication. After an account switch, callbacks
from the previous account must not update the current account's drafts.
Regression tests inspect every application-side value write during composition,
not only the final text. The desktop human-interactions checkpoint reuses a real
question and answer submission flow to check that DOM composition events cause no
stale value writes, focus loss, or input remounts. This automation does not verify
the operating system's candidate window.
New Conversations and Manual Sidebar Order
Standalone conversations and project tasks share one ordering rule: when a manual
order exists, new conversations absent from that order appear before ordered tasks,
while ordered tasks retain their user-defined relative order. The executor returns
sidebarOrder only for tasks included in the manual order. Refreshing or reloading
the list must not append new conversations to the bottom. Model selection,
reasoning effort, and prompt references do not change this rule.
When a conversation is first dragged before another conversation, the persisted order must include both the moved conversation and its target. Otherwise, the target is treated as unordered and the drag result is lost.
Latest Turn Identity
turn.id identifies the active turn and may be cleared after completion. turn.latestId retains the latest turn identity confirmed by runtime events, transcripts, or executor snapshots. Task-list refreshes, completion timestamp precision changes, and other metadata updates must not clear it. Reply predictions use turn.latestId as their request identity and wait until a real turn identity is available. Starting a new turn still invalidates the previous suggestion.
State Source Inventory
| State | Single Source | Derived Values / Consumers | Maintenance Rule |
|---|---|---|---|
| Message content and status | useWorkbenchPaneSession.messages | MessageList, export, file changes, request user input | Update only through transcript reset or reduceWorkbenchMessages |
| Whether the assistant turn is active | RuntimeTaskLifecycleStore task snapshot turn.phase | Streaming message presentation and debug state | Live stream and transcript events are routed into the task machine; layouts and pane code must not independently infer turn activity from messages |
| Local send phase | RuntimeTaskLifecycleStore task snapshot turn.phase | status.isSubmitting, status.isWaitingForAssistantIndicator, compatibility fields sending/waitingForAssistant | Optimistic send, acknowledgement, rejection, stream start, and settlement are events owned by the task machine |
| Current runtime running snapshot | Executor running input routed through RuntimeTaskLifecycleStore | Lifecycle snapshot, sidebar, composer, message area, queue advancement | The executor field is the authoritative external fact; the lifecycle Store is the only frontend access point and keeps optimistic transitions in the same machine |
| Whether the pane is busy | paneSession.status.isBusy | Whether the current pane queue may advance | Composed from isSubmitting, isAwaitingAssistant, isAssistantStreaming, and taskExecution.running |
| Queued messages | queuedMessages | ConversationQueuePanel, automatic next follow-up send | Mutate only inside the pane session; advancement must use status.canSendQueuedMessage |
| Guidance messages | One source per lifecycle phase: queuedMessages while pending, the mounted or background live projection after application, and the Codex transcript once the Provider covers that turn | ConversationQueuePanel, MessageList | Remove applied guidance from the queue; a background cache entry only bridges a running transcript gap, must never merge into Provider pages, and must yield the whole projection once the Provider covers the same turn |
| Transcript loading and pagination | transcriptLoading, transcriptHasMoreBefore, transcriptBeforeCursor, loadedTranscriptRanges | Infinite scroll, turn navigation | Update only from transcript API responses |
| Runtime goal | threadGoal + pendingGoalState | Goal bar, goal draft, first-message initial goal | Persisted goals come from the runtime goal API; goals before task creation live in pending seeds |
| Answered request user input ids | answeredRequestUserInputIds | Hide already submitted or ignored request user input cards | Update only from submit or ignore actions |
| Model context usage | Codex thread/tokenUsage/updated runtime stream events; runtime.tasks.transcript.contextUsage | Context-window usage ring and tooltip in the bottom-right composer controls | The executor must forward Codex token usage notifications unchanged and read the latest token count from the same rollout for historical transcript responses; UI stores it only as projectChat.contextUsage for the current runtime task |
| Long response content and tool output | Preview windows from reduceWorkbenchMessages; truncation fields and full-load marker from runtime.tasks.transcript | MessageList, processing blocks, Debug Panel memory summaries | Resident messages keep only a tail preview, original length, and load reference by default; only an explicit user-triggered full transcript load may upgrade the current pane and replace messages with complete content |
| Attachment/model/skill selection | projectChat context | Send payload, composer controls | In-task option locking is derived from projectChat.isOptionsLocked |
| Device availability | state.devices + current task/project device selection | Composer disabled reason, device prompts | Use only for send preconditions; never for assistant streaming status |
Conversation Scrolling and Diagram Rendering
TanStack Virtual and its native ResizeObserver own message measurements. Keep message content visible during scrolling and reuse rows by message identity. Do not introduce a second height writer, replace scrolling content with placeholders, or estimate Markdown height on every frame.
Opening a conversation initially positions it at the latest content; returning to a reader position restores the saved location. A boundary observer rooted in the current scroll container prefetches the previous page before the reader reaches loaded history. Prepending history preserves the reader position through stable message, content-item, and paragraph identities. The boundary marker always occupies one pixel; loading feedback and pagination actions are portaled outside the transcript without affecting measurements. Open search retains explicit pagination, and hidden panes suspend automatic loading. Failures and completed requests that leave the boundary unchanged offer manual recovery without a request loop. Manual retries retain the control and keyboard focus.
Diagram previews cache sanitized static SVG artifacts and dimensions by document, source, theme, and server address. Mermaid compilation is serialized, and concurrent requests for the same artifact share one compilation. Message rows mount static images with a lightweight zoom canvas, and fullscreen presentation reuses the same artifact. Inline diagrams allow vertical touch scrolling of the conversation; fullscreen retains unrestricted dragging. Remounting an offscreen row must not recompile the same diagram or retain a full file viewer for every row.
Board Conversation Split Views
The project-space board Hover task panel is a split view of the original task conversation, not a separate preview conversation. The Task page, Hover panel, and card summary must use the same runtime task address and read the same canonical RuntimeConversationTurn[] from runtimeConversationCache.
- The Hover panel directly reuses
TemporaryChatPanel; message loading, live events, continuation, and attachments all target the original task conversation. - Card progress text and tool activity may only be projected from canonical turns through pure selectors. Do not persist
finalResponsePreview, serialize assistant messages, or introduce board/Hover-specific message caches. - When the server confirms that a transcript is idle, that response is the task's authoritative snapshot and replaces old terminal turns absent from the server. Only an active transcript is merged with buffered local events.
- A terminal conversation is evicted after five idle minutes when no full conversation view retains it. Card summaries may observe updates but must not prevent eviction merely because the board remains mounted; an open Task page or Hover split view retains the conversation while subscribed.
- The board prefetches only a bounded transcript tail for running and review tasks. Increasing that range requires evidence that the UI needs it; a one-line summary must not load or copy complete tool output.
Runtime Event Flow
The conversation stream coordinator retains exactly one subscription and prefers the active page's service instance. When the active page uses a different instance, it releases the old subscription before subscribing to the new instance so a hidden page's retained device routes cannot lose events. Moving active ownership within the same service instance preserves the subscription. Only the active page handles messages.
The lifecycle coordinator must also continue unfinished event recovery when services are replaced. Recovery reasons and task addresses belong to the same lifecycle Store. After cancelling the old subscription's reads, the new subscription reads authoritative task and transcript snapshots, including pending-message receipts. A service-object change does not complete recovery, and an empty receipt does not confirm a send.
- A new message submit sets
sendPhasetosubmitting. - After runtime accepts the request,
sendPhasebecomesawaiting_assistant. chat:startbecomesassistant_started; the reducer creates or updates the assistant streaming message andsendPhasereturns toidle.chat:chunkand block events update onlymessages.- Codex
thread/tokenUsage/updatedevents update onlyprojectChat.contextUsage; they must not create empty messages or write transcript data. - When opening a historical task,
runtime.tasks.transcript.contextUsageonly restores the current task'sprojectChat.contextUsage; UI code must not add extra fallbacks that rescan messages or task lists. chat:done,chat:error, and cancellation events settle the assistant message through the reducer and refresh the work list.- If runtime work and message state disagree, do not settle it with fallback logic; fix the missing stream event, transcript data, or reducer action.
During send startup, Wework may first receive an empty transcript with
source: pending_local_task and running: false before the real runtime task
appears in the task list. While the turn remains submitting or
awaiting_assistant and no assistant has settled, that transcript must not
settle the executor or turn. The later task-list running: true snapshot is
authoritative once the executor has started. An explicit running: false
received while an assistant is already streaming remains terminal and must
settle normally; false snapshots must not be ignored indiscriminately.
A Codex provider may send only an item/completed snapshot containing the
complete assistant text, without any item/agentMessage/delta. If the current
final response has not received a delta, the executor must convert that text
into one response.output_text.delta. If deltas were already received, it
must ignore the completed snapshot to avoid duplicate text. Temporary chats
cannot depend on thread/read(includeTurns) to recover live text that was dropped.
After a task settles in the background, reopening its pane may initially load
a stale transcript that contains only an older turn. useWorkbenchPaneSession
must compare the latest cached turn identity with settled assistant identities
from the transcript. A turn identity includes both turnId and normalized
subtaskId. The cache remains authoritative until the transcript settles the
same turn; only then does the transcript become authoritative. Content length,
block count, or any other content-weight heuristic must not determine recency.
A work-list refresh may also immediately reload the completed transcript. That
transcript owns the final text, message status, and file changes, but Codex
thread/read may temporarily omit tool items that already completed in the
live stream. The assistant_done ingress normalizes the live message's
subtaskId to its canonical turnId. After switching to the matching
transcript turn, useWorkbenchPaneSession keeps the transcript's authoritative
fields while restoring live tool blocks whose status is done or error and
whose block id is absent from the transcript. It must not restore pending or
streaming blocks, which would make a completed task appear active again.
A single Codex turn can be split into multiple assistant messages by tool
calls or mid-turn guidance. Each message must have a distinct message id
while retaining the same canonical turnId. Turn-level actions such as fork
and rollback must use the canonical turn ID persisted by Codex, never a
UI-only segmented message ID.
Codex Turn Identity and Recovery
The Codex app-server turn/start response is the authoritative source for a
new turn identity. The executor must record its turn ID immediately after the
request succeeds. A later turn/started notification may confirm or correct
that identity, but it must not be required before the turn becomes active.
Guidance and interruption therefore still target the current turn when a live
notification is delayed or missing.
The UI and guidance must read the same NativeExecutionState, rather than
infer execution from local request controls. A start receipt may only bind the
current request's starting state; native notifications then supply process
generation and timing. A late receipt cannot reactivate a settled or replaced
request, and a delayed start notification for the same turn cannot revive a
stopped state. Native Goal continuations can outlive local request controls and must
remain guidable through their native running turn. Waiting for the next turn
is bounded and does not create another guidance state.
When the user sends guidance, Wework optimistically inserts the guidance
message into the current turn before calling runtime.tasks.guidance. If
Codex turn/steer explicitly reports that the expected turn ID differs from
the actual active turn ID, the executor corrects this guidance request's target
and retries exactly once without rewriting execution state. The successful
response turn ID may rebind the optimistic message to the correct turn. On failure, Wework removes the
optimistic message and keeps a retryable queue item so the transcript never
shows guidance that Codex did not accept.
Queue items display the specific failure reason; an absent active turn asks the user to edit and resend.
Interrupt and send now optimistically marks the running turn as cancelled and removes in-flight optimistic guidance before requesting interruption. The interrupt operation treats an already absent active turn as idempotent success, then starts the replacement turn. If the request fails, the frontend restores the previous turn state and optimistic guidance instead of losing the user's message.
Live events carry incremental updates only. After the WebView or runtime
transport is rebuilt, recovery for a persisted Codex thread must call
thread/resume and then read a complete snapshot with
thread/read(includeTurns). The snapshot re-establishes turns, messages, and
running state; code must not continue inferring them from pre-disconnect
in-memory events.
The provider transcript reader must not run concurrently with an active Codex turn. After a successful idle read of the latest transcript page, the executor persists that page's message snapshot scoped to the thread ID. A transcript response during execution merges that snapshot with pending user messages, settled messages, and active stream messages, deduplicating by message ID. This keeps historical assistant replies visible when a new turn starts after a WebView or executor rebuild. A thread ID change must clear both the completed message cache and transcript snapshot so messages cannot cross thread boundaries.
History snapshots must retain the Provider's before and after cursors. Default
reads during execution may reuse a snapshot, but explicit pagination must use
native history. A tail snapshot is not complete history. Merge live assistant
blocks and runtime items by item ID so new items cannot replace loaded history.
Adding live items must not upgrade a summary turn's itemsView to full.
Automatic Goal continuation owns a new turn. It must neither carry the previous turn's client user message association nor rewrite its subtask-to-turn mapping. The starting-next-turn label ends when that turn produces visible text or tools; history reconciliation remains active until continuation settles.
The capability publication lock covers session preparation and runtime initialization only. Codex and Claude Code release the read lease before model execution. Capability synchronization publishes under the write lease, so a long model run cannot block other tasks from starting.
When the first message carries a pending Goal seed, both the send entry point
and pane initialization must write the seed status into
RuntimeTaskLifecycleStore immediately. An asynchronous runtime.goal.get
may return no Goal before persistence completes; while the seed still belongs
to the current task, that empty result must not clear the lifecycle Goal
status. This lets an active Goal continue to constrain task lifecycle even
when stream settlement races ahead of Goal persistence.
When restoring a runtime task from the URL, the pane may initially know only the device ID and task ID before the task list hydrates its workspace, thread, and runtime fields. Goal and transcript loading must run again after that address hydration, while both addresses still belong to the same stable task identity. Rehydration must not create a new conversation or replay turn lifecycle events that were already processed.
Claude Code Conversation Executor
Claude Code uses the same ordinary runtime-task conversation UI as Codex in
Wework. A terminal or TUI must not replace the message list. The executor
starts each turn in Claude Code print mode, consumes stream-json events, and
maps them into the shared chat:start, text delta, tool block, and terminal
events. The Claude session ID returned by the first turn is stored in the
LocalTask runtime handle. Follow-ups, Goals, and /compact resume that same
session with --resume.
The executor persists Claude Code Goal state in the LocalTask runtime handle.
A Goal message is sent as Claude's native /goal <objective> command, and only
the turn that actually executes that Goal may update it to complete. An
ordinary follow-up must not complete a Goal that has not run. After turn
settlement, Wework may synchronize the latest snapshot through
runtime.tasks.goal.get, but an empty response must not overwrite an existing
Goal. Explicit removal belongs only to the Goal clear API or a
runtime.goal.cleared event. When asynchronous snapshots race, the newer
updatedAt value wins; at the same timestamp, complete wins over a
non-terminal status.
Model and permission selections are per-turn execution parameters. Wework maps
Claude Code permission modes to default, acceptEdits, plan, auto, or
bypassPermissions, and maps the selected model to the Claude CLI --model
argument. An absent model selection must not be interpreted as an explicit
Claude runtime override. Claude Code /compact remains an ordinary native
command, while Codex continues to use its app-server compact RPC; the two
runtimes must not share the compaction implementation.
Local Multi-Root Projects and Task Ownership
A local Codex project may contain an ordered list of workspace roots. The first
entry is the primary root used by the composer's default workspace and the
execution request cwd. The complete deduplicated root list is project-level
execution context and must not be reduced to the current workspace.
Multi-root context follows this primary path:
- Wework reads the runtime project's
roots. It derives roots fromdeviceWorkspacesonly when that field is absent, then sendsruntimeProjectKey,runtimeProjectName, andruntimeWorkspaceRootswhen creating a task or sending a message. - Local services write those fields into execution request metadata.
- The executor persists the project key and roots in
RuntimeTaskLink, then passesruntimeWorkspaceRootsto Codexthread/start,thread/fork,thread/resume, andturn/start. - If a follow-up request omits project metadata, the executor restores it from
the existing
RuntimeTaskLink, so the same thread and a reopened conversation keep the same project scope. - Codex global thread assignment uses the explicit project key before path matching, preventing one multi-root project from splitting into several sidebar projects.
The latest local runtimeWork snapshot is authoritative for local task
presentation. An asynchronous cloud refresh must merge with the latest local
snapshot when the refresh completes; it must not overwrite a newly created
task with state captured before the request began. New chat clears only the
current chat pane. It does not archive or delete the previous task, which must
remain under its project and be reopenable. The environment popover must list
and copy every project root, not only the primary root.
The local task inventory in My Work comes from runtimeWork, but its running
and queued groups must read the same RuntimeTaskLifecycleStore snapshot as the
sidebar. The sidebar spinner, composer, and My Work must not independently
infer lifecycle state from RuntimeTaskSummary.running, transcript data, or
messages; an asynchronous task-list snapshot could otherwise project a task
that is still running into a completed or action-required group.
These rules apply only to local Codex projects. Remote and cloud tasks retain their existing single-workspace selection semantics; local multi-root support must not implicitly broaden a remote execution scope.
Context Compaction Results
The context indicator uses the smaller of the runtime window and the model input budget. When no window is configured, it reads the selected Wework Catalog window; local custom models use their own Catalog window. The input budget reserves the explicit output ceiling, or 96,000 tokens for Chat Completions and Anthropic Messages when no ceiling is configured. Native Responses preserves provider defaults without an explicit output ceiling. With an explicit or translated-protocol output reservation, the executor auto-compacts at 80% of the input budget; the manual compaction hint defaults to 75%.
The executor live-item cache must retain compaction start/completion status. Persisting an interrupted turn must preserve the incomplete result of an unfinished compaction.
Only a compaction completion event can mark context_compaction successful. When a turn ends or the user stops it, an unfinished compaction displays “Context compaction did not complete”; ordinary block settlement must not mark it done. A completed compaction stays successful even if its containing turn is subsequently stopped.
Interrupted compaction has not reduced the context, so the next turn may compact again. History refresh must not turn an interrupted attempt into a success; Codex history may omit unfinished attempts. The desktop context-compaction checkpoint covers successful compaction, interruption, history refresh, and subsequent compaction recovery.
The same checkpoint also loads models from the real configuration file to verify shared and Kimi Catalog default windows, default and explicit output reservations for translated protocols, and explicit and omitted Responses output ceilings. Each case checks no compaction below the threshold, automatic compaction above it, summary-backed continuation, and stable conversation identity. Only the model service is mocked; model configuration, Electron, the executor, Codex, and protocol conversion use real implementations. These cases reuse the existing Core CI checkpoint and build artifacts.
Web Search Tool Blocks
A Codex web search may not include its query action in item/started; the final action can arrive only in item/completed. The executor must update the same block id, settle its status as done, and write the final action into tool_input. Otherwise Wework keeps showing a running web search whose expanded details are empty. Live events and historical transcripts must produce the same web_search tool block shape.
The Wework presentation layer accepts both Responses API snake_case action names (open_page, find_in_page) and Codex app-server camelCase names (openPage, findInPage). Normalize this naming difference at the tool-detail parsing boundary; do not mask missing completion events with UI placeholder content or status fallbacks.
Tool Call Duration
Tool start time, completion time, and duration come from the Codex item lifecycle forwarded by the executor. This lifecycle is the shared authoritative source for both the live stream and historical transcript. The executor must preserve millisecond-precision createdAt, completedAt, and durationMs fields in started/completed events and transcript projections, and merge the function call, command execution, and output for one invocation into a single block.
Pane caches and React components only restore and present those timing fields. A running local timer may temporarily anchor the start time from its first render to avoid jumps during incremental updates. Once the executor supplies a completion time or duration, the completed presentation must use the current block's authoritative fields. Switching panes, refreshing the transcript, or reusing a component must not retain a local completion time or start anchor from the previous pane state.
Tool Activity Preview Scrolling
The collapsed tool activity preview shows at most three rows and follows the latest activity while no tool detail is expanded. Auto-scroll must react both to changes in the tool row count and to the bottom “Thinking” row appearing or disappearing. When a tool completes without changing the row count, the thinking row must remain inside the inner scroll area's visible range. Expanding a detail removes the preview height limit, so forced scrolling must not override the user's reading position in that state.
Goal and Task Execution State
Full goal updates must go through syncRuntimeGoal. This
entry point reconciles the runtime goal with the conversation cache and projects
the accepted status into the lifecycle, keeping the goal bar and sidebar on the
same goal version. Do not write the cache and raw event status separately: when
the cache rejects an older update, the lifecycle must retain the accepted state
too. Valid updates with equal timestamps are accepted in arrival order, including
pause, block, resume, objective, and usage changes. Snapshot request versions
continue to isolate late asynchronous responses.
The center dot in the sidebar running indicator represents only an active
goal. Paused and blocked goals still exist but do not show this dot; its absence
does not mean that no goal exists.
The lifecycle Store owns Goal running presentation and elapsed time. When executionState
exists, its native phase takes precedence over the legacy running field. Only legacy
snapshots without native evidence derive an active Goal as paused from explicit
running: false; an absent field preserves unknown state. This presentation derivation
does not call the Goal pause API; persisting a pause remains a user action.
The executor's shared lifecycle transition owns Codex execution state independently of RPC
completion and message content. Task lists, task details, and runtime.turn.updated events
carry the same executionState, including execution phase, current/latest turn identity,
turn outcome, native timing, and revision. Revisions travel as decimal strings so JavaScript
does not lose nanosecond sequence precision. Viewers accept newer revisions only; old lists,
history reads, and previous RPC completions cannot overwrite a newer turn. Executor restart
never reuses an in-memory running state. The revision covers the complete snapshot: interaction, Goal, and model-selection changes advance it without inventing native progress or changing native timing. Model selection is included directly, so live events and list responses require no different client enrichment.
running denotes an executing native turn, continuing a completed turn awaiting Goal
continuation, and starting an admitted request. Continuing executions remain busy without
an active turn or a model-thinking indicator. Goal status and execution status are separate:
completing a Goal does not cancel its open turn, and completing a turn does not finish an
execution that is continuing toward its Goal.
queued means the scheduler has not admitted execution; no model turn is running.
Enqueue, cancellation, and dequeue advance the same execution revision only after queue
persistence succeeds. Cancelling admission preserves the previous turn's result, and an
older queued snapshot cannot make the cancelled task busy again.
recovering means the executor owns Goal recovery or bounded retry backoff. Recovery awaiting admission has no process binding; backoff retains process ownership and must settle when that process exits. Recovery remains busy without an active model turn; its queued legacy
task.running: false must not present that owned recovery as stopped.
Content mappers neither cache Goal status nor gate native lifecycle delivery. Native turn completion settles that turn's messages and thinking blocks. Late content can enrich a settled message but cannot restore streaming. After committing content, the native completion entry publishes the terminal event with full text and native turn identity. Clients must not synthesize an empty completion from a lifecycle snapshot. An ordinary RPC releases its own scheduling resources without repeating native successful results; it must preserve recovery journals for still-active Goal executions. Context compaction can update content routing without replacing execution state. Compaction events use the native turn identity carried by each notification, never a previous manual compaction request ID. Only a native turn start establishes the active content buffer; late text cannot reassign it.
Stopping requests cancellation and pauses the local outbound queue. A native terminal event with its turn identity settles the message. Cancellation callbacks and list refreshes can finish after the next turn starts; they must not mark the current message stopped and discard the new turn's streamed content.
After a native turn starts, successful, cancelled, and failed RPC results release request resources without repeating terminal events or changing the execution snapshot. Admission failures before native start still settle the request and report the error.
The overload recovery loop reports its exit through the same notification channel on cancellation, Goal mutation, or error, carrying the failed turn's identity. Only the matching recovery can settle; an old recovery exit cannot stop a newer turn. A successful native restart clears the pending exit notification. RPC completion does not settle recovery.
History reads obey the same native boundary. After Codex returns an admitted turn identity but before its start notification arrives, history may infer interruption for the unfinished record. The history merge protects that admitted identity before cloud outcome reporting. This changes only the history view, without advancing execution or creating an active content buffer. A leftover request binding cannot reopen a natively completed turn.
A user-only preview without native identity stays pending. Complete presentation content does not complete a turn. A synthetic done status would survive native binding, suppress streaming, and prematurely mark collaboration member activities successful.
A queued marker does not establish that optional queuePosition metadata has rendered. Desktop E2E waits for the position element before reading it, then preserves numeric and ordering assertions. Waiting for the queued icon is not an equivalent prerequisite.
The streaming ingress reads only required metadata rather than copying the complete task and completed history for each text chunk. The lifecycle reducer filters unrelated events before reading task state; routing identity and local Issue progress use read-only Store projections. Lifecycle boundaries still publish complete snapshots. Do not hide backlog by dropping events or increasing observation timeouts. Manual compaction records admission intent and request errors; native events provide its progress and turn outcome without a synthetic assistant message.
The lifecycle owner receives native events through a bounded channel with backpressure, so congestion cannot discard events. Subscriptions belong to the long-lived client and survive app-server replacement. Events and native turn executions carry a process generation; an old process exit settles only executions it owns, preserving replacement executions and unbound request admissions. Explicit stops also publish an exit boundary, while the owner keeps receiving events from the replacement process. Ordinary broadcast serves observers such as reply prediction. Request cleanup does not wait for native publication; no completion handshake or timeout fallback connects the two lifetimes. The sole native owner retains source order.
Per-turn processes for named Homes publish JSON-RPC notifications and local interaction settlements into that same native channel at their source, independently of request observers. Their process exits also carry a generation. Explicit thread unsubscription retains the existing admission lock until Codex responds, preventing a late unsubscribe from overtaking a new resume and removing the replacement execution's listener.
Interaction notifications reuse the execution entry point's effective permission profile. Automatically approved requests do not create pending user cards; read-only and stand-in confirmation limits remain intact. Local answer, cancellation, and expiry settlements enter the same native mailbox with their original turn/request identity, updating both messages and recovery items rather than reaching only the request observer. Successful requests cannot consume native content buffers early. Native completion commits the complete ordered content and removes that turn's buffer, clearing blocking questions while preserving asynchronous questions that remain unanswered across turns.
DSH session projection opens native turns from native start events. RPC admission, late user binding, and historical token usage replayed during resume cannot create turns. Restored usage updates only the cumulative baseline and cannot upload the previous turn again or advance its cloud sequence. Native results publish only for an open turn owned by that owner; historical completions and compaction notifications can update snapshots and caches without inventing new conversation results.
Native start also establishes the content turn. The request-created callback only binds user-message identity. An early binding retains a pending record without claiming model execution has started. Binding may also arrive after content or even completion, but cannot drop received text, move subsequent turns, or restore streaming on a settled turn.
When the user stops the current response for a task with an active goal, Wework must persist
paused through the runtime goal API before cancelling the current turn. This ordering disables
the automatic continuation source before the turn ends, so the goal cannot start another turn in
the window before its pause request arrives. If pausing the goal fails, Wework must not mark the
current response as stopped. While goal details are still loading, the stop flow must use the
goalStatus from the task-list snapshot to decide whether to pause; it must not skip persistence
merely because the goal bar has not rendered yet.
Task Execution, Continuity, and Unread State
The sidebar running indicator, composer state, message state, and unread reminder represent different facts and must not share one ambiguous "active" boolean:
| State | Definition | UI contract |
|---|---|---|
| Running | task.running === true | Show the sidebar spinner and pause in the current pane composer; the active turn determines the thinking indicator |
| Turn streaming | task.running === true with a streaming assistant message | Use only for streaming-message lifecycle; it must not create a second running definition for other UI states |
| Settled unread | The task transitions from running to not running while it is not the current task | Show the blue unread dot in the sidebar and optionally send one completion notification |
running is the executor's authoritative task-lifecycle state, not the state of one individual turn. While an Active Goal continuation loop is alive, the executor must continue reporting running: true even when no assistant message is streaming between two automatic turns. The sidebar spinner and composer pause button therefore remain visible without creating unread state. The message area shows continuation without inventing model thinking. If the executor restarts without restoring that execution loop, it must report running: false even when the persisted goalStatus is still active; Wework must not infer execution from the Goal status. A stale streaming message from the previous turn must not be revived.
Native execution changes only through newer executor snapshots; a turn terminal event cannot directly set running: false. Older snapshots cannot overwrite newer ones. Other runtimes without native snapshots retain their explicit execution boundaries. Execution identity is deviceId + taskId. workspacePath is routing metadata that may change between creation, refresh, and transcript recovery, so it must not participate in execution-state identity.
Unread is created only from a running-state edge for the same task: it was
running: true, it is now running: false, and it is not the currently open
task. Browser storage keeps task keys for unread results and, separately, task
keys that were last observed running so Wework can finish this edge comparison
after an abnormal application exit. The latter set exists only for unread
reminders and is not a source of current execution state; executor snapshots
and live events remain authoritative. The current task and every running task
must be excluded from visible unread state, and opening a task clears its unread
state. The new storage namespace does not import legacy unread data, so existing
tasks start read after the upgrade.
The executor's RuntimeTaskLink.running field exists only in current-process
memory and runtime API responses. runtime-work/index.json must not serialize
the field, and readers must ignore any legacy running value left in an older
index. Codex execution is determined by the current executor's owned native
execution state, independently of whether one request remains in the active-task set.
Composer Mode Indicators
When the composer is in plan mode or goal-draft mode, its bottom mode pill must show a semantic icon to the left of its label: a checklist for plan mode and a target for goal draft. Desktop and compact layouts must reuse the same mode-pill implementation so the state is expressed consistently.
The mode pill's cancel button appears only on hover and is absolutely positioned over the left icon while that icon fades out. Do not expand the cancel button or add spacing that changes the pill width, because that causes the label to shift horizontally.
Composer Draft Buffering
BufferedChatInput preserves a pane-level draft during editing and submission, while the external value remains the source of truth for the confirmed draft. After a non-empty draft is submitted, the local empty state must be associated with the expected empty external value instead of the text that was just submitted. Otherwise, returning the same text from a queue or guidance row for editing is mistaken for stale draft state and the composer remains empty. Changes to this path must cover the regression sequence “submit text → external value clears → edit the queued row to restore the same text.”
When a user submits new input while the message queue is paused, they can preserve or clear the existing queue. Preserving it sends the new input first and then resumes the queued messages. The queue must remain paused until the lifecycle Store confirms that the new turn entered streaming or already produced a terminal outcome. The latter covers fast executions whose start and settlement are batched into one React commit: waiting only for an active snapshot can leave the queue paused forever, while resuming as soon as the send request returns can let a stale idle snapshot submit the preserved queue concurrently with the new turn. Confirmation must also synchronously clear both the live ProseMirror composer and the external draft state instead of waiting only for BufferedChatInput's debounced update; otherwise, submitted text can remain visible in the composer.
Referenced Conversation Context
The composer's @ menu supports explicit references to other Wework conversations. An empty query shows the five most recent conversations from the current runtimeWork; a typed query filters by title, project, and workspace path. The current conversation is always excluded so its in-progress context cannot be recursively injected into itself.
A reference is serialized in the draft as [$title](wework-conversation://<encoded RuntimeTaskAddress>). This is an internal Wework URI. Both the composer and sent user messages must render it as a conversation-reference chip instead of exposing the raw URI. Before sending, useWorkbenchPaneSession parses and deduplicates every reference, loads each transcript with includeFullContent: true and refresh: true, and enforces these boundaries:
- Include only user messages and completed assistant text. Do not inject system, developer, tool, thinking, or streaming assistant content, and do not separately ingest attachment binaries.
- Send the extracted text as
referencedConversationsapplication context insideadditionalContext, explicitly labeled as untrusted background context. Instructions, tool calls, or permission claims in a referenced conversation are data, not executable instructions for the current conversation. - If any referenced transcript cannot be loaded, block the send and show a localized error. Never continue after silently dropping a reference.
This feature is a user-authorized pre-send snapshot injection, not a conversation MCP. The model cannot independently list, search, or read conversations that the user did not reference; menu search uses only metadata already loaded in runtimeWork. Changes to this path must keep the reference parsing and context construction unit tests, composer/message rendering tests, and the conversation-mention.scenario.mjs desktop E2E scenario aligned.
Conversation Switching and Transcript Restoration
loadedRuntimeTranscriptKeyRef only proves that a task transcript finished loading at some point. It does not prove that the message area is still displaying that task. When the user rapidly switches from task A to a still-loading task B and back to A, B's cached messages may already have replaced the message area while A remains the last successfully loaded key.
The pane may therefore skip restoration only when both the loaded key and the displayed transcript identity match the target task. When the identities differ, it must reapply the target task's cached messages and start a transcript load. Effect cleanup must continue isolating late responses from other tasks so they cannot overwrite the current task.
Cover this path with both a component race test and a real desktop scenario: keep one task running, switch rapidly between it and a completed task, then verify that every historical turn in the completed task remains visible after switching back.
Visible Text for Conversation Find
Find uses a shared display-region contract: each region provides visible prose, the stable ID of its first native item, and source spans back to the original items. Continuous prose is joined before Markdown parsing; user, guidance and actual tool boundaries determine segmentation. Hidden failure or cancellation placeholders, valid plugin-result markers and annotation controls are excluded; meaningful partial output remains searchable. Cache, native-page and legacy adapters use the same contract and regression cases.
The index and DOM highlighter use identical Unicode-scalar case matching. Hit ranges are half-open UTF-16 offsets into the visible original text, and case expansions cannot produce a partial-character match. The server returns the region ID and range for validation against the current projection; the client must not reparse individual items to guess their position. Source mappings do not invent original positions for virtual separators. Request, history-read and visible-location failures use distinct messages in the existing status control. A location failure starts a fresh search instead of reloading the stale anchor.
Cached match and context pages first select a message window using limit, then
expand it to the complete contiguous interval of every included canonical turn.
Interleaved turns expand the interval until its boundaries stabilize; cursors use
the final first and last messages. Failure, cancellation, guidance segments and
text deduplication therefore use complete projections, so pagination preserves
existing match identities and visibility. Isolated context reads do not change
the original conversation identity, cache or concurrent hydration generation.
Context pages merge in their requested direction, and closing find restores the
original reading state. Performance diagnostics are not a functional highlight
acceptance signal.
Long Output Memory Boundary
The Wework chat UI must not keep complete long-running output in React state. WorkbenchMessage.content, thinking/text/plan block content, and tool block toolOutput must enter messages through the shared preview-window path:
- After runtime stream events reach
reduceWorkbenchMessages, content above the threshold keeps only a tail preview and recordscontentTruncated,contentOriginalChars, ortoolOutputTruncated,toolOutputOriginalChars. - Stream offsets and historical metadata are only hints about the original length and cannot trigger a truncation notice by themselves. The reducer may set truncation state only when the original length actually exceeds the corresponding preview threshold; offset gaps on short content must preserve the visible text and clear invalid truncation metadata.
- Historical messages returned by
runtime.tasks.transcriptmust apply the same truncation semantics by default so refreshes or task switches do not load complete large strings back into the WebView. - When the user clicks "load full output", the frontend calls the same runtime transcript method with
includeFullContent: true. The executor returns the complete transcript withfullContent: true; the current pane replaces its preview messages with full messages and clears pagination/gap state, so later expanded controls reuse the full state instead of taking another long path. MessageListandToolBlocksDisplaymay only render the current preview and truncation notice; hiding complete content with CSS does not count as releasing memory.- Right-side temporary chats must reuse the same reducer and stream-action batching path instead of accumulating full output for temporary threads.
Transcript Order
The server-provided messageIndex is the only ordering authority for the persisted Codex transcript. Loading an older page or filling a middle gap may combine only pages from that same Provider transcript by messageIndex; it must not merge background message caches, runtimeHandle.messages, or locally matched user content into Provider pages.
While the current pane has a live turn that the transcript has not covered yet, the UI may keep displaying that pane's stream projection as a whole. Once the Provider covers the same turn, the pane switches to the transcript as a whole instead of taking the union of both message sets. Transcript-page deduplication uses only stable Provider message ids and must not infer identity from content, role, or subtask.
Codex may filter the initial user input from the Provider transcript items,
while Wework still retains the visible text that the user submitted in
RuntimeTaskLink.userMessagePresentations. When the executor restores such a
message, it must bind it to the next Provider turn on the timeline and write
the canonical turnId and subtaskId. If the user input and the first
assistant message share a timestamp, the user input must remain first.
Canonical turns are the frontend transcript's only input, so restoring a
message only in the compatibility messages array is insufficient.
Assistant In-Turn Display Order and Typography
The running transcript cache serves existing messages when reopening a task or
restoring a page. When a cached message has runtimeItems, canonical turns
must use those items directly, preserving native item IDs, types, and order,
including an empty array. Rebuilding items from presentation content and
blocks changes the text identity to an aggregate message ID and moves it
before process messages, so later deltas with the native ID create a second
text segment. Text extraction must also preserve raw whitespace: trailing spaces
or newlines in a snapshot can separate the next delta and must not be trimmed
like metadata. The streaming-content E2E restores the page after a paragraph
break in a partial plain-text response and then resumes output, checking cached
items, text uniqueness, and process placement before completion as well as the
final state.
While a Provider turn is running, assistant text without a phase must remain process text, even if the snapshot does not yet include a later tool. An explicit final phase can render as answer text immediately. Unphased trailing text can become the final answer only when the turn completes, so an in-flight transcript page cannot prematurely reclassify process text.
Final text, process text, and tool blocks within one assistant turn must be projected in runtime item arrival order. The UI must not group them by type and then render them in a fixed layout. In particular, when final text arrives before a later process update, that process update must appear below the final text, and transcript restoration must preserve the same order.
The thinking indicator, process body text, and final answer body all use the
semantic text-chat size. Tool summaries, timestamps, and other metadata may
retain their compact roles, but a chat body must not change font size when it
moves between streaming and completed states because that causes a visible
flash.
Before final answers enter the Markdown renderer, Wework must remove
cite… content-reference markers that have no structured citation metadata,
including an unfinished trailing marker during streaming. These internal
protocol characters must never appear as ordinary response text. They may be
converted into visible citations only after the matching metadata and
interaction component are available.
Assistant Markdown images in Electron chat may reference a local absolute path
or a file:// URL. The renderer must read the file through the Electron file
bridge and display it through a Blob URL; an HTTP page must not load the
file:// resource directly. The image file must still exist when the message
is rendered, and read failures show an explicit image error placeholder. After
changing this path, run the CI-covered desktop rendering-extensions
checkpoint, which reads a real PNG from the system temporary directory and
asserts that the final image uses a Blob URL.
Guidance Message Order
Running Codex LocalTasks can send a queued message as native guidance. Guidance is user input inside the current turn, not a new follow-up turn, so the UI must insert the local user message inside the active assistant as soon as guidance sending starts:
- Mark the matching
queuedMessagesitem assendingand show the "正在引导当前对话" notice. - Create the user message with the same local message id and
createdAt, then split the current streaming assistant into two messages. - Freeze the before-guidance assistant as done and remove its
subtaskId, so later stream events cannot write into it. - Keep the original
subtaskIdon the after-guidance assistant, and insert aconversation_guidancetool block first to mark the guidance position. - Later
chat:chunkandchat:doneevents may carry full text, so trim the assistant text prefix recorded at split time before sending them to the reducer.
Do not append the user message to the bottom after guidance succeeds, and do not wait for runtime.tasks.guidance to return before splitting the assistant. Assistant text generated while the guidance request is waiting would otherwise appear before the user guidance message, making live streaming order differ from refreshed transcript order.
If the source pane is unmounted when guidance is applied, the background subscriber must remove the matching item from queuedMessages and write the confirmed user message into the in-memory live projection in runtimeConversationCache.messages. The background path must reuse the foreground AppliedRuntimeGuidanceMessage constructor and assistant-splitting entry point; it must never append with [...messages, guidance], which places the user message after the entire running assistant when the source conversation is reopened. Split-prefix boundaries must be shared by conversation key so a foreground chat:done can still trim the already rendered assistant prefix after a background split. Reopening the source conversation before the Codex transcript covers the running turn must still show that confirmed message. Once the Provider transcript covers the same turn, it takes over the content and ordering as a whole. The cache is neither persisted nor merged into Provider transcript pages, so it does not become a second persistent transcript source.
After inserting guidance, the message area must scroll to the bottom and briefly maintain stable bottom-following even if the user had previously scrolled upward, so the new user message and assistant continuation remain visible. This forced scroll applies only to newly applied guidance in the current conversation; loading a historical page that contains older guidance must preserve the user's current viewport anchor.
Right-Side Temporary Chats
The right workspace Temporary chat feature starts a short side conversation next to the current local Codex thread. It is not a fork and it is not a normal runtime task shown in the left task list:
- Creating a new runtime task from either the project-space board task modal or the project-space task tab must call
useProjectRuntimeTaskComposerand then follow the samecreateProjectRuntimeTaskpath.TemporaryChatPanelconstructs one optimistic user message with a stable id and passes that same message object into the creation path.sendPreparedRuntimeMessageowns forwarding the id to the executor and writing the message toruntimeConversationCache. Entry components must not append the first message independently, or the live UI can retain both the local message and the transcript message until refresh. - Each temporary chat tab has an independent
chat:<id>instance id, so the right workspace can hold multiple temporary chats at the same time. - Before a runtime thread exists,
TemporaryChatPaneluses the instance id as itsconversationKey. After creation, pane workspace state retains the tab's runtime address andruntimeConversationCacherestores its live message projection. Unobserved terminal conversations can expire after five minutes. When the cache is missing, the panel loads the executor's side-chat content through the sharedruntime.tasks.transcriptpath, retaining loading, retry, and stale-request isolation. Reading the native fork's full history directly would mix inherited main-conversation content into the side chat. TemporaryChatPanelshares normal DOM scrolling coordinates with the main conversation: zero at the top andscrollHeight - clientHeightat the bottom of overflowing content. Short threads start at the top. When existing long conversations first appear or reopen, the virtualizer restores the bottom or saved reading position within the layout commit instead of painting a top position and visibly correcting downward.- Attachment selection, upload progress, and errors are also isolated per temporary-chat instance and must not reuse the main composer attachment state. The first message passes that instance's attachments explicitly to
createTemporaryRuntimeTask. - Every successfully sent or optimistically displayed user message must retain its persisted attachment references, including the first message, regular follow-ups, and queued sends. Clearing composer attachments only resets the current input state and must not remove sent attachments from the message list; local
blob:preview URLs must be converted to recoverable local paths. - When a temporary chat is the only open right-workspace tab, the panel defaults to a compact
420pxwidth. Opening another workspace tab restores the general split default, while a user-resized width remains authoritative. - The first message calls
createTemporaryRuntimeTask, creating anephemeralruntime task with the current main thread assideSource. This flag controls task visibility: the task does not enter the left task list or navigate the main pane. For new reusable side chats, the executor savespersist_runtime_threadin the execution request and creates a normal persisted Codex thread. Task visibility and native thread persistence are separate decisions. - Follow-ups in new side chats resume the same native thread, including after capability changes restart the Codex app-server. Legacy in-memory side chats continue through
direct_thread_id; if their thread is lost, report that a new temporary chat is required. Never fabricate history or silently start a new thread without the original context. Displayed content still comes from the executor's side-chat cache; this path does not guarantee content restoration after the entire executor restarts. - Reply prediction resumes and forks only recoverable source threads; legacy in-memory side chats return no suggestion. Prediction forks and one-shot text generation remain native ephemeral threads and must not inherit the reusable side chat's persistence marker.
- Capability synchronization detects changes in effective content. Repeating identical content must not advance the capability revision. Real changes, including changes left by partial failures, must become effective on the next turn; stale requests and successful rollbacks must not trigger restarts merely because a synchronization ran.
- A regular follow-up must write its user message into the conversation cache before awaiting
runtime.tasks.sendMessage, keeping it ahead of the current turn's Thinking indicator. A failed send removes that same client message id from the cache. TemporaryChatPanelmust preserve the running-send options supplied byBufferedChatInput. When the user selects Guide current response or sends a queued row as guidance, the temporary chat must callruntime.tasks.guidanceand settle the matching queue item byclientGuidanceId; it must not downgrade guidance to a regular follow-up after the active turn.- Temporary chats reuse only the current workspace and current thread context. If no main thread source is available, sending should be blocked and the user should be asked to open an existing conversation first.
- Temporary chats default to lightweight, non-mutating exploration. Parent-thread messages, plans, and tool results before the side boundary are reference-only and must not be continued. If the user explicitly requests a file, source, Git, configuration, or workspace mutation after the boundary, the temporary chat may perform it within the thread's existing permissions, keeping the change minimal and local to the request. Without an explicit mutation request, it must not write or seek broader permissions.
- After a runtime-work refresh, the reducer must hydrate the current task address with the authoritative
threadId/runtimeHandlefrom the same device and task. Keeping an optimistic address without its thread merely because the device is still online prevents the temporary chat from establishingsideSource.
Maintenance rule: do not add UI fallbacks that insert temporary chats into the left task list, and do not fabricate rollout records in the executor. Reusable temporary chats use hidden runtime tasks, sideSource, and recoverable native threads; one-shot auxiliary generation uses native ephemeral threads.
The CI regression entry is pnpm --filter wework e2e:desktop --segment temporary-chat; run it locally only when explicitly requested. The independent real Electron scenario holds an assistant response open, asserts that a regular follow-up stays above the Thinking indicator, verifies normal scrolling coordinates and bottom anchoring when messages first appear and after reopening the conversation, and confirms that both temporary-chat user messages are restored after switching back. It writes screenshots for each critical stage to wework/test-results/desktop-e2e/<run-id>/.
Top-Level Page Transitions
The workbench owns live state that cannot be serialized reliably, including composer drafts, Terminal sessions, and the in-app browser. When users move from the workbench to plugins, apps, or iframe apps, AppRoutes must keep WorkbenchProvider and WorkbenchPage mounted and only hide the workbench surface. Returning to the workbench then reuses the original component instances. A direct visit to an auxiliary page may defer the initial workbench mount to avoid creating unused background sessions.
Do not unmount the workbench during route transitions, and do not add incomplete restoration fallbacks for Terminal or browser state. New top-level pages should join the auxiliary-page rendering branch without changing the workbench lifecycle.
Multiple top-level document tabs use React Activity to retain independent
workbench instances. Updates inside a hidden Activity may be deferred while
portals that it created in global titlebar targets remain attached. Every
global titlebar portal must therefore identify its owning document tab, and
AppRoutes must control portal visibility from the active-tab state. Do not
rely only on conditional rendering inside the hidden workbench to withdraw a
portal. After a tab switch, only the active tab may expose its main header,
panel actions, right-workspace title, and feedback entry.
Workbench Pane Cache
The desktop workbench caches up to 20 regular panes so messages, composer drafts, and local UI state survive switches between parallel tasks. Once the limit is exceeded, inactive panes are evicted in least-recently-used order. Panes for running tasks and panes with pinned terminals remain mounted outside the regular cache limit until the task finishes or the terminal is unpinned. Maintain this boundary through the existing CachedWorkbenchPaneStack LRU and pinning mechanisms; do not add a second pane cache in the layout.
The message area stores each task's reading position by conversationKey. During a task switch, restoration realigns the saved message anchor throughout the layout stabilization window; programmatic scroll events in that window must not overwrite the snapshot. An explicit wheel or touch gesture must cancel restoration immediately. Changes to this path must cover the real desktop flow “scroll to the middle of a long response → switch to another task → switch back” and retain screenshots from before the switch, after the switch, and after restoration.
Project Task Status Synchronization
After a Wework runtime task is bound to a project-space task, the frontend keeps exactly one status synchronization path:
RuntimeTaskLifecycleStoreproduces the lifecycle snapshot for the current runtime task.WorkbenchProviderconverts that snapshot intoruntimeTaskTrackingStatusand callsreconcileProjectTaskTrackingStatus.reconcileProjectTaskTrackingStatusderives one target status from the bound task's current status and execution status, then updates it through the project-space storage owner.- The fixed Task tab is the only UI owner of this synchronization path. Changing the bound task reruns the same reconciler and must not write status directly.
Running execution maps to in_progress. Terminal outcomes such as success, failure, and cancellation map to in_review for user confirmation. Archiving also reuses the same reconciler for final convergence. Backend runtime event projection owns project automation workflow status, so the renderer must not issue a separate status PATCH that competes with it.
The following paths are obsolete and must not be restored:
- scanning and replaying task status during renderer startup;
- exposing replay RPCs such as
runtime.tasks.status.replayin Wework, Backend, or Executor; - letting the renderer write cloud workflow status through
cloud-context/status; - letting boards, detail views, or other tabs infer and write task status from local messages or runtime lists.
After changing this path, run pnpm --filter wework e2e:desktop -- --segment task-status-sync. This real Electron checkpoint binds the fixed task and verifies that a running task appears only in the In Progress column, then appears only in the Awaiting Confirmation column after execution settles.
Audit Result
- Desktop and mobile layouts no longer scan
messagesdirectly to decide whether the assistant is streaming; they readpaneSession.status.isAssistantStreaming. - Composer disabled state no longer reads independent
paneSession.sending; it readspaneSession.status.isSubmitting. - Message waiting indicators no longer combine
sending || waitingForAssistant; they readpaneSession.status.isWaitingForAssistantIndicator. - Queue advancement no longer uses scattered
currentRuntimeTask && !busy; it readspaneSession.status.canSendQueuedMessage. - Sidebar, composer, and message-area task status all subscribe to
RuntimeTaskLifecycleStore; none readsruntimeWork.runningdirectly. - Optimistic send state and executor-confirmed state use the same task machine, so send acknowledgement and stream races cannot create separate frontend authorities.
runtimePaneStatus.tsonly projects the lifecycle snapshot into compatibility presentation fields; it does not maintain task or turn state.
Maintenance Rules
Display Clock Update Boundaries
Tool duration is presentation state: ToolDurationText owns the 0.1-second clock in a leaf component. Clock ticks must not rerender labels, command parsing, expanded output, or the tool list. Non-tool blocks and branches that do not display a duration must not create timers.
The summary header owns the thinking-only duration and updates once per displayed second. When tool activity hides the summary duration, it neither formats that duration nor starts a timer. The list computes tool classification statistics once and shares them with the header; unchanged rows do not need reclassification.
packages/collaboration/src/conversation/ToolBlocks.performance.test.tsx advances individual clock ticks without new business data and asserts that classification, labels, and expanded details are not recomputed. It also covers input/output updates, completion, resumption, and unmount cleanup. Real Electron regression coverage reuses the streaming-tools and streaming-timers checkpoints. These local optimizations do not establish a process-wide CPU reduction; measure that with the same task and recording conditions.
Lifecycle State
- Define native lifecycle transitions in the executor owner first, then expose versioned snapshots through the Store and derive UI fields in
RuntimeTaskMachine. - Do not recompute task running, turn activity, busy, or unread state in layouts, pane hooks, or components.
- Do not add independent
isSending,isRunning, orisStreamingReact state. New external facts must enter through Store event routing. - When runtime work and message state disagree, do not override display inside UI components and do not add fallback settlement; fix the primary path.