Skip to content

feat(wework): add runtime project archive workflow - #1547

Merged
qdaxb merged 3 commits into
wecode-ai:mainfrom
icycrystal4:codex/wework-project-archive-state-sync
Jun 25, 2026
Merged

qdaxb merged 3 commits into
wecode-ai:mainfrom
icycrystal4:codex/wework-project-archive-state-sync

Conversation

@icycrystal4

@icycrystal4 icycrystal4 commented Jun 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add device-side runtime archived conversation APIs across backend and executor, including list/archive/unarchive/delete/bulk delete flows.
  • Update Wework sidebar/settings archive UX, project/conversation archive controls, Codex global project state handling, and remove the old unmapped workspace assumptions in runtime work state.
  • Document archived conversations as device-side runtime state in the runtime local work guide.

Tests

  • python3 -m py_compile backend/app/api/endpoints/runtime_work.py backend/app/schemas/runtime_work.py backend/app/services/runtime_work_service.py executor/runtime_work/rpc_handler.py executor/runtime_work/codex_discovery.py executor/runtime_work/codex_global_state.py
  • cd backend && uv run pytest tests/api/endpoints/test_runtime_work_api.py tests/services/test_runtime_work_service.py -q
  • cd executor && uv run pytest tests/runtime_work/test_local_task_store.py tests/agents/test_codex_config_builder.py -q
  • pnpm --dir wework exec eslint src/api/runtimeWork.ts src/types/api.ts src/components/layout/DesktopSidebar.tsx src/components/layout/DesktopSidebar.test.tsx src/components/layout/DesktopWorkbenchLayout.tsx src/components/settings/ArchivedConversationsSettingsPage.tsx
  • pnpm --dir wework exec vitest run src/api/runtimeWork.test.ts src/components/layout/DesktopSidebar.test.tsx src/components/layout/DesktopWorkbenchLayout.test.tsx src/components/settings/MobileSettingsPage.test.tsx src/features/workbench/WorkbenchProvider.test.tsx src/components/layout/WorkbenchSearchDialog.test.tsx
  • AI_VERIFIED=1 git push pre-push gate: passed

Summary by CodeRabbit

  • New Features
    • Added an “Archived Conversations” settings page with search, source filtering, and grouped results.
    • Enabled archived conversation actions: view/list, archive/unarchive, delete (single), and bulk delete.
    • Added runtime task renaming plus runtime workspace rename/remove.
    • Added optional labels for opening standalone runtime workspaces and improved remote workspace presentation.
  • Bug Fixes
    • Updated runtime work list rendering to consistently surface chat/conversation items separately.
    • Improved archive/rename user flows with confirmation, undo behavior, and clearer error handling.

sifang added 2 commits June 25, 2026 12:33
…archive-state-sync

# Conflicts:
#	backend/app/api/endpoints/runtime_work.py
#	backend/app/schemas/runtime_work.py
#	backend/app/services/runtime_work_service.py
#	backend/tests/api/endpoints/test_runtime_work_api.py
#	backend/tests/services/test_runtime_work_service.py
#	executor/runtime_work/rpc_handler.py
#	executor/tests/runtime_work/test_local_task_store.py
#	wework/src/api/runtimeWork.ts
#	wework/src/components/layout/DesktopSidebar.test.tsx
#	wework/src/components/layout/DesktopSidebar.tsx
#	wework/src/components/layout/DesktopWorkbenchLayout.tsx
#	wework/src/types/api.ts
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 236163b9-e417-4263-8ab2-34b477c31976

📥 Commits

Reviewing files that changed from the base of the PR and between bed3508 and 7d823d8.

📒 Files selected for processing (2)
  • executor/runtime_work/codex_global_state.py
  • executor/tests/runtime_work/test_local_task_store.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • executor/runtime_work/codex_global_state.py
  • executor/tests/runtime_work/test_local_task_store.py

📝 Walkthrough

Walkthrough

The PR adds runtime-work archive, rename, and workspace mutation flows to backend and frontend APIs, updates executor Codex state and discovery handling, and propagates the chats runtime-work shape through workbench, settings, docs, and tests.

Changes

Runtime Work Archives and Workspace Renames

Layer / File(s) Summary
Contracts and API surface
backend/app/api/endpoints/runtime_work.py, backend/app/schemas/runtime_work.py, backend/tests/api/endpoints/test_runtime_work_api.py, wework/src/types/api.ts, wework/src/api/runtimeWork.ts, wework/src/api/runtimeWork.test.ts
RuntimeWorkListResponse switches to chats, archived-conversation and workspace rename/remove models are added, and the router/client surface the new POST endpoints with tests.
Backend runtime-work service
backend/app/services/runtime_work_service.py, backend/tests/services/test_runtime_work_service.py
Runtime-work listing now carries workspace metadata and ordering, and the service adds task rename, archive/unarchive/delete, and workspace rename/remove handlers with matching coverage.
Executor Codex runtime support
executor/agents/codex/config_builder.py, executor/runtime_work/*.py, executor/tests/agents/test_codex_config_builder.py, executor/tests/runtime_work/test_local_task_store.py
Codex config, global state, discovery, RPC dispatch, and local task storage add archived-thread handling, workspace registration, rename/delete paths, and executor test fixtures.
Workbench runtime data flow
wework/src/features/workbench/*, wework/src/lib/workspace-target.*, wework/src/lib/project-workspace-selection.test.ts, wework/src/pages/*.tsx, wework/src/components/layout/MobileDrawer.tsx, wework/src/components/layout/MobileWorkbenchLayout.*, wework/src/components/chat/*test.tsx, wework/src/components/chat/composer/ProjectWorkBar.test.tsx, wework/src/components/layout/TaskForkDialog.test.tsx
Workbench state, routing, and mobile layouts switch to runtimeWork.chats, add label-aware standalone workspace opening, and expose archive/rename actions across runtime tasks and projects.
Shared controls and project dialogs
wework/src/components/common/*, wework/src/components/projects/*, wework/src/components/layout/DesktopSidebar.*
Menu items, text dialogs, project icons, standalone workspace dialogs, folder picker confirmation flows, and sidebar project/task controls add disabled states, async submission handling, remote icons, and label propagation.
Desktop shell and sidebar
wework/src/components/layout/DesktopWorkbenchLayout.*, wework/src/components/layout/DesktopSidebar.test.tsx, wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
Desktop sidebar and layout wiring add archive-confirm dialogs, delayed task archiving, rename/remove actions, and updated callback/test coverage for the new workbench props.
Archived conversations settings and navigation
wework/src/components/settings/*, docs/*/developer-guide/runtime-local-work.md, wework/src/i18n/locales/*/common.json
Archived-conversations settings pages, navigation entries, docs, and locale strings add list, filter, group, unarchive, delete, and mobile entry points.
Utility formatting and helpers
wework/src/lib/model-ui.*, wework/src/features/workbench/turnFileChanges.*
Model UI helpers and turn-file normalization receive formatting-only changes without behavior changes.

Sequence Diagram(s)

sequenceDiagram
  participant runtime_work_service
  participant runtime_work_rpc_handler
  participant CodexSessionDiscovery
  participant sqlite3
  runtime_work_service->>runtime_work_rpc_handler: runtime.archived_conversations.delete
  runtime_work_rpc_handler->>CodexSessionDiscovery: delete_archived_thread(...)
  CodexSessionDiscovery->>sqlite3: _delete_archived_thread_state(...)
  CodexSessionDiscovery->>sqlite3: _delete_codex_thread_child_rows(...)
  sqlite3-->>CodexSessionDiscovery: deleted rows
  CodexSessionDiscovery-->>runtime_work_rpc_handler: deletedState / deletedFiles
  runtime_work_rpc_handler-->>runtime_work_service: archived delete response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • wecode-ai/Wegent#1502: Introduced the runtime-work stack that this PR extends with archived conversations and workspace rename/remove flows.
  • wecode-ai/Wegent#1530: Shifted the runtime-work response shape toward the projects/chats/totalLocalTasks model used here.
  • wecode-ai/Wegent#1538: Touched the shared runtime-work workspace surface and open_runtime_workspace, which this PR extends with label and workspace mutation behavior.

Suggested reviewers

  • qdaxb

Poem

🐰 I hopped through chats and archived trails,
Renamed the signs by moonlit rails.
With workspace roots and labels bright,
The burrow hums in tidy light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.29% 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 clearly matches the main change: adding a runtime project archive workflow in Wework.
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.

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

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

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

❤️ Share

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

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

🤖 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 `@backend/app/services/runtime_work_service.py`:
- Line 2507: The Ruff E712 issue is in the boolean filter expressions using
explicit True comparisons. Update the SQLAlchemy condition in
runtime_work_service.py around the relevant Project.is_active check, and any
similar boolean comparisons like the one near the other flagged line, to use
direct truthiness or .is_(True) so the filters remain SQLAlchemy-compatible and
pass CI. Use the existing Project.is_active predicate and related query-building
code to locate and replace the offending comparisons.

In `@backend/tests/services/test_runtime_work_service.py`:
- Around line 1029-1040: The wall-clock check in the runtime work test is too
tight and can flake under CI load. In the test around list_runtime_work and
rpc.await_count, relax the timing assertion by using a more tolerant bound based
on the expected serialized cost (for example, compare against the 2 * 0.2
baseline instead of a near-edge 0.35 cutoff) while keeping the rpc.await_count
== 2 assertion to verify both calls were made.

In `@executor/runtime_work/codex_global_state.py`:
- Around line 287-290: The global state update flow in codex_global_state.py
currently treats a missing file and an unreadable existing file the same because
mutate() uses _read_global_state_payload(...) or {}. Update the read path around
_read_global_state_payload and mutate so a parse/read failure is detected
separately from “file not found,” and abort or surface the error instead of
starting from an empty payload. Keep the existing state intact when
CODEX_GLOBAL_STATE_FILENAME exists but cannot be read.

In `@executor/runtime_work/rpc_handler.py`:
- Around line 1498-1504: The Codex rename path in rpc_handler should tolerate
missing discovery support the same way archive/unarchive/delete do, because
calling codex_discovery.rename_thread can fail on limited adapters before the
local title change is persisted. In the rename handling block around
_is_codex_runtime and _sdk_codex_thread_id, guard the rename_thread call behind
a capability check or missing-method fallback, and skip or safely no-op when the
discovery adapter does not support rename while still allowing the local title
override to save.

In `@executor/tests/runtime_work/test_local_task_store.py`:
- Around line 2353-2354: The thread_source assertions are too strict because
_codex_thread_source_user() may return either an enum or the plain string
"user"; update the affected checks in the local task store tests to normalize
both forms before comparing, using the existing
fake_codex.thread_start_kwargs["thread_source"] and result["threadId"]
assertions as the place to adjust. Apply the same enum-or-string handling
consistently across the other matching test pairs so the tests accept either
representation without relying only on .value.

In `@wework/src/components/layout/DesktopSidebar.tsx`:
- Around line 795-800: Runtime task actions in DesktopSidebar are only exposed
through double-click and a hover-only action container, so make the rename,
notify, and archive controls reachable via keyboard and touch by using
persistent interactive controls with proper click/keydown handling instead of
hover-only behavior. Update the runtime task action area and the rename trigger
in DesktopSidebar so these actions can be focused and activated without a mouse,
while preserving or adding data-testid attributes on the interactive elements
for E2E coverage.
- Around line 996-1017: The archive flow is mixing the raw optional project key
with the runtime key helper, which can leave legacy runtime projects unhandled
and allow undefined keys into the bulk archive payload. Update DesktopSidebar’s
archive-related logic to consistently use runtimeProjectKey() wherever archive
eligibility or archive handlers are computed, including the
confirmArchiveProjectConversations path and the bulk archive selection logic.
Keep the existing guards, but replace direct runtimeProjectWork?.project.key
usage with the helper so the archive actions always receive a valid runtime
project key.

In `@wework/src/components/layout/MobileWorkbenchLayout.tsx`:
- Around line 83-87: The new optional label parameter on
onOpenStandaloneWorkspace is not being forwarded in the folder-picker flow, so
update StandaloneProjectDialogs.tsx to pass a meaningful label when opening an
existing folder. Use the existing result object or derive a label from the
selected path, and ensure the call site matches the updated
onOpenStandaloneWorkspace signature from MobileWorkbenchLayout.

In `@wework/src/components/settings/ArchivedConversationsSettingsPage.tsx`:
- Around line 162-170: The project filter in visibleItems is using a different
key derivation than the select options, which can make matching items disappear.
Update the filter logic in ArchivedConversationsSettingsPage to compare against
the same project key shape produced by projectOptionKey (including the
projectId-derived and workspacePath cases), rather than only using
item.projectKey || item.projectName. Keep the key generation consistent between
the <select> value and the visibleItems memo so selecting any project option
returns the expected items.
- Around line 175-206: `handleUnarchive` and `handleDelete` in
`ArchivedConversationsSettingsPage` currently await API calls without handling
failures, so rejected requests become unhandled and provide no UI feedback. Add
`catch` handling around the `api.unarchiveConversation` and
`api.deleteArchivedConversation` calls, and set the existing `error` state on
failure to match `loadArchivedConversations` behavior. Keep `setBusyKey(null)`
in `finally` so the spinner always clears, and preserve the existing reload flow
only on success.

In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Around line 1914-1974: The type narrowing for resolvedServices.runtimeWorkApi
is lost inside the Promise.all callbacks in archiveProjectsConversations and
archiveChatConversations. After the null check, assign
resolvedServices.runtimeWorkApi to a local constant and use that captured
reference for archiveProjectConversations and archiveConversation so the
narrowed type is preserved inside the nested map callbacks.

In `@wework/src/i18n/locales/en/common.json`:
- Around line 70-73: The common.json locale object has duplicate keys for
rename_project and chat_name, which causes the later declarations to override
the new values. Update the en/common.json object to keep only one definition of
each key by removing the duplicates from the earlier block or consolidating them
with the existing entries later in the file, and ensure the remaining keys are
unique throughout the object.
🪄 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

Run ID: fbd2f4ca-98da-489e-bc2a-7f1877419222

📥 Commits

Reviewing files that changed from the base of the PR and between 6342ce3 and bed3508.

📒 Files selected for processing (56)
  • backend/app/api/endpoints/runtime_work.py
  • backend/app/schemas/runtime_work.py
  • backend/app/services/runtime_work_service.py
  • backend/tests/api/endpoints/test_runtime_work_api.py
  • backend/tests/services/test_runtime_work_service.py
  • docs/en/developer-guide/runtime-local-work.md
  • docs/zh/developer-guide/runtime-local-work.md
  • executor/agents/codex/config_builder.py
  • executor/runtime_work/codex_discovery.py
  • executor/runtime_work/codex_global_state.py
  • executor/runtime_work/local_task_store.py
  • executor/runtime_work/rpc_handler.py
  • executor/tests/agents/test_codex_config_builder.py
  • executor/tests/runtime_work/test_local_task_store.py
  • wework/src/api/runtimeWork.test.ts
  • wework/src/api/runtimeWork.ts
  • wework/src/components/chat/ChatInput.test.tsx
  • wework/src/components/chat/ConversationQueuePanel.tsx
  • wework/src/components/chat/FileChangesReviewPanel.tsx
  • wework/src/components/chat/MessageList.tsx
  • wework/src/components/chat/composer/ProjectWorkBar.test.tsx
  • wework/src/components/common/ActionMenu.tsx
  • wework/src/components/common/TextInputDialog.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/DesktopWorkbenchLayout.tsx
  • wework/src/components/layout/MobileDrawer.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.test.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.tsx
  • wework/src/components/layout/TaskForkDialog.test.tsx
  • wework/src/components/projects/DeviceFolderPicker.tsx
  • wework/src/components/projects/ProjectFolderIcon.test.tsx
  • wework/src/components/projects/ProjectFolderIcon.tsx
  • wework/src/components/projects/StandaloneProjectDialogs.tsx
  • wework/src/components/settings/ArchivedConversationsSettingsPage.tsx
  • wework/src/components/settings/ConnectionsSettingsPage.tsx
  • wework/src/components/settings/MobileSettingsPage.test.tsx
  • wework/src/components/settings/MobileSettingsPage.tsx
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/features/workbench/turnFileChanges.test.ts
  • wework/src/features/workbench/turnFileChanges.ts
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/lib/model-ui.test.ts
  • wework/src/lib/model-ui.ts
  • wework/src/lib/project-workspace-selection.test.ts
  • wework/src/lib/workspace-target.test.ts
  • wework/src/lib/workspace-target.ts
  • wework/src/pages/PluginManagementPage.tsx
  • wework/src/pages/PluginsPage.tsx
  • wework/src/pages/WorkbenchPage.tsx
  • wework/src/types/api.ts

.filter(
Project.user_id == user_id,
Project.id.in_(project_ids),
Project.is_active == True,

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.

📐 Maintainability & Code Quality | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n backend/app/services/runtime_work_service.py | sed -n '2500,2515p'

Repository: wecode-ai/Wegent

Length of output: 797


🏁 Script executed:

cd backend && uv run ruff check app/services/runtime_work_service.py --select E712

Repository: wecode-ai/Wegent

Length of output: 194


🏁 Script executed:

cd backend && python3 -c "from app.services.runtime_work_service import *; print('Import successful')"

Repository: wecode-ai/Wegent

Length of output: 416


🏁 Script executed:

pip install ruff --quiet 2>/dev/null && python3 -m ruff check backend/app/services/runtime_work_service.py --select E712 --output-format=text

Repository: wecode-ai/Wegent

Length of output: 372


🏁 Script executed:

pip install ruff --quiet 2>/dev/null && python3 -m ruff check backend/app/services/runtime_work_service.py --select E712 --output-format=concise

Repository: wecode-ai/Wegent

Length of output: 628


Fix the Ruff E712 boolean comparisons

Lines 1471 and 2507 use == True, which violates PEP 8 and fails CI. Switch to direct truthiness or .is_(True) for SQLAlchemy compatibility:

Proposed fix
-            Project.is_active == True,
+            Project.is_active.is_(True),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Project.is_active == True,
Project.is_active.is_(True),
🧰 Tools
🪛 Ruff (0.15.18)

[error] 2507-2507: Avoid equality comparisons to True; use Project.is_active: for truth checks

Replace with Project.is_active

(E712)

🤖 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 `@backend/app/services/runtime_work_service.py` at line 2507, The Ruff E712
issue is in the boolean filter expressions using explicit True comparisons.
Update the SQLAlchemy condition in runtime_work_service.py around the relevant
Project.is_active check, and any similar boolean comparisons like the one near
the other flagged line, to use direct truthiness or .is_(True) so the filters
remain SQLAlchemy-compatible and pass CI. Use the existing Project.is_active
predicate and related query-building code to locate and replace the offending
comparisons.

Source: Linters/SAST tools

Comment on lines +1029 to +1040
started_at = time.perf_counter()
response = await runtime_work_service.list_runtime_work(
db=test_db,
user_id=test_user.id,
)
elapsed = time.perf_counter() - started_at

assert response.projects == []
assert response.chats == []
assert response.total_local_tasks == 0
assert rpc.await_count == 2
assert elapsed < 0.35

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wall-clock timing assertion may flake in CI.

assert elapsed < 0.35 with two concurrent asyncio.sleep(0.2) calls leaves only ~0.15s headroom. Under a loaded runner or slow event-loop scheduling this can exceed the threshold and fail intermittently. The rpc.await_count == 2 assertion already proves both devices were dispatched; the concurrency guarantee can be made more robust by widening the margin (e.g. compare against the serial lower bound of 2 * 0.2) rather than a fixed near-boundary value.

🧪 Suggested more tolerant bound
-    assert rpc.await_count == 2
-    assert elapsed < 0.35
+    assert rpc.await_count == 2
+    # Concurrent dispatch must be well below the serial cost (2 * 0.2s).
+    assert elapsed < 0.35  # serial would be >= 0.4s
🤖 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 `@backend/tests/services/test_runtime_work_service.py` around lines 1029 -
1040, The wall-clock check in the runtime work test is too tight and can flake
under CI load. In the test around list_runtime_work and rpc.await_count, relax
the timing assertion by using a more tolerant bound based on the expected
serialized cost (for example, compare against the 2 * 0.2 baseline instead of a
near-edge 0.35 cutoff) while keeping the rpc.await_count == 2 assertion to
verify both calls were made.

Comment on lines +287 to +290
codex_home = Path(codex_home).expanduser()
path = codex_home / CODEX_GLOBAL_STATE_FILENAME
payload = _read_global_state_payload(codex_home) or {}
mutate(payload)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not overwrite an unreadable existing Codex state file.

_read_global_state_payload(...) or {} treats “missing file” and “invalid existing file” the same, so a parse/read failure can clobber the user’s Codex global state on the next mutation.

Proposed fix
     codex_home = Path(codex_home).expanduser()
     path = codex_home / CODEX_GLOBAL_STATE_FILENAME
-    payload = _read_global_state_payload(codex_home) or {}
+    payload = _read_global_state_payload(codex_home)
+    if payload is None and path.exists():
+        raise ValueError("Cannot update invalid Codex global state")
+    if payload is None:
+        payload = {}
     mutate(payload)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
codex_home = Path(codex_home).expanduser()
path = codex_home / CODEX_GLOBAL_STATE_FILENAME
payload = _read_global_state_payload(codex_home) or {}
mutate(payload)
codex_home = Path(codex_home).expanduser()
path = codex_home / CODEX_GLOBAL_STATE_FILENAME
payload = _read_global_state_payload(codex_home)
if payload is None and path.exists():
raise ValueError("Cannot update invalid Codex global state")
if payload is None:
payload = {}
mutate(payload)
🤖 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 `@executor/runtime_work/codex_global_state.py` around lines 287 - 290, The
global state update flow in codex_global_state.py currently treats a missing
file and an unreadable existing file the same because mutate() uses
_read_global_state_payload(...) or {}. Update the read path around
_read_global_state_payload and mutate so a parse/read failure is detected
separately from “file not found,” and abort or surface the error instead of
starting from an empty payload. Keep the existing state intact when
CODEX_GLOBAL_STATE_FILENAME exists but cannot be read.

Comment on lines +1498 to +1504
codex_rename: Optional[dict[str, Any]] = None
if self._is_codex_runtime(task.runtime) and self.codex_discovery is not None:
thread_id = self._sdk_codex_thread_id(task) or task.local_task_id
codex_rename = self.codex_discovery.rename_thread(
thread_id,
normalized_title,
)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard optional Codex rename support.

Archive/unarchive/delete already tolerate missing discovery methods; rename should do the same or a limited discovery adapter returns internal_error before the local title override is saved.

Proposed fix
         codex_rename: Optional[dict[str, Any]] = None
         if self._is_codex_runtime(task.runtime) and self.codex_discovery is not None:
             thread_id = self._sdk_codex_thread_id(task) or task.local_task_id
-            codex_rename = self.codex_discovery.rename_thread(
-                thread_id,
-                normalized_title,
-            )
+            rename_thread = getattr(self.codex_discovery, "rename_thread", None)
+            if callable(rename_thread):
+                codex_rename = await self._maybe_await(
+                    rename_thread(thread_id, normalized_title)
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
codex_rename: Optional[dict[str, Any]] = None
if self._is_codex_runtime(task.runtime) and self.codex_discovery is not None:
thread_id = self._sdk_codex_thread_id(task) or task.local_task_id
codex_rename = self.codex_discovery.rename_thread(
thread_id,
normalized_title,
)
codex_rename: Optional[dict[str, Any]] = None
if self._is_codex_runtime(task.runtime) and self.codex_discovery is not None:
thread_id = self._sdk_codex_thread_id(task) or task.local_task_id
rename_thread = getattr(self.codex_discovery, "rename_thread", None)
if callable(rename_thread):
codex_rename = await self._maybe_await(
rename_thread(thread_id, normalized_title)
)
🤖 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 `@executor/runtime_work/rpc_handler.py` around lines 1498 - 1504, The Codex
rename path in rpc_handler should tolerate missing discovery support the same
way archive/unarchive/delete do, because calling codex_discovery.rename_thread
can fail on limited adapters before the local title change is persisted. In the
rename handling block around _is_codex_runtime and _sdk_codex_thread_id, guard
the rename_thread call behind a capability check or missing-method fallback, and
skip or safely no-op when the discovery adapter does not support rename while
still allowing the local title override to save.

Comment thread executor/tests/runtime_work/test_local_task_store.py Outdated
Comment on lines +83 to +87
onOpenStandaloneWorkspace?: (
deviceId: string,
workspacePath: string,
label?: string
) => Promise<void> | void

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.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect onOpenStandaloneWorkspace definition + call sites to confirm label is threaded through.
rg -nP --type=ts --type=tsx -C3 'onOpenStandaloneWorkspace' wework/src
rg -nP --type=ts --type=tsx -C3 'openStandaloneWorkspace\s*[=(:]' wework/src

Repository: wecode-ai/Wegent

Length of output: 210


🏁 Script executed:

#!/bin/bash
cd wework/src
rg -nP 'onOpenStandaloneWorkspace' --type-add 'tsx:*.tsx' --type-add 'ts:*.ts' -t ts -t tsx -C 5 'label'
awk 'NR>=360 && NR<=380' components/projects/StandaloneProjectDialogs.tsx

Repository: wecode-ai/Wegent

Length of output: 792


🏁 Script executed:

head -n 380 wework/src/components/projects/StandaloneProjectDialogs.tsx | tail -n 30

Repository: wecode-ai/Wegent

Length of output: 1183


Ensure label is propagated in StandaloneProjectDialogs.tsx

The label parameter added to onOpenStandaloneWorkspace is not currently passed in the folder picker flow, causing it to be silently dropped when opening an existing folder:

See code
wework/src/components/projects/StandaloneProjectDialogs.tsx
@~373,3
  onConfirm={async result => {
    await onOpenStandaloneWorkspace?.(result.deviceId, result.path)
    onClose()
  }}

Update this call to include the appropriate label (e.g., from result or a derived path name) so the signature is fully utilized across the codebase.

🤖 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.tsx` around lines 83 - 87,
The new optional label parameter on onOpenStandaloneWorkspace is not being
forwarded in the folder-picker flow, so update StandaloneProjectDialogs.tsx to
pass a meaningful label when opening an existing folder. Use the existing result
object or derive a label from the selected path, and ensure the call site
matches the updated onOpenStandaloneWorkspace signature from
MobileWorkbenchLayout.

Comment on lines +162 to +170
const visibleItems = useMemo(() => {
const projectItems =
projectKey === 'all'
? filteredItemsBeforeProject
: filteredItemsBeforeProject.filter(
item => (item.projectKey || item.projectName) === projectKey
)
return sortItems(projectItems, sort)
}, [filteredItemsBeforeProject, projectKey, sort])

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Project filter selection can return zero results due to key derivation mismatch.

The <select> option value is projectOptionKey(group) (Line 316), which resolves to group.projectKey || (group.projectId ? \project:${group.projectId}` : group.name). But visibleItemsfilters with(item.projectKey || item.projectName) === projectKey. These two derivations disagree when an item has no projectKey`:

  • projectKey absent + projectId present → option value is project:<id>, but the filter compares projectName, so the group renders empty.
  • projectKey/projectId/projectName all absent (workspace-only) → option value is workspacePath, but the filter yields undefined, again empty.

Unify the comparison with the same key used for the option value.

🐛 Proposed fix to align the filter with the option key
   const visibleItems = useMemo(() => {
     const projectItems =
       projectKey === 'all'
         ? filteredItemsBeforeProject
         : filteredItemsBeforeProject.filter(
-            item => (item.projectKey || item.projectName) === projectKey
+            item =>
+              projectOptionKey({
+                projectKey: item.projectKey,
+                projectId: item.projectId,
+                name: item.projectName || item.workspacePath,
+              }) === projectKey
           )
     return sortItems(projectItems, sort)
   }, [filteredItemsBeforeProject, projectKey, sort])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const visibleItems = useMemo(() => {
const projectItems =
projectKey === 'all'
? filteredItemsBeforeProject
: filteredItemsBeforeProject.filter(
item => (item.projectKey || item.projectName) === projectKey
)
return sortItems(projectItems, sort)
}, [filteredItemsBeforeProject, projectKey, sort])
const visibleItems = useMemo(() => {
const projectItems =
projectKey === 'all'
? filteredItemsBeforeProject
: filteredItemsBeforeProject.filter(
item =>
projectOptionKey({
projectKey: item.projectKey,
projectId: item.projectId,
name: item.projectName || item.workspacePath,
}) === projectKey
)
return sortItems(projectItems, sort)
}, [filteredItemsBeforeProject, projectKey, sort])
🤖 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/settings/ArchivedConversationsSettingsPage.tsx` around
lines 162 - 170, The project filter in visibleItems is using a different key
derivation than the select options, which can make matching items disappear.
Update the filter logic in ArchivedConversationsSettingsPage to compare against
the same project key shape produced by projectOptionKey (including the
projectId-derived and workspacePath cases), rather than only using
item.projectKey || item.projectName. Keep the key generation consistent between
the <select> value and the visibleItems memo so selecting any project option
returns the expected items.

Comment on lines +175 to +206
const handleUnarchive = async (item: ArchivedConversationItem) => {
const key = `unarchive:${item.id}`
setBusyKey(key)
try {
await api.unarchiveConversation({
deviceId: item.deviceId,
workspacePath: item.workspacePath,
localTaskId: item.localTaskId,
})
await loadArchivedConversations()
} finally {
setBusyKey(null)
}
}

const handleDelete = async (item: ArchivedConversationItem) => {
if (!window.confirm(t('workbench.archived_delete_confirm', '确定删除该归档对话吗?'))) {
return
}
const key = `delete:${item.id}`
setBusyKey(key)
try {
await api.deleteArchivedConversation({
deviceId: item.deviceId,
workspacePath: item.workspacePath,
localTaskId: item.localTaskId,
})
await loadArchivedConversations()
} finally {
setBusyKey(null)
}
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Surface failures from unarchive/delete.

handleUnarchive and handleDelete have no catch. Since callers invoke them via void handleDelete(item) (Lines 392, 409), a rejected request produces an unhandled rejection and the user gets no feedback — the row simply stops spinning and the (still-archived) item silently disappears after the reload only if it succeeded. Consider setting the existing error state on failure for parity with loadArchivedConversations.

🛡️ Example for handleDelete (apply similarly to handleUnarchive)
     setBusyKey(key)
     try {
       await api.deleteArchivedConversation({
         deviceId: item.deviceId,
         workspacePath: item.workspacePath,
         localTaskId: item.localTaskId,
       })
       await loadArchivedConversations()
+    } catch {
+      setError(t('workbench.archived_conversations_load_failed', '加载已归档对话失败'))
     } finally {
       setBusyKey(null)
     }
🤖 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/settings/ArchivedConversationsSettingsPage.tsx` around
lines 175 - 206, `handleUnarchive` and `handleDelete` in
`ArchivedConversationsSettingsPage` currently await API calls without handling
failures, so rejected requests become unhandled and provide no UI feedback. Add
`catch` handling around the `api.unarchiveConversation` and
`api.deleteArchivedConversation` calls, and set the existing `error` state on
failure to match `loadArchivedConversations` behavior. Keep `setBusyKey(null)`
in `finally` so the spinner always clears, and preserve the existing reload flow
only on success.

Comment on lines +1914 to +1974
const archiveProjectsConversations = useCallback(
async (runtimeProjectKeys: string[]) => {
if (!resolvedServices.runtimeWorkApi) {
dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
return
}

const uniqueProjectKeys = [...new Set(runtimeProjectKeys.filter(Boolean))]
if (uniqueProjectKeys.length === 0) return

const archivedAddresses = projectTaskAddresses(state.runtimeWork, uniqueProjectKeys)
const responses = await Promise.all(
uniqueProjectKeys.map(runtimeProjectKey =>
resolvedServices.runtimeWorkApi.archiveProjectConversations({ runtimeProjectKey })
)
)
const failedResponse = responses.find(response => !response.accepted)
if (failedResponse) {
dispatch({
type: 'error_set',
error: failedResponse.error || 'Failed to archive project conversations',
})
return
}

clearCurrentRuntimeTaskIfArchived(archivedAddresses)
await refreshWorkLists()
},
[
clearCurrentRuntimeTaskIfArchived,
refreshWorkLists,
resolvedServices.runtimeWorkApi,
state.runtimeWork,
]
)

const archiveChatConversations = useCallback(
async (addresses: RuntimeTaskAddress[]) => {
if (!resolvedServices.runtimeWorkApi) {
dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
return
}

if (addresses.length === 0) return

const responses = await Promise.all(
addresses.map(address => resolvedServices.runtimeWorkApi.archiveConversation(address))
)
const failedResponse = responses.find(response => !response.accepted)
if (failedResponse) {
dispatch({
type: 'error_set',
error: failedResponse.error || 'Failed to archive chat conversations',
})
return
}

clearCurrentRuntimeTaskIfArchived(addresses)
await refreshWorkLists()
},
[refreshWorkLists, resolvedServices.runtimeWorkApi, state.currentRuntimeTask]
[clearCurrentRuntimeTaskIfArchived, refreshWorkLists, resolvedServices.runtimeWorkApi]

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.

🎯 Functional Correctness | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'runtimeWorkApi\??\s*:' wework/src --type=ts -C2

Repository: wecode-ai/Wegent

Length of output: 14624


🏁 Script executed:

sed -n '1900,1980p' wework/src/features/workbench/WorkbenchProvider.tsx

Repository: wecode-ai/Wegent

Length of output: 2716


Capture runtimeWorkApi to a local constant to preserve type narrowing inside callback functions.

In archiveProjectsConversations and archiveChatConversations, the check if (!resolvedServices.runtimeWorkApi) narrows the type within the outer function scope, but this narrowing is lost when accessing resolvedServices.runtimeWorkApi inside the nested Promise.all map callbacks. To prevent TypeScript errors under strict mode and ensure type safety, extract the API reference to a local constant immediately after the check.

🐛 Proposed fix (capture the narrowed reference)
   const archiveProjectsConversations = useCallback(
     async (runtimeProjectKeys: string[]) => {
-      if (!resolvedServices.runtimeWorkApi) {
+      const runtimeWorkApi = resolvedServices.runtimeWorkApi
+      if (!runtimeWorkApi) {
         dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
         return
       }

       const uniqueProjectKeys = [...new Set(runtimeProjectKeys.filter(Boolean))]
       if (uniqueProjectKeys.length === 0) return

       const archivedAddresses = projectTaskAddresses(state.runtimeWork, uniqueProjectKeys)
       const responses = await Promise.all(
         uniqueProjectKeys.map(runtimeProjectKey =>
-          resolvedServices.runtimeWorkApi.archiveProjectConversations({ runtimeProjectKey })
+          runtimeWorkApi.archiveProjectConversations({ runtimeProjectKey })
         )
       )
   const archiveChatConversations = useCallback(
     async (addresses: RuntimeTaskAddress[]) => {
-      if (!resolvedServices.runtimeWorkApi) {
+      const runtimeWorkApi = resolvedServices.runtimeWorkApi
+      if (!runtimeWorkApi) {
         dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
         return
       }

       if (addresses.length === 0) return

       const responses = await Promise.all(
-        addresses.map(address => resolvedServices.runtimeWorkApi.archiveConversation(address))
+        addresses.map(address => runtimeWorkApi.archiveConversation(address))
       )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const archiveProjectsConversations = useCallback(
async (runtimeProjectKeys: string[]) => {
if (!resolvedServices.runtimeWorkApi) {
dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
return
}
const uniqueProjectKeys = [...new Set(runtimeProjectKeys.filter(Boolean))]
if (uniqueProjectKeys.length === 0) return
const archivedAddresses = projectTaskAddresses(state.runtimeWork, uniqueProjectKeys)
const responses = await Promise.all(
uniqueProjectKeys.map(runtimeProjectKey =>
resolvedServices.runtimeWorkApi.archiveProjectConversations({ runtimeProjectKey })
)
)
const failedResponse = responses.find(response => !response.accepted)
if (failedResponse) {
dispatch({
type: 'error_set',
error: failedResponse.error || 'Failed to archive project conversations',
})
return
}
clearCurrentRuntimeTaskIfArchived(archivedAddresses)
await refreshWorkLists()
},
[
clearCurrentRuntimeTaskIfArchived,
refreshWorkLists,
resolvedServices.runtimeWorkApi,
state.runtimeWork,
]
)
const archiveChatConversations = useCallback(
async (addresses: RuntimeTaskAddress[]) => {
if (!resolvedServices.runtimeWorkApi) {
dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
return
}
if (addresses.length === 0) return
const responses = await Promise.all(
addresses.map(address => resolvedServices.runtimeWorkApi.archiveConversation(address))
)
const failedResponse = responses.find(response => !response.accepted)
if (failedResponse) {
dispatch({
type: 'error_set',
error: failedResponse.error || 'Failed to archive chat conversations',
})
return
}
clearCurrentRuntimeTaskIfArchived(addresses)
await refreshWorkLists()
},
[refreshWorkLists, resolvedServices.runtimeWorkApi, state.currentRuntimeTask]
[clearCurrentRuntimeTaskIfArchived, refreshWorkLists, resolvedServices.runtimeWorkApi]
const archiveProjectsConversations = useCallback(
async (runtimeProjectKeys: string[]) => {
const runtimeWorkApi = resolvedServices.runtimeWorkApi
if (!runtimeWorkApi) {
dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
return
}
const uniqueProjectKeys = [...new Set(runtimeProjectKeys.filter(Boolean))]
if (uniqueProjectKeys.length === 0) return
const archivedAddresses = projectTaskAddresses(state.runtimeWork, uniqueProjectKeys)
const responses = await Promise.all(
uniqueProjectKeys.map(runtimeProjectKey =>
runtimeWorkApi.archiveProjectConversations({ runtimeProjectKey })
)
)
const failedResponse = responses.find(response => !response.accepted)
if (failedResponse) {
dispatch({
type: 'error_set',
error: failedResponse.error || 'Failed to archive project conversations',
})
return
}
clearCurrentRuntimeTaskIfArchived(archivedAddresses)
await refreshWorkLists()
},
[
clearCurrentRuntimeTaskIfArchived,
refreshWorkLists,
resolvedServices.runtimeWorkApi,
state.runtimeWork,
]
)
const archiveChatConversations = useCallback(
async (addresses: RuntimeTaskAddress[]) => {
const runtimeWorkApi = resolvedServices.runtimeWorkApi
if (!runtimeWorkApi) {
dispatch({ type: 'error_set', error: 'Local runtime work is unavailable' })
return
}
if (addresses.length === 0) return
const responses = await Promise.all(
addresses.map(address => runtimeWorkApi.archiveConversation(address))
)
const failedResponse = responses.find(response => !response.accepted)
if (failedResponse) {
dispatch({
type: 'error_set',
error: failedResponse.error || 'Failed to archive chat conversations',
})
return
}
clearCurrentRuntimeTaskIfArchived(addresses)
await refreshWorkLists()
},
[clearCurrentRuntimeTaskIfArchived, refreshWorkLists, resolvedServices.runtimeWorkApi]
🤖 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/WorkbenchProvider.tsx` around lines 1914 -
1974, The type narrowing for resolvedServices.runtimeWorkApi is lost inside the
Promise.all callbacks in archiveProjectsConversations and
archiveChatConversations. After the null check, assign
resolvedServices.runtimeWorkApi to a local constant and use that captured
reference for archiveProjectConversations and archiveConversation so the
narrowed type is preserved inside the nested map callbacks.

Comment on lines 70 to +73
"rename_project": "Rename project",
"rename_chat": "Rename chat",
"rename_chat_description": "Keep it short and easy to recognize",
"chat_name": "Chat name",

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicate locale keys.

rename_project (Line 70) and chat_name (Line 73) are already declared later in this object (Lines 191 and 192). Biome flags both as duplicate object keys; the later declarations win at runtime, so the newly added values here are dead. Remove one of each duplicate.

🧰 Tools
🪛 Biome (2.5.0)

[error] 70-70: The key rename_project was already declared.

(lint/suspicious/noDuplicateObjectKeys)


[error] 73-73: The key chat_name was already declared.

(lint/suspicious/noDuplicateObjectKeys)

🤖 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/i18n/locales/en/common.json` around lines 70 - 73, The common.json
locale object has duplicate keys for rename_project and chat_name, which causes
the later declarations to override the new values. Update the en/common.json
object to keep only one definition of each key by removing the duplicates from
the earlier block or consolidating them with the existing entries later in the
file, and ensure the remaining keys are unique throughout the object.

Source: Linters/SAST tools

@qdaxb
qdaxb merged commit 86dadb1 into wecode-ai:main Jun 25, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants