Claude Code forwards Agent-tool subagent records with parent_tool_use_id set; the streaming parser treated them as parent records, so subagent thinking streamed as the assistant's reasoning and subagent tool calls surfaced as unattributed top-level tool events. Gate the thinking/tool dispatchers and assistant-snapshot path on one shared isClaudeSubagentRecord guard.
Also read Claude result errors[] (skipping [ede_diagnostic] telemetry) for every error result instead of reporting only the subtype name.
Fixture: live Claude Code 2.1.234 stream-json capture with a background Explore subagent. Telegram E2E on the claude-cli backend shows the progress draft with only the parent Agent row.
With no servers configured, mcp probe emitted only its header line and exited
0, while mcp list and mcp doctor both name the condition and the command that
fixes it. Short-circuit the human path with the same guidance and skip building
a runtime for zero servers; JSON output keeps its existing empty envelope.
Browser screenshot results always named the message tool even when policy had
removed it from the model's effective tool surface. Report the staged sanitized
outbound copy and its path without naming another independently gated tool, so
the result remains actionable without becoming hallucination bait. Update the
vision, fallback, and raw-image result tests to protect that contract.
Transcript search excludes sessions whose indexes are rebuilding and already
returns indexing=true, but the model-facing tool gave no guidance that its
results were incomplete. Add a bounded warning telling the model to retry
sessions_search shortly, and cover the exact result contract with a regression
test that fails on the previous behavior.
The gateway-routed path already mapped terminal run status to an exit code,
but the local embedded path did not, so a failed turn exited 0 while its own
JSON envelope reported status "error". Route both through the canonical
agent-run terminal outcome, and fail closed on an unrecognized status since
the gateway response carries an open string.
worker_session_placement_moves.target_machine_class was in the canonical state schema and the feature lazy ensureColumn path but not in CLAW_LAZY_ADDITIVE_STATE_COLUMN_DEFINITIONS, so a global state DB that already had the lazily-created table failed the strict startup schema check (column definitions differ) and the gateway crash-looped. Register it as lazy-additive so startup repair adds the column in place, like the sibling worker_session_placements entries.
* fix(cli): render gateway transport failures as expected conditions
An unreachable gateway is an expected, recoverable operator condition, but
only the devices command family surfaced it through the root failure handler,
where it rendered as "The CLI command failed" with debug hints. Extract
GatewayTransportError into its own module so the failure renderer can classify
it without importing the transport stack, and treat it as an expected CLI
error alongside missing credentials.
* fix(gateway): import the transport error kind used by the JSON shape
* test(cli): extract the process-test gateway harness
The new devices coverage pushed gateway-backed-exit.process.test.ts past the
max-lines budget, and that file is not in the ratchet baseline. Move the mock
Gateway servers into a sibling test-helpers module instead of suppressing the
rule.
* fix(models): reject unknown providers when setting a default model
`openclaw models set` validated only the provider/model syntax, so a
nonexistent provider was written to config with a success message while
config validate, doctor, models status and gateway startup all stayed silent.
An unknown provider now fails without changing config; an unknown model under
a known provider still saves with a warning, since the local catalog can lag a
newly released or self-hosted model. Doctor reports refs already in config.
* fix(models): avoid shadowing the repair warning and the map spread
* fix(ui): stop composer mic hover shift and stray red hover
The device-picker chevron collapsed to width 0 while idle and grew to
20px on hover, shoving the right-aligned mic button 20px left under
the cursor on every mouse-over. Detach the picker into its own
fixed-width ghost button beside the mic instead of morphing the mic's
shape into a split pill; hover only fades the picker's opacity, so the
reserved layout never shifts.
Also fixes the mic showing the send button's loud accent-red hover in
dark/openknot themes: those themes overrode .chat-send-btn:hover
directly, which outranked every variant's own hover rule (including
the intentional neutral mic hover and the stop button's danger hover).
Theme overrides now set a --chat-send-hover-background variable that
each variant's hover rule can still take precedence over.
* chore(scripts): register composer mic hover proof script
Missing package.json entry made the deadcode full-tree scan flag the
new capture script as an unused file, same as the existing
ui:proof:workboard sibling.
* fix(sessions): bound store lookup to stop gateway CPU wedge without dropping retired stores
* fix(sessions): finish bounded retired store resolution
Path-derived ownership now avoids irrelevant registry filesystem identity matching while preserving unreadable and ambiguous registry semantics and cross-root retired-store visibility.
Co-authored-by: SunnyShu0925 <shu.zongyu@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(ui): show session hovercards for every sidebar row beside the rail
Sidebar session rows were excluded from session-link hovercards, so only
the Home (main) row showed a card, and it overlaid the sidebar itself.
Hovercards now open for all sidebar session rows and position to the
right of the sidebar so navigation stays visible.
* fix(ui): yield sidebar previews to active progress hovercards
Sidebar rows already open a progress hovercard when the session has an
active progress card; the new preview card stacked on top of it. The
preview now probes the shared progress-card store (deduped with the
progress provider's own load) and opens only when the row has none.
The /new page renders its own composer, which never wired the `$skill` mention menu owned by the chat composer, so typing `$` there did nothing while /chat offered completions. Narrow the skill-menu module to a small host contract shared by both composers, drive it from the new-session textarea, and refresh commands.list on the first `$`. Also moves `paneDomId` to the composer DOM helpers to break a types→skill-menu→slash-menu→state import cycle.
TTS attempt records only recognized err.name === 'AbortError' as a
timeout, but the shared transport names its abort 'TimeoutError'
(fetch-timeout.ts) and provider operation deadlines throw plain Errors
ending in 'timed out'. Every real HTTP provider timeout was therefore
recorded as reasonCode 'provider_error', dead-ending diagnosis in
/tts status and telemetry — the timeout classifier was dead code.
Recognize all three shapes via one helper shared by the reason code
and the formatted error text. The existing classification test crafted
an AbortError by hand, green-lighting the misclassification; it now
uses the real TimeoutError transport shape.
* fix(google-meet): classify gateway CLI fallback with typed guards instead of message matching
Replaces the message-substring classifier in the Meet CLI's
gateway-unavailable local fallback with the structural
isGatewayTransportError/isGatewayClientRequestError guards (seam shared
verbatim with #125458). Decision surface chosen per Meet semantics:
uncoded transport closes (unreachable sockets, now incl. ETIMEDOUT/
ENETUNREACH) fall back; coded closes incl. 1006 deliberately do not,
since a live gateway may still own Meet browser sessions; unknown-method
fallback stays but requires a structural gateway request error. Adds
ECONNRESET to GATEWAY_UNREACHABLE_SOCKET_CODES so connect-time resets
wrap into the typed unreachable error instead of escaping raw.
* test(process): treat Linux zombies as terminated
The lock auto-releases only while the pre-side-effect validation marker is active; review_init marked side effects before the read-only metadata fetch, so a transient GitHub failure retained the lock and forced a lock-recover loop. The read-only fetch now runs before the marker.
* feat(ui): show worktrees only for Git group folders
* feat(ui): enrich group environment picker
* fix(ui): preserve worktree defaults on probe errors
* fix(ui): reject stale group repository probes
* test(ui): type repository inspection helper
* fix(openai): clamp realtime barge-in truncation
Clamp OpenAI Realtime barge-in truncation to the audio duration produced for each assistant item. This prevents interruption failures observed live when 3,700 ms of decoded audio was paired with a 3,760 ms transport-clock timestamp.
* fix(openai): propagate PCM sideband audio format
The browser sideband negotiated PCM16, but its bridge defaulted to G.711 for duration accounting. Propagate the negotiated format so truncation uses the actual PCM duration and closes ClawSweeper's in-scope finding.