* refactor(sdk): collapse plan updates to typed steps
Per owner decision, remove the shipped steps: string[] SDK field and its unreleased planSteps replacement early. Collapse onPlanUpdate to one typed steps field. Retain wire-level string normalization for external Codex plugin version skew.
* chore(sdk): tighten rebased surface budget
* feat(channels): render live plan checklists from typed update_plan snapshots
Codex, Copilot, CLI, and embedded update_plan events now carry typed
{step, status} snapshots end to end instead of flattened strings. Slack,
Discord, Telegram, Matrix, and MS Teams render a live checklist in
progress drafts; Slack native task cards show the real plan with
position-keyed rows and id reconciliation. The embedded update_plan tool
is default-on for every model (tools.experimental.planTool: false opts
out). The shipped onPlanUpdate steps: string[] SDK field stays populated
during a deprecation window alongside the canonical planSteps.
* fix(auto-reply): align plan bridge types with SDK callback contract
* fix(auto-reply): split agent event bridge module and fix plan type boundaries
* fix(channels): satisfy oxlint on plan checklist helpers
* feat: correlate native search outcomes in audit history
Metadata-only audit ledger for agent runs and tool actions in the shared
state DB: stable event identity, closed action/status/error vocabularies,
one-way-hashed tool-call ids, never-inferred terminal outcomes for native
web-search (explicit completed/failed only; otherwise unknown), bounded
retention, audit.list gateway RPC and openclaw audit CLI. Squashed from
the 82-commit audit stack for replay onto current main.
* feat(audit): add audit.enabled config gate (default on)
The metadata-only audit ledger records by default: an audit trail enabled
only after an incident cannot explain the incident, and the rows are
strictly less sensitive than the transcripts every install already
stores. audit.enabled=false stops new writes at the gateway subscription
seam; audit.list and openclaw audit keep serving existing records until
they expire. Documented in the configuration reference, protocol page,
and CLI reference.
* fix: repair full-matrix CI findings after rebase
- break the dynamic-tools/dynamic-tool-execution import cycle by
extracting resolveCodexToolAbortTerminalReason into a leaf module
- restore main's session-worktree protocol exports lost in the
index.ts auto-merge
- register the audit event writer worker as a knip entry point
- docs table formatting; subagent wait-cancellation test scoped to its
audit intent (outcome + timing) and advanced past main's new
lifecycle-timeout retry grace
The claude-cli (claude-stdio) live-session backend emits native
reasoning on stream-json (`thinking_delta` + a snapshot `thinking`
block), but the CLI parser had no thinking handling at all, so it was
silently dropped on every surface in every /reasoning mode.
- cli-output.ts: parse thinking_delta/snapshot into stream:"thinking"
with replace-style per-content-block-index dedupe
(assembleThinkingTextByIndex), robust to tool-interleaved multi-block
turns (snapshot replaces per-index rather than assuming a streamed
prefix). signature_delta/redacted_thinking stay skipped.
- agent-runner-cli-dispatch.ts: bridge stream:"thinking" into
onReasoningStream via a new createReasoningTextBridge (mirrors the
existing assistant-text bridge), replacing the old
assistant-text-as-reasoning heuristic. Track the final reasoning
snapshot and, when present, prepend a durable
`{text, isReasoning: true}` payload before the answer — mirroring
the embedded-runner durable reasoning payload
(embedded-agent-runner/run/payloads.ts).
- agent-runner-execution.ts: thread isReasoningSnapshot through to
onReasoningStream, still passing requiresReasoningProgressOptIn:true
so CLI reasoning rides the exact same channel gates as embedded
reasoning (Discord/Telegram): /reasoning off -> nothing on any
surface, stream -> window preview only, on -> durable payload too.
The bridge is suppressed under the same conditions as the assistant
bridge (e.g. silentExpected follow-ups), so no durable payload is
synthesized for turns that are expected to stay silent.
Supersedes this branch's prior commit (a straight port of
af7eb48e05's emit-always model, no gating and no durable mode) with
the above gated/durable design per product decision.
Also, in cli-output.ts: reset the per-content-block-index thinking
tracker on every new Anthropic message (message_start, or a fresh
top-level "assistant" snapshot's message.id) instead of leaving it
scoped to the whole CLI turn. Content-block indices restart at 0 per
Anthropic message, so a tool round-trip within one turn (message A:
thinking + tool_use; tool result fed back; message B: a fresh
message_start) was letting message A's stale index-0 thinking text
bleed into message B's index-0 delta.
And in agent-runner-cli-dispatch.ts: attach the durable reasoning
payload whenever any thinking was bridged, not only when the CLI also
produced a visible final answer, so a thinking-only turn (no visible
reply) doesn't silently drop its reasoning.
Round 2: the durable `{isReasoning: true}` payload synthesized above
was also reaching the QUEUED FOLLOW-UP delivery path
(followup-runner.ts -> resolveFollowupDeliveryPayloads) with no gate
at all, so a claude-cli queued follow-up with /reasoning off would
leak internal reasoning to the channel as an ordinary visible message.
resolveFollowupDeliveryPayloads (followup-delivery.ts) now takes a
reasoningPayloadsEnabled flag and drops isReasoning payloads unless
it is true; followup-runner.ts threads opts?.reasoningPayloadsEnabled
through at both call sites, from the same GetReplyOptions the direct
path already reads (dispatch-from-config.ts:2054). This makes the
queued-follow-up path apply the identical isReasoning/
reasoningPayloadsEnabled gate that direct dispatch already enforces
(dispatch-from-config.ts:3347, :3537) — no new mechanism, CLI
follow-ups now go through the exact same gate embedded follow-ups do.
Scope note: routeReply's own unconditional suppression of isReasoning
payloads on the origin-routing branch (route-reply.ts:131,
shouldSuppressReasoningPayload) is pre-existing, shared with the
embedded runner, and unchanged by this commit — that gate governs
whether a kept-and-routed reasoning payload is actually rendered on a
cross-channel queued follow-up, and is out of scope here. This commit
closes the leak (off -> never eligible on any path); it does not
change what happens to an eligible payload once it reaches routeReply.
Co-Authored-By: Claude <noreply@anthropic.com>
* Mark active main sessions during restart shutdown
* Type restart marker mock in close tests
* fix(gateway): preserve active run ownership across restart
* fix(gateway): preserve active runs across restart
* fix(gateway): close restart recovery edge cases
* fix(cron): preserve lifecycle ownership across restart
* fix(gateway): release rejected run contexts
* fix(gateway): preserve restart lifecycle ownership
* fix(cron): retain overlapping run ownership
* fix(agents): preserve restart terminal precedence
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The CLI parser already emits tool result events (name, toolCallId, isError,
sanitized result), but the runner bridge dropped them, so CLI-backed runs had
no durable tool record under verbose while embedded runs did. The bridge now
forwards result events, and both runners feed a summary tracker that renders
the same formatToolAggregate line the embedded runner emits (meta captured
from the start event args), plus the tool output block when full verbose
output is enabled. Delivery rides each runner's existing tool-result route, so
verbose gating, ordering ahead of the final answer, and the Telegram durable
routing all apply unchanged.