Fix gateway tool output visibility and timing - #2555
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves how the web gateway surfaces tool activity by correlating live tool events via call_id, exposing actual tool output (when persisted), and carrying real execution timings (duration_ms) end-to-end so the UI can render accurate tool cards and durations.
Changes:
- Add
call_id(andduration_msfor completions) to tool-relatedStatusUpdate/AppEventvariants and preserve these fields through the bridge and web channel layers. - Use persisted tool-call
resultto populate expanded history tool cards (with display truncation), avoiding new history storage. - Refactor frontend tool activity rendering to a controller that correlates events by
call_idand formats durations in a millisecond-friendly way.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ws_gateway_integration.rs | Asserts WS payloads include call_id and duration_ms for tool events. |
| tests/support_unit_tests.rs | Updates test fixtures for new duration_ms field on ToolCompleted. |
| src/channels/web/util.rs | Adds tool_result_for_display and threads persisted result + call_id into ToolCallInfo. |
| src/channels/web/types.rs | Extends ToolCallInfo DTO with call_id and result. |
| src/channels/web/tests/tool_event_passthrough.rs | Regression test ensuring gateway preserves tool identity/timing fields to SSE. |
| src/channels/web/tests/mod.rs | Registers new tool passthrough test module. |
| src/channels/web/server.rs | Includes call_id and display-ready result for in-memory turn tool calls. |
| src/channels/web/responses_api.rs | Correlates tool events by call_id when building response output items. |
| src/channels/web/mod.rs | Ensures call_id/duration_ms are passed through as AppEvents. |
| src/channels/wasm/wrapper.rs | Updates WASM channel tests for duration_ms. |
| src/channels/channel.rs | Adds duration_ms to StatusUpdate::ToolCompleted and propagates it in constructor helpers/tests. |
| src/bridge/router.rs | Preserves engine action call_id and forwards duration_ms via tool-status events. |
| src/agent/thread_ops.rs | Measures tool execution durations and passes duration_ms into status updates. |
| src/agent/dispatcher.rs | Measures tool execution durations and passes duration_ms into status updates. |
| crates/ironclaw_gateway/static/app.js | Refactors tool activity cards to correlate by call_id, show persisted results, and format ms durations. |
| crates/ironclaw_common/src/event.rs | Adds optional call_id and duration_ms fields to tool-related AppEvents. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Code Review
This pull request introduces call_id and duration_ms fields across the system to improve the tracking and display of tool activity. Key changes include refactoring the frontend tool activity state into a reusable controller, adding execution timing in the agent dispatcher, and updating event propagation to preserve tool identity. A logic error was identified in the frontend's tool correlation function where name-based fallback could lead to incorrect state updates during parallel tool calls.
|
Addressed the open review comments on
Validation after the follow-up patch:
Follow-up commit pushed: |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 733f5b95ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
henrypark133
left a comment
There was a problem hiding this comment.
Review: keep Responses API tool completions correlated per call
Most of the earlier tool-visibility feedback looks addressed, and the new call_id / duration_ms plumbing is much cleaner. One correctness issue still remains in the streaming Responses API path.
Critical: response.output_item.done still uses one global tool slot
File: src/channels/web/responses_api.rs:947
The streaming worker now threads call_id into the FunctionCall and FunctionCallOutput items, but it still tracks completion with a single current_tool_index. If tool A starts, tool B starts, and tool A completes first, the later start overwrites that slot and the code emits response.output_item.done for B instead of A. The final output list keeps the right call_id, but streamed clients can observe the wrong item transition to done and never get a done event for the actual completed call.
Suggested fix: track in-flight output indexes by call_id (or look them up by call_id on completion) instead of using a single mutable index.
Recommended verdict: Request changes.
Residual risk: I also kicked off a couple of targeted tests from the worktree, but they were still compiling when I posted this review.
a10f575 to
ca654d8
Compare
Live in-memory turns have only the full tool result, not a separately persisted short preview. Populate `ToolCallInfo.result` from the live value and leave `result_preview` empty so both paths surface the same field semantics to the UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -2761,15 +2790,12 @@ function addToolCard(name, callId) { | |||
|
|
|||
| const icon = document.createElement('span'); | |||
| icon.className = 'activity-tool-icon'; | |||
| icon.innerHTML = '<div class="spinner"></div>'; | |||
|
|
|||
| const toolName = document.createElement('span'); | |||
| toolName.className = 'activity-tool-name'; | |||
| toolName.textContent = name; | |||
|
|
|||
| const duration = document.createElement('span'); | |||
| duration.className = 'activity-tool-duration'; | |||
| duration.textContent = ''; | |||
|
|
|||
| const chevron = document.createElement('span'); | |||
| chevron.className = 'activity-tool-chevron'; | |||
| @@ -2787,175 +2813,332 @@ function addToolCard(name, callId) { | |||
| output.className = 'activity-tool-output'; | |||
| body.appendChild(output); | |||
|
|
|||
| const rendered = { entry, card, header, icon, toolName, duration, chevron, body, output, timer: null }; | |||
| header.addEventListener('click', () => { | |||
| const isExpanded = body.classList.contains('expanded'); | |||
| setActivityToolExpanded(header, body, chevron, !isExpanded); | |||
| const willExpand = !body.classList.contains('expanded'); | |||
| setToolActivityCardExpanded(rendered, willExpand); | |||
| }); | |||
There was a problem hiding this comment.
createToolActivityCard/applyToolActivityCardState always leaves the header as an enabled <button> with an active chevron, even when there is no body text (no result/error/preview). This creates a focusable control that expands/collapses an empty region, which is confusing UX and an accessibility regression vs the previous behavior that disabled the header and hid the chevron when there was nothing to expand. Consider disabling the header (and hiding the chevron) whenever rendered.output.textContent is empty, and only wiring expansion/aria affordances when there is expandable content.
GitHub Actions dropped the Code Style workflow on the prior push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rust 1.95's stricter clippy::collapsible_match warning trips on the inner `if` inside the ToolResult arm. Fold the preview check into the arm's guard to match the same predicate-in-guard style as the arm above. Fixes the Clippy (all-features) CI failure inherited from staging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| container.scrollTop = container.scrollHeight; | ||
| function getToolActivityBodyText(entry) { | ||
| if (!entry) return ''; | ||
| return entry.error || entry.result || entry.result_preview || ''; |
There was a problem hiding this comment.
getToolActivityBodyText() returns entry.error || entry.result || entry.result_preview, which means historical tool cards with both error and result_preview will now hide the preview entirely (previously the history UI showed preview + error text together). Consider composing the body when error is present (e.g., include result/result_preview plus an Error: section) so failures don’t lose the tool output preview.
| return entry.error || entry.result || entry.result_preview || ''; | |
| const output = entry.result || entry.result_preview || ''; | |
| if (!entry.error) return output; | |
| return output ? (output + '\n\nError:\n' + entry.error) : ('Error:\n' + entry.error); |
…onclaw#2599 stage 1
First increment of the ironclaw#2599 gateway platform/feature split. Moves
the shared gateway state and the unauthenticated static-serving surface out
of the 8.5k-line `server.rs` monolith into a new `platform/` subtree:
- `platform/state.rs` — `GatewayState`, `RateLimiter`, `PerUserRateLimiter`,
`WorkspacePool` (+ `WorkspaceResolver` impl), `FrontendHtmlCache`,
`FrontendCacheKey`, `ActiveConfigSnapshot`, `PromptQueue`,
`RoutineEngineSlot`, `rate_limit_key_from_headers`.
- `platform/static_files.rs` — CSP directive set + `BASE_CSP_HEADER`
(single source of truth for both the global header layer and the
per-response nonce variant), `build_frontend_html` + cache-key plumbing,
unauthenticated static handlers (`/`, `/style.css`, `/app.js`,
`/theme.css`, `/favicon.ico`, `/i18n/*`, `/admin*`, `/api/health`), and
the authenticated `/projects/{id}/...` file-serving routes with the
ownership check.
`server.rs` keeps `start_server()`, the route table, and the feature
handlers that have not yet moved (OAuth callbacks, chat, extensions,
pairing, logs, gateway status). It re-exports the relocated state types
under their old paths so the 30+ external call sites that reach for
`crate::channels::web::server::GatewayState` continue to resolve without
churn — follow-up PRs update them incrementally.
`CLAUDE.md` file map is updated to document `platform/` vs the
transitional `handlers/` folder and explains the layering rule:
features depend on platform, never the reverse.
No behavior change — route table, CSP policy, cache semantics, and
multi-tenant guard rails are byte-identical. `cargo clippy --all
--benches --tests --examples --all-features` is clean; the relocated
tests (`test_base_csp_header_matches_build_csp_none`,
`test_stamp_nonce_into_html_*`, `workspace_pool_resolve_seeds_new_user_workspace`,
`test_build_frontend_html_returns_none_in_multi_tenant_mode`, etc.) pass
against the re-exported surface.
Scope is intentionally narrow to avoid colliding with the open
XL PRs that touch `server.rs` (#2548 workspace entities, #2555 tool
output, #2532 engine v2 sidebar) — this PR does not modify anything
inside the feature-handler blocks those PRs edit.
Stats: server.rs 8534 → 7463 lines (−1071); new files 1210 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onclaw#2599 stage 1 (#2628) First increment of the ironclaw#2599 gateway platform/feature split. Moves the shared gateway state and the unauthenticated static-serving surface out of the 8.5k-line `server.rs` monolith into a new `platform/` subtree: - `platform/state.rs` — `GatewayState`, `RateLimiter`, `PerUserRateLimiter`, `WorkspacePool` (+ `WorkspaceResolver` impl), `FrontendHtmlCache`, `FrontendCacheKey`, `ActiveConfigSnapshot`, `PromptQueue`, `RoutineEngineSlot`, `rate_limit_key_from_headers`. - `platform/static_files.rs` — CSP directive set + `BASE_CSP_HEADER` (single source of truth for both the global header layer and the per-response nonce variant), `build_frontend_html` + cache-key plumbing, unauthenticated static handlers (`/`, `/style.css`, `/app.js`, `/theme.css`, `/favicon.ico`, `/i18n/*`, `/admin*`, `/api/health`), and the authenticated `/projects/{id}/...` file-serving routes with the ownership check. `server.rs` keeps `start_server()`, the route table, and the feature handlers that have not yet moved (OAuth callbacks, chat, extensions, pairing, logs, gateway status). It re-exports the relocated state types under their old paths so the 30+ external call sites that reach for `crate::channels::web::server::GatewayState` continue to resolve without churn — follow-up PRs update them incrementally. `CLAUDE.md` file map is updated to document `platform/` vs the transitional `handlers/` folder and explains the layering rule: features depend on platform, never the reverse. No behavior change — route table, CSP policy, cache semantics, and multi-tenant guard rails are byte-identical. `cargo clippy --all --benches --tests --examples --all-features` is clean; the relocated tests (`test_base_csp_header_matches_build_csp_none`, `test_stamp_nonce_into_html_*`, `workspace_pool_resolve_seeds_new_user_workspace`, `test_build_frontend_html_returns_none_in_multi_tenant_mode`, etc.) pass against the re-exported surface. Scope is intentionally narrow to avoid colliding with the open XL PRs that touch `server.rs` (#2548 workspace entities, #2555 tool output, #2532 engine v2 sidebar) — this PR does not modify anything inside the feature-handler blocks those PRs edit. Stats: server.rs 8534 → 7463 lines (−1071); new files 1210 lines. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR Re-Review (post-fix, head
|
Two independent staging CI regressions: 1. Docker Build was failing because `cargo install wasm-tools@1.246.1` re-resolved to the newest compatible `constant_time_eq@0.4.3`, which requires rustc >= 1.95, while the chef stage is pinned to rust:1.92. Add `--locked` so cargo uses the Cargo.lock shipped with each crate. 2. `test_builtin_echo_tool` started failing after PR #2555 intentionally aligned the in-memory history path with DB semantics: tool previews now surface in `result` with `result_preview` left empty. The test only inspected `result_preview`, so it timed out. Accept the preview from either field in `_wait_for_turn`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent staging CI regressions: 1. Docker Build was failing because `cargo install wasm-tools@1.246.1` re-resolved to the newest compatible `constant_time_eq@0.4.3`, which requires rustc >= 1.95, while the chef stage is pinned to rust:1.92. Add `--locked` so cargo uses the Cargo.lock shipped with each crate. 2. `test_builtin_echo_tool` started failing after PR #2555 intentionally aligned the in-memory history path with DB semantics: tool previews now surface in `result` with `result_preview` left empty. The test only inspected `result_preview`, so it timed out. Accept the preview from either field in `_wait_for_turn`. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onclaw#2599 stage 1 (nearai#2628) First increment of the ironclaw#2599 gateway platform/feature split. Moves the shared gateway state and the unauthenticated static-serving surface out of the 8.5k-line `server.rs` monolith into a new `platform/` subtree: - `platform/state.rs` — `GatewayState`, `RateLimiter`, `PerUserRateLimiter`, `WorkspacePool` (+ `WorkspaceResolver` impl), `FrontendHtmlCache`, `FrontendCacheKey`, `ActiveConfigSnapshot`, `PromptQueue`, `RoutineEngineSlot`, `rate_limit_key_from_headers`. - `platform/static_files.rs` — CSP directive set + `BASE_CSP_HEADER` (single source of truth for both the global header layer and the per-response nonce variant), `build_frontend_html` + cache-key plumbing, unauthenticated static handlers (`/`, `/style.css`, `/app.js`, `/theme.css`, `/favicon.ico`, `/i18n/*`, `/admin*`, `/api/health`), and the authenticated `/projects/{id}/...` file-serving routes with the ownership check. `server.rs` keeps `start_server()`, the route table, and the feature handlers that have not yet moved (OAuth callbacks, chat, extensions, pairing, logs, gateway status). It re-exports the relocated state types under their old paths so the 30+ external call sites that reach for `crate::channels::web::server::GatewayState` continue to resolve without churn — follow-up PRs update them incrementally. `CLAUDE.md` file map is updated to document `platform/` vs the transitional `handlers/` folder and explains the layering rule: features depend on platform, never the reverse. No behavior change — route table, CSP policy, cache semantics, and multi-tenant guard rails are byte-identical. `cargo clippy --all --benches --tests --examples --all-features` is clean; the relocated tests (`test_base_csp_header_matches_build_csp_none`, `test_stamp_nonce_into_html_*`, `workspace_pool_resolve_seeds_new_user_workspace`, `test_build_frontend_html_returns_none_in_multi_tenant_mode`, etc.) pass against the re-exported surface. Scope is intentionally narrow to avoid colliding with the open XL PRs that touch `server.rs` (nearai#2548 workspace entities, nearai#2555 tool output, nearai#2532 engine v2 sidebar) — this PR does not modify anything inside the feature-handler blocks those PRs edit. Stats: server.rs 8534 → 7463 lines (−1071); new files 1210 lines. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix gateway tool output visibility * Address PR review follow-ups * fix(web): truncate live tool activity previews * fix(engine): preserve failed tool durations in v2 gateway events * fix(engine): default missing ActionFailed durations * style: format scripting executor * fix(web): keep history tool results aligned with preview * fix(web): restore persisted tool result parsing * fix(web): align in-memory turn result/preview with DB path Live in-memory turns have only the full tool result, not a separately persisted short preview. Populate `ToolCallInfo.result` from the live value and leave `result_preview` empty so both paths surface the same field semantics to the UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: re-trigger CI GitHub Actions dropped the Code Style workflow on the prior push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tests): collapse nested match arm in live_harness Rust 1.95's stricter clippy::collapsible_match warning trips on the inner `if` inside the ToolResult arm. Fold the preview check into the arm's guard to match the same predicate-in-guard style as the arm above. Fixes the Clippy (all-features) CI failure inherited from staging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2661) Two independent staging CI regressions: 1. Docker Build was failing because `cargo install wasm-tools@1.246.1` re-resolved to the newest compatible `constant_time_eq@0.4.3`, which requires rustc >= 1.95, while the chef stage is pinned to rust:1.92. Add `--locked` so cargo uses the Cargo.lock shipped with each crate. 2. `test_builtin_echo_tool` started failing after PR nearai#2555 intentionally aligned the in-memory history path with DB semantics: tool previews now surface in `result` with `result_preview` left empty. The test only inspected `result_preview`, so it timed out. Accept the preview from either field in `_wait_for_turn`. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Testing