Skip to content

feat: improve local task execution flow - #1596

Merged
qdaxb merged 9 commits into
wecode-ai:mainfrom
FicoHu:codex/local-task-execution-flow
Jun 29, 2026
Merged

qdaxb merged 9 commits into
wecode-ai:mainfrom
FicoHu:codex/local-task-execution-flow

Conversation

@FicoHu

@FicoHu FicoHu commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Chat tasks now resolve the correct local dispatch device earlier and persist it for existing tasks.
    • Responses streaming now treats response.created as a start event and includes shell metadata; streamed reasoning is preserved through completion.
    • Local Claude execution improves runtime preparation (attachments, task skills, enabled plugin cache, and file-edit hooks).
  • Bug Fixes
    • Better fallback when the preferred local device is offline/inactive; unknown device IDs are retained.
  • Tests
    • Added/updated coverage for websocket event ordering, device resolution, reasoning/text streaming, and local execution.
  • Documentation
    • Updated local device resolution and launch-context developer guidance.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds chat task device resolution and response.created START handling, streams executor output as incremental events with Claude setup and Codex isolation, and updates frontend query parsing plus thinking-block rendering.

Changes

Backend chat dispatch and START events

Layer / File(s) Summary
Task device resolution service
backend/app/services/chat/task_device_resolution.py
New module resolving chat task device ids from params, project config, or task spec, plus online-device checks and task JSON persistence.
Chat send and lifecycle wiring
backend/app/api/ws/chat_namespace.py, backend/app/services/chat/trigger/lifecycle.py
Chat send resolves dispatch device ids before task creation, and existing-task execution sessions persist a resolved device id when needed.
response.created START handling
backend/app/services/execution/dispatcher.py
RESPONSE_CREATED now maps to internal EventType.START and is removed from the lifecycle skip set.
Backend tests for device resolution and START events
backend/tests/services/chat/test_task_device_resolution.py, backend/tests/api/ws/test_device_capabilities_state.py, backend/tests/services/chat/storage/test_task_manager.py, backend/tests/services/execution/test_responses_api_event_parser.py
Adds resolver coverage, websocket emission coverage, task persistence coverage, and parser coverage.
Device resolution and launch-context docs
docs/en/developer-guide/local-device-architecture.md, docs/zh/developer-guide/local-device-architecture.md
Developer guide sections describe local chat task device resolution, executor launch setup, attachment and skill handling, hook setup, and Responses API event ordering.

Executor streaming, Claude setup, and Codex isolation

Layer / File(s) Summary
Runner and event builder updates
executor/src/runner/mod.rs, executor/src/local/backend/tasks.rs, executor/src/emitter/mod.rs, executor/src/stream/mod.rs
Adds run_with_events, streaming delta emitters, and reasoning/text extraction helpers.
Streaming process execution
executor/src/process/mod.rs
Adds streaming command execution, stdout NDJSON parsing, chunked delta emission, and chunk configuration helpers.
Claude pre-launch attachments, cache, skills, and hooks
executor/src/agents/mod.rs, executor/src/services/skill_deployer.rs, executor/src/agents/runtime_capabilities.rs
Adds attachment download/rewrite, plugin cache restore, file-edit hook setup, and skills directory selection/merge logic.
Plugin cache restoration and hook permissions
executor/src/local/capabilities.rs, executor/src/local/capabilities/support.rs
Restores enabled plugin zips and ensures hook files receive executable permissions.
Codex runtime home isolation
executor/src/agents/codex.rs
Isolates Codex runtime state and synchronizes runtime config files.
Workspace and session roots
executor/src/claude_session.rs
Adds a default local workspace fallback and splits session file candidates across read/write/remove paths.
Executor tests
executor/tests/...
Covers streaming events, attachments, skills, hooks, plugin cache, Codex isolation, workspace paths, local backend events, and hook permissions.

Frontend null-safe search params and thinking rendering

Layer / File(s) Summary
Search params utility
frontend/src/lib/search-params.ts
Adds SearchParamsLike plus helper functions for reading and stringifying optional query params.
Helper adoption in chat UI
frontend/src/app/(tasks)/chat/..., frontend/src/features/tasks/components/chat/ChatArea.tsx, frontend/src/features/tasks/components/sidebar/TaskSidebar.tsx
Switches chat pages, chat area, and sidebar route logic to the new helpers.
Tests for missing search params
frontend/src/__tests__/app/(tasks)/chat/*, frontend/src/__tests__/features/tasks/components/...
Updates mocks to allow undefined search params and adds rendering coverage for that case.
Thinking types and display routing
frontend/src/features/tasks/components/message/thinking/types.ts, frontend/src/features/tasks/components/message/thinking/ThinkingDisplay.tsx, frontend/src/__tests__/features/tasks/components/message/thinking/ThinkingDisplay.test.tsx
Adds a thinking-block type guard and updates ThinkingDisplay routing for tool-block and text thinking steps.
Message bubble thinking rendering
frontend/src/features/tasks/components/message/MessageBubble.tsx, frontend/src/__tests__/features/tasks/components/message/MessageBubble.test.tsx
MessageBubble now filters thinking blocks from mixed content, derives reasoning display state from message blocks, and updates related tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • qdaxb
  • sunnights

Poem

🐰 Hop hop, the streams now sing,
Delta clues on a silver string.
Devices choose their proper den,
Then chat and Claude run straight again.
Search params whisper, “I am here,”
And rabbit ears applaud with cheer.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.77% 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 is concise and broadly matches the main change around local task execution flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 13

🤖 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/chat/task_device_resolution.py`:
- Around line 100-128: The online-device fallback in
resolve_online_local_executor_device_id() only treats device.name as a valid
local alias, so inactive appDeviceId values are not recognized and the stale ID
can be returned. Update the alias check to include the frontend appDeviceId from
the local device records when building the local identifier set, using
_list_local_devices(..., include_inactive=True) and the same normalization
helper, so the fallback can switch to the sole online executor when the stored
device is inactive.

In `@executor/src/agents/codex.rs`:
- Around line 360-377: Guard the destructive cleanup in
prepare_codex_runtime_home by checking whether the resolved runtime_home matches
the user Codex home before any remove_runtime_capability_dir or
sync_codex_runtime_file calls; if codex_runtime_home() points to
user_codex_home(), return an error instead of deleting skills/plugins. Apply the
same protection to the related cleanup/sync path in user_codex_home and
prepare_codex_runtime_home so WEGENT_CODEX_RUNTIME_HOME cannot target
$HOME/.codex.

In `@executor/src/agents/mod.rs`:
- Around line 342-346: The attachment path builder in the attachment download
flow currently uses only the sanitized original filename, so duplicate names in
the same task/subtask collide. Update the path construction in the attachment
path logic used by the relevant helper in executor/src/agents/mod.rs to include
attachment.id in the local filename or path while keeping
sanitize_attachment_filename as part of the name. Make sure both download and
record lookup paths use the same unique scheme so each attachment maps to its
own file.
- Around line 547-548: The current hook update logic in the group-building flow
is removing too broadly: `groups.retain()` is deleting any group matched by
`file_edit_hook_group_matches`, including user-defined groups. Tighten the
predicate so only the Wegent-owned hook group is removed/updated, then append or
replace that specific hook in the same `groups` handling path used around
`file_edit_hook_group_matches` and the `groups.push(json!({ ... }))` block.
- Line 248: The attachment download client is created without any timeout, so a
stalled backend can block pre-launch indefinitely. Update the attachment
download path in the relevant agent startup flow to use a reqwest client or
request timeout, and make sure timeout failures are handled as failed
attachments rather than hanging the agent; use the existing client setup around
reqwest::Client::new as the place to apply this change.
- Around line 517-523: read_claude_settings currently masks read/parse/type
failures by converting them into an empty object, which can overwrite a
malformed or non-object Claude settings file. Update read_claude_settings to
return a result/error instead of always producing Value::Object(Map::new()), and
make the caller that injects hooks detect that failure and skip writing settings
when the existing file cannot be safely parsed. Use read_claude_settings and the
hook injection path in executor/src/agents/mod.rs to locate the change.

In `@executor/src/local/capabilities.rs`:
- Around line 944-954: The plugin restore fallback in the cache lookup is too
broad and can select a same-named ZIP from the wrong marketplace. Update the
restoration logic in the cache scanning path around the `read_dir`/`candidates`
search so it only restores `{marketplace}/{name}.zip`, or verifies the package
manifest/source matches the enabled plugin before extraction. Use the existing
cache resolution flow in `capabilities.rs` to skip restoration when the exact
marketplace match is missing.

In `@executor/src/local/capabilities/support.rs`:
- Around line 92-100: The hook permission repair traversal in
support::ensure_plugin_hook_executable is following symlinks because it uses
fs::metadata on each entry target, which can let the walk escape plugin_path.
Switch the metadata check to symlink_metadata so symlinks are detected without
being followed, and explicitly skip symlink entries before deciding whether to
push into pending or call ensure_plugin_hook_executable; keep the existing
traversal logic in the read_dir loop unchanged otherwise.

In `@executor/src/process/mod.rs`:
- Around line 331-344: The child process handling in
process::mod::wait_for_child is short-circuiting on stdin write errors and can
replace the real exit result with a generic failure. Update the logic around
child.wait().await, writer.await, and the CommandOutcome::Failure return so that
a write error does not override an already collected child status, especially
non-zero exits. Preserve the process result first, always continue through
join_output(stdout_task) and join_output(stderr_task), and only use the write
error as supplemental information when the child exit result does not already
explain the outcome.

In `@executor/tests/agent_process_engine_contract.rs`:
- Around line 607-668: The stale hook fixture is being written to a path that
may not match the actual Claude config directory used by the test, so the test
can pass without exercising the file it creates. Update the setup in the stale
file edit hook test to write `settings.json` through the resolved
`CLAUDE_CONFIG_DIR` path used by the runtime, or set `HOME` first and derive the
config path from it. Use the existing test symbols `workspace_root`, `task_dir`,
`settings_path`, and `fake_claude` to keep the fixture aligned with the real
Claude task-turn config resolution.

In `@executor/tests/local_backend_contract.rs`:
- Around line 315-325: The test in local_backend_contract.rs is hard-coding an
assumed count of response.output_text.delta events, which makes it brittle when
read_streaming_stdout changes chunking via stream_chunk_chars(). Update the
assertion around transport.wait_for_emits and the emitted event checks so it
either sets the chunk size explicitly for this test or collects all
response.output_text.delta events until response.completed, then verifies the
concatenated payload matches long_text. Use the existing transport and emitted
event parsing logic to avoid relying on a fixed delta count.

In `@frontend/src/features/tasks/components/chat/ChatArea.tsx`:
- Around line 430-431: `ChatArea` is resolving task-id aliases in a different
order than the page shell, which can make it bind to a different task when both
aliases are present. Update the `taskIdFromUrl` lookup in `ChatArea` to use the
same alias precedence as `ChatPageDesktop` and the shared chat page, keeping the
`getFirstSearchParam` order consistent across these entry points so downstream
task/team sync logic sees the same task ID.

In `@frontend/src/lib/search-params.ts`:
- Around line 14-20: getFirstSearchParam currently stops on empty strings
because it only checks for non-null values, so alias fallback never runs when a
query key is present but blank. Update getFirstSearchParam to treat empty
results from getSearchParam as missing and continue iterating through keys until
a non-empty value is found, while preserving the existing return type and
behavior for null/undefined cases.
🪄 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: ae693c1b-3cab-4997-9594-1226869d803b

📥 Commits

Reviewing files that changed from the base of the PR and between c57fcd5 and 95673e5.

📒 Files selected for processing (37)
  • backend/app/api/ws/chat_namespace.py
  • backend/app/services/chat/task_device_resolution.py
  • backend/app/services/chat/trigger/lifecycle.py
  • backend/app/services/execution/dispatcher.py
  • backend/tests/api/ws/test_device_capabilities_state.py
  • backend/tests/services/chat/storage/test_task_manager.py
  • backend/tests/services/chat/test_task_device_resolution.py
  • backend/tests/services/execution/test_responses_api_event_parser.py
  • docs/en/developer-guide/local-device-architecture.md
  • docs/zh/developer-guide/local-device-architecture.md
  • executor/src/agents/codex.rs
  • executor/src/agents/mod.rs
  • executor/src/claude_session.rs
  • executor/src/emitter/mod.rs
  • executor/src/local/backend/tasks.rs
  • executor/src/local/capabilities.rs
  • executor/src/local/capabilities/support.rs
  • executor/src/process/mod.rs
  • executor/src/runner/mod.rs
  • executor/src/services/skill_deployer.rs
  • executor/src/stream/mod.rs
  • executor/tests/agent_command_contract.rs
  • executor/tests/agent_process_engine_contract.rs
  • executor/tests/codex_app_server_contract.rs
  • executor/tests/docker_runtime_contract.rs
  • executor/tests/local_backend_contract.rs
  • executor/tests/local_capability_sync_contract.rs
  • frontend/src/__tests__/app/(tasks)/chat/ChatPageDesktop.remote-workspace.test.tsx
  • frontend/src/__tests__/app/(tasks)/chat/ChatPageMobile.remote-workspace.test.tsx
  • frontend/src/__tests__/features/tasks/components/chat/ChatArea.queue-message-handler.test.tsx
  • frontend/src/__tests__/features/tasks/components/sidebar/TaskSidebar.test.tsx
  • frontend/src/app/(tasks)/chat/ChatPageDesktop.tsx
  • frontend/src/app/(tasks)/chat/ChatPageMobile.tsx
  • frontend/src/app/(tasks)/chat/page.tsx
  • frontend/src/features/tasks/components/chat/ChatArea.tsx
  • frontend/src/features/tasks/components/sidebar/TaskSidebar.tsx
  • frontend/src/lib/search-params.ts

Comment on lines +100 to +128
candidate = resolve_local_executor_device_id(
db,
user_id=user_id,
device_id=device_id,
)
if not candidate:
return None

local_devices = _list_local_devices(db, user_id=user_id)
online_device_ids = await _online_local_device_ids(
user_id=user_id,
local_devices=local_devices,
)
if candidate in online_device_ids:
return candidate

local_device_ids = {
device_id
for device in _list_local_devices(
db,
user_id=user_id,
include_inactive=True,
)
if (device_id := _clean_string(device.name))
}
if candidate in local_device_ids and len(online_device_ids) == 1:
return next(iter(online_device_ids))

return candidate

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

Handle inactive appDeviceId aliases during online-device fallback.

resolve_local_executor_device_id() only searches active device rows, and the fallback in resolve_online_local_executor_device_id() only recognizes device.name as a local alias. If a project/task stores the frontend appDeviceId and that device later becomes inactive, this path keeps the stale app ID instead of switching to the sole online executor. The new tests only cover appDeviceId while the device record is still active, so this regression would slip through.

Also applies to: 143-166

🤖 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/chat/task_device_resolution.py` around lines 100 - 128,
The online-device fallback in resolve_online_local_executor_device_id() only
treats device.name as a valid local alias, so inactive appDeviceId values are
not recognized and the stale ID can be returned. Update the alias check to
include the frontend appDeviceId from the local device records when building the
local identifier set, using _list_local_devices(..., include_inactive=True) and
the same normalization helper, so the fallback can switch to the sole online
executor when the stored device is inactive.

Comment thread executor/src/agents/codex.rs Outdated
Comment thread executor/src/agents/mod.rs Outdated
Comment thread executor/src/agents/mod.rs Outdated
Comment thread executor/src/agents/mod.rs Outdated
Comment on lines +517 to +523
fn read_claude_settings(settings_path: &PathBuf) -> Value {
fs::read_to_string(settings_path)
.ok()
.and_then(|content| serde_json::from_str::<Value>(&content).ok())
.filter(|value| value.is_object())
.unwrap_or_else(|| Value::Object(Map::new()))
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether invalid settings.json cases are covered before changing behavior.
rg -n -C3 'read_claude_settings|write_claude_file_edit_hook_settings|settings\.json' executor/src executor/tests

Repository: wecode-ai/Wegent

Length of output: 8925


🏁 Script executed:

#!/bin/bash
sed -n '500,525p' executor/src/agents/mod.rs
printf '\n---\n'
sed -n '520,700p' executor/tests/agent_process_engine_contract.rs
printf '\n---\n'
sed -n '500,740p' executor/tests/local_capability_sync_contract.rs

Repository: wecode-ai/Wegent

Length of output: 17092


🏁 Script executed:

#!/bin/bash
rg -n -C3 'read_claude_settings|write_claude_file_edit_hook_settings|read_json_or_default|settings\.json' executor/src executor/tests
printf '\n---\n'
sed -n '1,220p' executor/src/agents/mod.rs

Repository: wecode-ai/Wegent

Length of output: 19471


Avoid clobbering malformed Claude settings
read_claude_settings() turns any parse/type failure into {}, so an existing malformed or non-object settings.json gets rewritten and the user's config is lost. Return the error and skip hook injection instead.

🤖 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/src/agents/mod.rs` around lines 517 - 523, read_claude_settings
currently masks read/parse/type failures by converting them into an empty
object, which can overwrite a malformed or non-object Claude settings file.
Update read_claude_settings to return a result/error instead of always producing
Value::Object(Map::new()), and make the caller that injects hooks detect that
failure and skip writing settings when the existing file cannot be safely
parsed. Use read_claude_settings and the hook injection path in
executor/src/agents/mod.rs to locate the change.

Comment thread executor/src/process/mod.rs
Comment thread executor/tests/agent_process_engine_contract.rs
Comment thread executor/tests/local_backend_contract.rs Outdated
Comment on lines +430 to +431
const searchParamsString = stringifySearchParams(searchParams)
const taskIdFromUrl = getFirstSearchParam(searchParams, ['taskId', 'task_id', 'taskid'])

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

Keep task-id alias precedence consistent with the page shell.

ChatArea now prefers taskId before task_id, but frontend/src/app/(tasks)/chat/ChatPageDesktop.tsx and the shared chat page still do the reverse. If both aliases are present, the page and ChatArea will bind to different task IDs, which breaks the task/team sync logic below.

Proposed fix
-  const taskIdFromUrl = getFirstSearchParam(searchParams, ['taskId', 'task_id', 'taskid'])
+  const taskIdFromUrl = getFirstSearchParam(searchParams, ['task_id', 'taskid', 'taskId'])
📝 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 searchParamsString = stringifySearchParams(searchParams)
const taskIdFromUrl = getFirstSearchParam(searchParams, ['taskId', 'task_id', 'taskid'])
const searchParamsString = stringifySearchParams(searchParams)
const taskIdFromUrl = getFirstSearchParam(searchParams, ['task_id', 'taskid', 'taskId'])
🤖 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 `@frontend/src/features/tasks/components/chat/ChatArea.tsx` around lines 430 -
431, `ChatArea` is resolving task-id aliases in a different order than the page
shell, which can make it bind to a different task when both aliases are present.
Update the `taskIdFromUrl` lookup in `ChatArea` to use the same alias precedence
as `ChatPageDesktop` and the shared chat page, keeping the `getFirstSearchParam`
order consistent across these entry points so downstream task/team sync logic
sees the same task ID.

Comment on lines +14 to +20
export function getFirstSearchParam(
searchParams: SearchParamsLike | null | undefined,
keys: string[]
): string | null {
for (const key of keys) {
const value = getSearchParam(searchParams, key)
if (value !== null) return value

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 | ⚡ Quick win

Skip empty query values when falling back.

URLSearchParams.get() returns '' for ?taskId=. With the current !== null check, getFirstSearchParam() stops there and never checks later aliases, so a URL like ?taskId=&task_id=123 resolves as “no task” even though a valid fallback is present.

Proposed fix
 export function getFirstSearchParam(
   searchParams: SearchParamsLike | null | undefined,
   keys: string[]
 ): string | null {
   for (const key of keys) {
     const value = getSearchParam(searchParams, key)
-    if (value !== null) return value
+    if (value !== null && value !== '') return value
   }

   return null
 }
📝 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
export function getFirstSearchParam(
searchParams: SearchParamsLike | null | undefined,
keys: string[]
): string | null {
for (const key of keys) {
const value = getSearchParam(searchParams, key)
if (value !== null) return value
export function getFirstSearchParam(
searchParams: SearchParamsLike | null | undefined,
keys: string[]
): string | null {
for (const key of keys) {
const value = getSearchParam(searchParams, key)
if (value !== null && value !== '') return value
}
return 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 `@frontend/src/lib/search-params.ts` around lines 14 - 20, getFirstSearchParam
currently stops on empty strings because it only checks for non-null values, so
alias fallback never runs when a query key is present but blank. Update
getFirstSearchParam to treat empty results from getSearchParam as missing and
continue iterating through keys until a non-empty value is found, while
preserving the existing return type and behavior for null/undefined cases.

FicoHu and others added 3 commits June 29, 2026 18:24
# Conflicts:
#	executor/src/agents/mod.rs
#	executor/src/process/mod.rs
#	executor/src/stream/mod.rs
#	executor/tests/agent_process_engine_contract.rs
# Conflicts:
#	executor/tests/agent_runtime_capabilities_contract.rs

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

♻️ Duplicate comments (2)
executor/src/agents/runtime_capabilities.rs (2)

277-278: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep attachment paths unique per attachment.

Using only the sanitized original filename means two attachments named image.png in the same task/subtask overwrite each other, and both prompt records can point at the later file.

Proposed fix
-    let path = attachments_dir.join(sanitize_filename(&attachment.original_filename));
+    let path = attachments_dir.join(format!(
+        "{}-{}",
+        attachment.id,
+        sanitize_filename(&attachment.original_filename)
+    ));
🤖 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/src/agents/runtime_capabilities.rs` around lines 277 - 278, The
attachment write in runtime_capabilities::add_attachment uses only
sanitize_filename(&attachment.original_filename), so duplicate names can
overwrite each other and leave prompt records pointing to the wrong file. Update
the path construction in that attachment-saving flow to include a unique
per-attachment component (for example from the attachment id or a generated
unique suffix) while keeping the sanitized original name for readability. Make
sure the uniqueness is applied before fs::write is called so each attachment
gets its own distinct file path.

804-836: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve malformed Claude settings instead of replacing them.

A parse error or non-object settings.json currently becomes {} and is written back, which can destroy the user's existing settings. Skip hook injection when the existing file cannot be safely parsed.

Proposed fix
     let settings_path = config_dir.join("settings.json");
-    let mut settings = fs::read_to_string(&settings_path)
-        .ok()
-        .and_then(|content| serde_json::from_str::<Value>(&content).ok())
-        .filter(Value::is_object)
-        .unwrap_or_else(|| json!({}));
+    let mut settings = match fs::read_to_string(&settings_path) {
+        Ok(content) => match serde_json::from_str::<Value>(&content) {
+            Ok(value) if value.is_object() => value,
+            _ => return,
+        },
+        Err(error) if error.kind() == std::io::ErrorKind::NotFound => json!({}),
+        Err(_) => return,
+    };
🤖 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/src/agents/runtime_capabilities.rs` around lines 804 - 836, The
settings update logic in the runtime capabilities hook installer is overwriting
malformed or non-object Claude settings by falling back to an empty object;
instead, in this settings.json flow, detect parse failures or non-object content
before mutating and return early without writing anything. Update the code
around the settings.json load, settings_object extraction, and write_json_file
call so hook injection only happens when the existing file can be safely parsed
as an object, preserving the user’s original settings when it cannot.
🤖 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 `@executor/src/agents/runtime_capabilities.rs`:
- Around line 786-796: The write_claude_user_config helper is overwriting the
entire claude.json instead of preserving existing user settings. Update
write_claude_user_config to read the current Claude config from
config_dir.join("claude.json"), merge in only the Wegent-owned keys from the new
JSON value, and then write the merged result back using write_json_file. Keep
the change localized to write_claude_user_config and ensure unrelated fields
already present in the user’s Claude config remain untouched.

In `@executor/tests/agent_runtime_capabilities_contract.rs`:
- Around line 223-233: The one-shot test server tasks can hang forever if the
runtime never connects, so the current JoinHandle awaits in the agent runtime
capability contract tests need a timeout. Update the async test helpers around
the server spawn logic to wrap each server.await.unwrap() in
tokio::time::timeout, and apply the same pattern in the related test blocks that
use the spawned server handles so failures surface locally and quickly instead
of blocking the test suite.

---

Duplicate comments:
In `@executor/src/agents/runtime_capabilities.rs`:
- Around line 277-278: The attachment write in
runtime_capabilities::add_attachment uses only
sanitize_filename(&attachment.original_filename), so duplicate names can
overwrite each other and leave prompt records pointing to the wrong file. Update
the path construction in that attachment-saving flow to include a unique
per-attachment component (for example from the attachment id or a generated
unique suffix) while keeping the sanitized original name for readability. Make
sure the uniqueness is applied before fs::write is called so each attachment
gets its own distinct file path.
- Around line 804-836: The settings update logic in the runtime capabilities
hook installer is overwriting malformed or non-object Claude settings by falling
back to an empty object; instead, in this settings.json flow, detect parse
failures or non-object content before mutating and return early without writing
anything. Update the code around the settings.json load, settings_object
extraction, and write_json_file call so hook injection only happens when the
existing file can be safely parsed as an object, preserving the user’s original
settings when it cannot.
🪄 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: a92ed43c-10a8-4ce0-ab66-e3b09c5136cd

📥 Commits

Reviewing files that changed from the base of the PR and between 4d44f3e and 3532d23.

📒 Files selected for processing (11)
  • executor/src/agents/codex.rs
  • executor/src/agents/mod.rs
  • executor/src/agents/runtime_capabilities.rs
  • executor/src/claude_session.rs
  • executor/src/process/mod.rs
  • executor/src/runner/mod.rs
  • executor/src/stream/mod.rs
  • executor/tests/agent_command_contract.rs
  • executor/tests/agent_process_engine_contract.rs
  • executor/tests/agent_runtime_capabilities_contract.rs
  • executor/tests/codex_app_server_contract.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • executor/src/runner/mod.rs
  • executor/src/stream/mod.rs
  • executor/tests/agent_command_contract.rs
  • executor/src/claude_session.rs
  • executor/tests/codex_app_server_contract.rs
  • executor/tests/agent_process_engine_contract.rs
  • executor/src/agents/codex.rs
  • executor/src/process/mod.rs

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

♻️ Duplicate comments (2)
executor/src/agents/runtime_capabilities.rs (2)

277-278: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep attachment paths unique per attachment.

Using only the sanitized original filename means two attachments named image.png in the same task/subtask overwrite each other, and both prompt records can point at the later file.

Proposed fix
-    let path = attachments_dir.join(sanitize_filename(&attachment.original_filename));
+    let path = attachments_dir.join(format!(
+        "{}-{}",
+        attachment.id,
+        sanitize_filename(&attachment.original_filename)
+    ));
🤖 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/src/agents/runtime_capabilities.rs` around lines 277 - 278, The
attachment write in runtime_capabilities::add_attachment uses only
sanitize_filename(&attachment.original_filename), so duplicate names can
overwrite each other and leave prompt records pointing to the wrong file. Update
the path construction in that attachment-saving flow to include a unique
per-attachment component (for example from the attachment id or a generated
unique suffix) while keeping the sanitized original name for readability. Make
sure the uniqueness is applied before fs::write is called so each attachment
gets its own distinct file path.

804-836: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve malformed Claude settings instead of replacing them.

A parse error or non-object settings.json currently becomes {} and is written back, which can destroy the user's existing settings. Skip hook injection when the existing file cannot be safely parsed.

Proposed fix
     let settings_path = config_dir.join("settings.json");
-    let mut settings = fs::read_to_string(&settings_path)
-        .ok()
-        .and_then(|content| serde_json::from_str::<Value>(&content).ok())
-        .filter(Value::is_object)
-        .unwrap_or_else(|| json!({}));
+    let mut settings = match fs::read_to_string(&settings_path) {
+        Ok(content) => match serde_json::from_str::<Value>(&content) {
+            Ok(value) if value.is_object() => value,
+            _ => return,
+        },
+        Err(error) if error.kind() == std::io::ErrorKind::NotFound => json!({}),
+        Err(_) => return,
+    };
🤖 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/src/agents/runtime_capabilities.rs` around lines 804 - 836, The
settings update logic in the runtime capabilities hook installer is overwriting
malformed or non-object Claude settings by falling back to an empty object;
instead, in this settings.json flow, detect parse failures or non-object content
before mutating and return early without writing anything. Update the code
around the settings.json load, settings_object extraction, and write_json_file
call so hook injection only happens when the existing file can be safely parsed
as an object, preserving the user’s original settings when it cannot.
🤖 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 `@executor/src/agents/runtime_capabilities.rs`:
- Around line 786-796: The write_claude_user_config helper is overwriting the
entire claude.json instead of preserving existing user settings. Update
write_claude_user_config to read the current Claude config from
config_dir.join("claude.json"), merge in only the Wegent-owned keys from the new
JSON value, and then write the merged result back using write_json_file. Keep
the change localized to write_claude_user_config and ensure unrelated fields
already present in the user’s Claude config remain untouched.

In `@executor/tests/agent_runtime_capabilities_contract.rs`:
- Around line 223-233: The one-shot test server tasks can hang forever if the
runtime never connects, so the current JoinHandle awaits in the agent runtime
capability contract tests need a timeout. Update the async test helpers around
the server spawn logic to wrap each server.await.unwrap() in
tokio::time::timeout, and apply the same pattern in the related test blocks that
use the spawned server handles so failures surface locally and quickly instead
of blocking the test suite.

---

Duplicate comments:
In `@executor/src/agents/runtime_capabilities.rs`:
- Around line 277-278: The attachment write in
runtime_capabilities::add_attachment uses only
sanitize_filename(&attachment.original_filename), so duplicate names can
overwrite each other and leave prompt records pointing to the wrong file. Update
the path construction in that attachment-saving flow to include a unique
per-attachment component (for example from the attachment id or a generated
unique suffix) while keeping the sanitized original name for readability. Make
sure the uniqueness is applied before fs::write is called so each attachment
gets its own distinct file path.
- Around line 804-836: The settings update logic in the runtime capabilities
hook installer is overwriting malformed or non-object Claude settings by falling
back to an empty object; instead, in this settings.json flow, detect parse
failures or non-object content before mutating and return early without writing
anything. Update the code around the settings.json load, settings_object
extraction, and write_json_file call so hook injection only happens when the
existing file can be safely parsed as an object, preserving the user’s original
settings when it cannot.
🪄 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: a92ed43c-10a8-4ce0-ab66-e3b09c5136cd

📥 Commits

Reviewing files that changed from the base of the PR and between 4d44f3e and 3532d23.

📒 Files selected for processing (11)
  • executor/src/agents/codex.rs
  • executor/src/agents/mod.rs
  • executor/src/agents/runtime_capabilities.rs
  • executor/src/claude_session.rs
  • executor/src/process/mod.rs
  • executor/src/runner/mod.rs
  • executor/src/stream/mod.rs
  • executor/tests/agent_command_contract.rs
  • executor/tests/agent_process_engine_contract.rs
  • executor/tests/agent_runtime_capabilities_contract.rs
  • executor/tests/codex_app_server_contract.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • executor/src/runner/mod.rs
  • executor/src/stream/mod.rs
  • executor/tests/agent_command_contract.rs
  • executor/src/claude_session.rs
  • executor/tests/codex_app_server_contract.rs
  • executor/tests/agent_process_engine_contract.rs
  • executor/src/agents/codex.rs
  • executor/src/process/mod.rs
🛑 Comments failed to post (2)
executor/src/agents/runtime_capabilities.rs (1)

786-796: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Merge Claude user config instead of overwriting it.

write_json_file(&config_dir.join("claude.json"), &value) replaces the whole existing Claude config under the active home directory. In local mode this can erase user settings unrelated to Wegent.

Proposed fix
 fn write_claude_user_config(config_dir: &Path) {
-    let value = json!({
-        "numStartups": 2,
-        "installMethod": "unknown",
-        "autoUpdates": true,
-        "sonnet45MigrationComplete": true,
-        "hasCompletedOnboarding": true,
-        "bypassPermissionsModeAccepted": true,
-        "isQualifiedForDataSharing": false,
-    });
-    let _ = write_json_file(&config_dir.join("claude.json"), &value);
+    let path = config_dir.join("claude.json");
+    let mut value = fs::read_to_string(&path)
+        .ok()
+        .and_then(|content| serde_json::from_str::<Value>(&content).ok())
+        .filter(Value::is_object)
+        .unwrap_or_else(|| json!({}));
+    if let Some(object) = value.as_object_mut() {
+        object.entry("numStartups".to_owned()).or_insert(json!(2));
+        object.entry("installMethod".to_owned()).or_insert(json!("unknown"));
+        object.entry("autoUpdates".to_owned()).or_insert(json!(true));
+        object
+            .entry("sonnet45MigrationComplete".to_owned())
+            .or_insert(json!(true));
+        object
+            .entry("hasCompletedOnboarding".to_owned())
+            .or_insert(json!(true));
+        object
+            .entry("bypassPermissionsModeAccepted".to_owned())
+            .or_insert(json!(true));
+        object
+            .entry("isQualifiedForDataSharing".to_owned())
+            .or_insert(json!(false));
+    }
+    let _ = write_json_file(&path, &value);
 }
📝 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.

fn write_claude_user_config(config_dir: &Path) {
    let path = config_dir.join("claude.json");
    let mut value = fs::read_to_string(&path)
        .ok()
        .and_then(|content| serde_json::from_str::<Value>(&content).ok())
        .filter(Value::is_object)
        .unwrap_or_else(|| json!({}));
    if let Some(object) = value.as_object_mut() {
        object.entry("numStartups".to_owned()).or_insert(json!(2));
        object.entry("installMethod".to_owned()).or_insert(json!("unknown"));
        object.entry("autoUpdates".to_owned()).or_insert(json!(true));
        object
            .entry("sonnet45MigrationComplete".to_owned())
            .or_insert(json!(true));
        object
            .entry("hasCompletedOnboarding".to_owned())
            .or_insert(json!(true));
        object
            .entry("bypassPermissionsModeAccepted".to_owned())
            .or_insert(json!(true));
        object
            .entry("isQualifiedForDataSharing".to_owned())
            .or_insert(json!(false));
    }
    let _ = write_json_file(&path, &value);
}
🤖 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/src/agents/runtime_capabilities.rs` around lines 786 - 796, The
write_claude_user_config helper is overwriting the entire claude.json instead of
preserving existing user settings. Update write_claude_user_config to read the
current Claude config from config_dir.join("claude.json"), merge in only the
Wegent-owned keys from the new JSON value, and then write the merged result back
using write_json_file. Keep the change localized to write_claude_user_config and
ensure unrelated fields already present in the user’s Claude config remain
untouched.
executor/tests/agent_runtime_capabilities_contract.rs (1)

223-233: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound one-shot test servers with a timeout.

If the runtime regresses and never connects, server.await.unwrap() waits forever. Wrap these JoinHandle awaits with tokio::time::timeout so failures are reported locally and quickly.

Proposed fix
-    server.await.unwrap();
+    tokio::time::timeout(std::time::Duration::from_secs(5), server)
+        .await
+        .expect("mock backend did not receive the expected request")
+        .unwrap();

Also applies to: 268-270, 281-295, 331-339

🤖 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/tests/agent_runtime_capabilities_contract.rs` around lines 223 -
233, The one-shot test server tasks can hang forever if the runtime never
connects, so the current JoinHandle awaits in the agent runtime capability
contract tests need a timeout. Update the async test helpers around the server
spawn logic to wrap each server.await.unwrap() in tokio::time::timeout, and
apply the same pattern in the related test blocks that use the spawned server
handles so failures surface locally and quickly instead of blocking the test
suite.

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

Caution

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

⚠️ Outside diff range comments (1)
executor/tests/agent_command_contract.rs (1)

407-426: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Isolate executor-home in this test as well.

Line 407 only overrides WORKSPACE_ROOT, but the code under test now reads saved sessions from the executor-home path. If the process HOME or temp fallback already contains sessions/77/.claude_session_id_987, this assertion becomes machine-state dependent. Point WEGENT_EXECUTOR_HOME or HOME at a fresh unique_dir() here too. As per coding guidelines, "keep tests isolated."

🤖 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/tests/agent_command_contract.rs` around lines 407 - 426, This test
is still coupled to ambient machine state because `build_claude_command` can
read saved sessions from executor-home, not just `WORKSPACE_ROOT`. Update
`claude_command_ignores_legacy_workspace_task_session_file` to also point
`WEGENT_EXECUTOR_HOME` or `HOME` at a fresh `unique_dir()` so the session lookup
cannot pick up an existing `sessions/77/.claude_session_id_987`. Keep the setup
fully isolated alongside the existing `env_lock`, `EnvGuard::set`, and
`unique_dir` usage.

Source: Coding guidelines

🤖 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/execution/response_bridge.py`:
- Around line 147-159: Update EmitterBridgeTransport._convert_event so it
mirrors the current ResponsesAPIEventParser contract instead of the legacy
mapping. Make sure response.created is translated to START, and split
function-call completion into TOOL_ARGUMENT_DONE for
response.function_call_arguments.done followed by TOOL_RESULT for
response.output_item.done when the item is a function call. Keep the DONE
handling in response_completed consistent with the parser’s event flow and reuse
the existing event type symbols in response_bridge.py to align in-process
execution with SSE/callback mode.

In `@executor/src/claude_session.rs`:
- Around line 114-116: The readable_session_file_candidates helper currently
only returns writable_session_file_candidates, which breaks resuming older
sessions stored in legacy workspace-root locations. Update
readable_session_file_candidates to include both the new executor-home candidate
path(s) and the legacy workspace-root .claude_session_id* locations so existing
sessions remain discoverable, while keeping the cleanup logic for legacy files
separate and unchanged.

In `@frontend/src/features/tasks/components/message/MessageBubble.tsx`:
- Around line 526-530: `hasThinkingToolSteps` in `MessageBubble` only checks
`step.details.type`, so nested tool events inside `details.message.content[]`
are missed and `ThinkingDisplay` is gated off incorrectly. Update the
`React.useMemo` logic around `hasThinkingToolSteps` to also scan `msg.thinking`
entries for tool-related content nested in `details.message.content` for
`assistant`/`user` payloads, and make the gate at `ThinkingDisplay` use that
broader detection so nested `tool_use`/`tool_result` steps are rendered.

In `@frontend/src/features/tasks/components/message/thinking/ThinkingDisplay.tsx`:
- Around line 38-43: The renderer selection in ThinkingDisplay.tsx is too narrow
because hasTextTypeSteps(thinking) only detects direct details.type === 'text',
so nested text content inside details.message.content for assistant/user
ThinkingStep entries can be sent to the wrong view. Update the view-selection
logic in ThinkingDisplay to also treat these message-based text steps as
detailed thinking, and route them to DetailedThinkingView instead of falling
through to ToolBlocksView. Use the existing hasTextTypeSteps predicate and the
ThinkingStep shape as the place to expand the detection.

---

Outside diff comments:
In `@executor/tests/agent_command_contract.rs`:
- Around line 407-426: This test is still coupled to ambient machine state
because `build_claude_command` can read saved sessions from executor-home, not
just `WORKSPACE_ROOT`. Update
`claude_command_ignores_legacy_workspace_task_session_file` to also point
`WEGENT_EXECUTOR_HOME` or `HOME` at a fresh `unique_dir()` so the session lookup
cannot pick up an existing `sessions/77/.claude_session_id_987`. Keep the setup
fully isolated alongside the existing `env_lock`, `EnvGuard::set`, and
`unique_dir` usage.
🪄 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: 246fee81-d50f-495d-a6c1-68fc643bc7b3

📥 Commits

Reviewing files that changed from the base of the PR and between aa90e8d and ba52971.

📒 Files selected for processing (16)
  • backend/app/services/execution/dispatcher.py
  • backend/app/services/execution/response_bridge.py
  • backend/tests/services/execution/test_responses_api_event_parser.py
  • executor/src/claude_session.rs
  • executor/src/emitter/mod.rs
  • executor/src/process/mod.rs
  • executor/src/stream/mod.rs
  • executor/tests/agent_command_contract.rs
  • executor/tests/agent_process_engine_contract.rs
  • executor/tests/claude_stream_event_contract.rs
  • executor/tests/local_backend_contract.rs
  • frontend/src/__tests__/features/tasks/components/message/MessageBubble.test.tsx
  • frontend/src/__tests__/features/tasks/components/message/thinking/ThinkingDisplay.test.tsx
  • frontend/src/features/tasks/components/message/MessageBubble.tsx
  • frontend/src/features/tasks/components/message/thinking/ThinkingDisplay.tsx
  • frontend/src/features/tasks/components/message/thinking/types.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • executor/tests/agent_process_engine_contract.rs
  • executor/src/process/mod.rs
  • executor/src/emitter/mod.rs

Comment on lines 147 to 159
elif event_type == ResponsesAPIStreamEvents.RESPONSE_COMPLETED.value:
response_data = data.get("response", {})
result = extract_completed_result(response_data)
if not result.get("reasoning_content") and self._reasoning_content:
result["reasoning_content"] = self._reasoning_content
return ExecutionEvent(
type=EventType.DONE.value,
task_id=self.task_id,
subtask_id=self.subtask_id,
content="",
result=extract_completed_result(response_data),
result=result,
message_id=message_id,
)

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

Mirror the parser’s event contract in EmitterBridgeTransport._convert_event.

ResponsesAPIEventParser now emits response.created -> START and splits function-call completion into TOOL_ARGUMENT_DONE (response.function_call_arguments.done) followed by TOOL_RESULT (response.output_item.done(function_call)), but this bridge still uses the old mapping. That leaves in-process execution on a different event contract than SSE/callback mode.

Suggested direction
+        if event_type == ResponsesAPIStreamEvents.RESPONSE_CREATED.value:
+            start_data = {
+                key: data.get(key)
+                for key in ("shell_type", "bot_name")
+                if data.get(key) is not None
+            }
+            return ExecutionEvent(
+                type=EventType.START.value,
+                task_id=self.task_id,
+                subtask_id=self.subtask_id,
+                data=start_data,
+                message_id=message_id,
+            )
+
         elif event_type == ResponsesAPIStreamEvents.FUNCTION_CALL_ARGUMENTS_DONE.value:
             ...
             return ExecutionEvent(
-                type=EventType.TOOL_RESULT.value,
+                type=EventType.TOOL_ARGUMENT_DONE.value,
                 ...
             )

         elif event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE.value:
             item = data.get("item", {})
+            if item.get("type") == "function_call":
+                ...
+                return ExecutionEvent(
+                    type=EventType.TOOL_RESULT.value,
+                    ...
+                )
             if item.get("type") != "shell_call":
                 return None

         elif event_type in (
-            ResponsesAPIStreamEvents.RESPONSE_CREATED.value,
             ResponsesAPIStreamEvents.RESPONSE_IN_PROGRESS.value,
             ...
         ):

Also applies to: 411-414

🤖 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/execution/response_bridge.py` around lines 147 - 159,
Update EmitterBridgeTransport._convert_event so it mirrors the current
ResponsesAPIEventParser contract instead of the legacy mapping. Make sure
response.created is translated to START, and split function-call completion into
TOOL_ARGUMENT_DONE for response.function_call_arguments.done followed by
TOOL_RESULT for response.output_item.done when the item is a function call. Keep
the DONE handling in response_completed consistent with the parser’s event flow
and reuse the existing event type symbols in response_bridge.py to align
in-process execution with SSE/callback mode.

Comment on lines +114 to +116
fn readable_session_file_candidates(request: &ExecutionRequest) -> Vec<PathBuf> {
writable_session_file_candidates(request)
}

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

This drops resume compatibility for existing legacy session files.

Line 115 now reads only from the executor-home path, so any pre-existing .claude_session_id* saved under workspace roots becomes unreadable even though those same paths are still treated as legacy state worth deleting later. That means in-flight tasks created before this path migration will stop resuming after rollout.

Suggested fix
 fn readable_session_file_candidates(request: &ExecutionRequest) -> Vec<PathBuf> {
-    writable_session_file_candidates(request)
+    let mut candidates = writable_session_file_candidates(request);
+    candidates.extend(legacy_workspace_session_file_candidates(request));
+    dedup_paths(candidates)
 }
📝 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
fn readable_session_file_candidates(request: &ExecutionRequest) -> Vec<PathBuf> {
writable_session_file_candidates(request)
}
fn readable_session_file_candidates(request: &ExecutionRequest) -> Vec<PathBuf> {
let mut candidates = writable_session_file_candidates(request);
candidates.extend(legacy_workspace_session_file_candidates(request));
dedup_paths(candidates)
}
🤖 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/src/claude_session.rs` around lines 114 - 116, The
readable_session_file_candidates helper currently only returns
writable_session_file_candidates, which breaks resuming older sessions stored in
legacy workspace-root locations. Update readable_session_file_candidates to
include both the new executor-home candidate path(s) and the legacy
workspace-root .claude_session_id* locations so existing sessions remain
discoverable, while keeping the cleanup logic for legacy files separate and
unchanged.

Comment on lines +526 to +530
const hasThinkingToolSteps = React.useMemo(
() =>
msg.thinking?.some(
step => step.details?.type === 'tool_use' || step.details?.type === 'tool_result'
) ?? false,

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

Nested tool events in msg.thinking will be hidden by this gate.

Line 528 only treats direct details.type === 'tool_use' | 'tool_result' as tool steps. Valid ThinkingStep payloads also carry tool events inside details.message.content[] for assistant/user messages, so hasThinkingToolSteps stays false for those cases and the ThinkingDisplay gate at Lines 1419-1423 suppresses them whenever result.blocks is non-empty.

Suggested fix
 const hasThinkingToolSteps = React.useMemo(
   () =>
     msg.thinking?.some(
-      step => step.details?.type === 'tool_use' || step.details?.type === 'tool_result'
+      step =>
+        step.details?.type === "tool_use" ||
+        step.details?.type === "tool_result" ||
+        (step.details?.message?.content?.some(
+          item => item.type === "tool_use" || item.type === "tool_result"
+        ) ?? false)
     ) ?? false,
   [msg.thinking]
 )

Also applies to: 1419-1423

🤖 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 `@frontend/src/features/tasks/components/message/MessageBubble.tsx` around
lines 526 - 530, `hasThinkingToolSteps` in `MessageBubble` only checks
`step.details.type`, so nested tool events inside `details.message.content[]`
are missed and `ThinkingDisplay` is gated off incorrectly. Update the
`React.useMemo` logic around `hasThinkingToolSteps` to also scan `msg.thinking`
entries for tool-related content nested in `details.message.content` for
`assistant`/`user` payloads, and make the gate at `ThinkingDisplay` use that
broader detection so nested `tool_use`/`tool_result` steps are rendered.

Comment on lines +38 to +43
// Text-type steps (e.g. deep research) need the detailed renderer.
if (hasTextTypeSteps(thinking)) {
return <DetailedThinkingView thinking={thinking} taskStatus={taskStatus} />
}

// Default to detailed view for ClaudeCode, Agno, and other shell types
return <DetailedThinkingView thinking={thinking} taskStatus={taskStatus} />
return <ToolBlocksView thinking={thinking} taskStatus={taskStatus} />

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

Nested text thinking can now be routed to the wrong view.

Line 39 makes the renderer choice depend only on hasTextTypeSteps(thinking), but that predicate only catches direct details.type === 'text'. ThinkingStep also carries text inside details.message.content[] for assistant/user items, and DetailedThinkingView is the component that actually renders that shape, so those entries now fall through to ToolBlocksView.

Suggested fix
 function hasTextTypeSteps(thinking: ThinkingDisplayProps['thinking']): boolean {
   if (!thinking || thinking.length === 0) return false
-  return thinking.some(step => step.details?.type === 'text')
+  return thinking.some(step => {
+    if (step.details?.type === 'text') return true
+    return (
+      step.details?.message?.content?.some(
+        content => content.type === 'text' && Boolean(content.text?.trim())
+      ) ?? false
+    )
+  })
 }
🤖 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 `@frontend/src/features/tasks/components/message/thinking/ThinkingDisplay.tsx`
around lines 38 - 43, The renderer selection in ThinkingDisplay.tsx is too
narrow because hasTextTypeSteps(thinking) only detects direct details.type ===
'text', so nested text content inside details.message.content for assistant/user
ThinkingStep entries can be sent to the wrong view. Update the view-selection
logic in ThinkingDisplay to also treat these message-based text steps as
detailed thinking, and route them to DetailedThinkingView instead of falling
through to ToolBlocksView. Use the existing hasTextTypeSteps predicate and the
ThinkingStep shape as the place to expand the detection.

@qdaxb
qdaxb merged commit 2cfa507 into wecode-ai:main Jun 29, 2026
27 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