* feat(slack): render live session cards as the default Slack progress mode
Slack streaming.mode default flips partial->progress.
Progress mode renders one live Block Kit session card with a status header, narration, plan, activity, diff stat, and elapsed time; it is edited in place and finalized to success or error with an Open in OpenClaw button when gateway.publicOrigin is set.
Final assistant text always delivers separately.
The shared progress compositor gains a success-only additive per-turn diffStat mirroring the task ledger fold.
resolveGatewayPublicOrigin is exported through the plugin SDK.
The diffs viewer URL falls back to publicOrigin.
The old rich/text progress render fork is deleted.
Native task cards remain unchanged and opt-in.
* chore(config): regenerate bundled channel config metadata
* refactor(slack): keep session card state type internal
* refactor(slack): split session-card and diff-stat owners under lint ceilings
* refactor(channels): reuse diff-stat type from its owner module
Import ChannelProgressDraftDiffStat from progress-draft-diffstat instead of
redeclaring it in the compositor, resolving the all-exports deadcode scan.
* chore(plugin-sdk): regenerate api baselines for channel barrels
Baselines drifted after the rebase reconciled them against main; regenerate to
match the branch's actual channel-message/channel-outbound surface.
* fix(slack): drop a session card that cannot terminalize after final delivery
If the final reply is delivered but the terminal card edit fails, the caller
now clears the stale card instead of leaving it stuck in its Working state
(mirrors the pre-card preview cleanup). Adds a transport-failure regression and
corrects three tests that asserted the prior ignore-the-result behavior.
Documents resolveGatewayPublicOrigin as a dependency-light runtime helper on the
config-contracts SDK subpath, which previously described a type-only surface.
Addresses ClawSweeper P2 (unfinalized card) and P1 (runtime SDK contract).
* fix(slack): suppress default tool messages under the default progress card
resolveChannelStreamingSuppressDefaultToolProgressMessages re-derived the stream
mode from config with an "off" default, unlike its sibling resolvers which take
a caller-resolved mode override. After this branch made progress the Slack
default, a default-config channel turn saw mode "off" and left a stray
"Using tool: X" plain message posting alongside the session card. Thread the
caller-resolved mode through (compositor passes params.mode; Slack dispatch passes
slackStreaming.mode), matching resolveChannelStreamingPreviewToolProgress.
Retarget the progress-session-card delivery-trace golden at an EMPTY Slack config
so it proves the real default path; the regenerated golden is byte-identical,
confirming defaults now yield the clean card sequence (one card post, separate
final text, one terminal update with the Open in OpenClaw button, no stray tool
message). Switch the dispatch delivery-mode mock to the real resolver so the card
tests exercise the true channel default (automatic), not a hand-rolled one.
* chore(plugin-sdk): regenerate api baselines for the streaming mode param
resolveChannelStreamingSuppressDefaultToolProgressMessages gained an optional
mode override; the changed signature reflows the surface hash of every barrel
that re-exports it, so regenerate the affected baselines.
* chore(config): regenerate config baselines
* fix(agents): remove stale media completion reply mechanics
Make generated image, music, and video completion wakes follow the current tool-agnostic visible-reply contract, preserving every structured attachment without NO_REPLY or transport-specific instructions.
Refs #121933.
* test(agents): remove stale media helper
Delete the unused assertion helper left behind by the table-driven generated-media completion coverage.\n\nRefs #121933.
* test(tooling): wait for profiler descendant pid
Require the PID file to contain a valid positive process ID before asserting profiler descendant cleanup, closing the create-before-write CI race.\n\nRefs #121971.
* fix: exec timeout field does not state its unit, so callers pass milliseconds
`exec.timeout` is in seconds, but its sibling `yieldMs` is in milliseconds and
the `process` tool's identically named `timeout` is also in milliseconds. Those
two tools are used together in one workflow: exec backgrounds a long command,
then process polls it.
The unit exists only in the field description, and code mode deliberately defers
descriptions - the model sees `timeout?: number` and nothing else. A caller that
guesses milliseconds passes 900000 intending 15 minutes and gets ~10 days, so
the command never times out.
Add `timeoutSeconds` as the canonical field, matching the convention already
used by the config default (`tools.exec.timeoutSeconds`) and the sibling
(`yieldMs`). Keep `timeout` as a deprecated alias; `timeoutSeconds` wins when
both are supplied. Project the new field onto `nodeExecSchema`, which
hand-picks its properties.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(ci): satisfy test-type and dependency checks
Two CI failures from the test approach, not the change itself.
- check-test-types: TypeBox's TOptional does not surface `description` on its
static type, so the schema assertions failed under tsgo. Read it through a
narrow accessor instead.
- check-dependencies: knip flagged resolveExecTimeoutSeconds as an unused
export because only the test imported it. Move it beside the schema it
resolves, where the exec runtime imports it, so it is production-reachable.
Verified with the same commands CI runs: `run-tsgo.mjs -p
test/tsconfig/tsconfig.core.test.json` reports no errors in these files, and
knip --production reports no unused exports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(ci): fit the exec description budget and satisfy oxlint
- `agent-tools.schema.test.ts` enforces a 550-char budget on the exec tool's
model-facing descriptions; the two new ones pushed it to 640. Shortened both.
The rename is what makes this possible: the unit now lives in the field name,
so the description no longer has to carry it. Extended that budget test to
pin `timeoutSeconds` alongside `timeout`.
- oxlint `no-base-to-string`: the test's description accessor typed the field as
`unknown` and stringified it. Typed as `string | undefined` instead.
Verified: budget test passes (137 tests green across both suites), and all six
behavior tests still fail against origin/main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: remove the unit-ambiguous exec timeout field instead of deprecating it
Owner decision on the review's "ambiguous model-visible alias" finding: drop
`timeout` entirely rather than keep it as a deprecated alias.
Keeping it visible defeated the purpose. Code mode renders property names and
types and defers descriptions, so a model would still be offered a bare
`timeout` sitting next to a millisecond-based `yieldMs` and could still pick it.
A deprecation marker in a description the model never sees changes nothing.
Backward compatibility is not required here: every tool call is constructed
fresh by the model from the current schema, and a repository search found no
internal caller passing `timeout` to the exec tool. `timeoutSeconds` is now the
only exec timeout field, on both the main and node-only surfaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: migrate the internal exec callers to timeoutSeconds
Removing `timeout` from the exec schema broke three real internal callers that
the earlier survey missed, plus two test files and the runtime's own reads.
`check-test-types` caught it; my local search had filtered `timeout:` matches to
lines mentioning "exec" and discarded these.
- src/auto-reply/reply/bash-command.ts
- src/auto-reply/reply/commands-diagnostics.ts
- src/auto-reply/reply/commands-export-trajectory.ts
all passed `timeout: timeoutSec` and now pass `timeoutSeconds: timeoutSec`.
- bash-tools.exec-run.ts reads the field through the same typeof narrowing the
third call site already used; `params` is loosely typed, so a raw read is
`unknown`.
- Test fixtures updated. A vitest `waitFor({ timeout })` option in
background-abort.test.ts is NOT an exec param and was left alone.
`run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json` now reports zero errors
in changed files; the 5 remaining `rejectSymlinks` errors are pre-existing on
main. 148 tests pass across the affected suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* docs: synchronize the exec config table with timeoutSeconds
Addresses the P2 finding. The tools.exec.timeoutSeconds row still described the
per-call override as `timeout` and `timeout: 0`, which no longer exist. The
only remaining bare `timeout` reference in this page is the deliberate
contrast with the process tool's millisecond field.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: migrate the QA lifecycle caller and the shared ExecToolArgs contract
Two findings from the re-review of 6e8c3560e7, both real.
[P1] test/e2e/qa-lab/runtime/openclaw-exec-process-lifecycle.e2e.test.ts:174
still passed `timeout: 0.05`. After the removal that field is accepted and
ignored, so the call silently used the default timeout and could not produce
the asserted 50 ms `overall-timeout` result. Migrated.
[P2] ExecToolArgs in bash-tools.exec-request-preparation.ts:33 still declared
`timeout?: number` while the runtime reads only `timeoutSeconds`, offering
request-preparation users an accepted-but-inert argument. Renamed.
Why the typecheck missed both: ExecToolArgs is
`Record<string, unknown> & {...}`, and that index signature defeats excess
property checking, so a stale member produces no error at any call site. The
e2e file also sits outside the core test tsconfig I had been running locally.
Left alone deliberately: `processTool.execute({ action: "poll", timeout })` is
milliseconds and correct, and `createBashTool` in sessions/tools/bash.ts is a
separate tool surface with its own timeout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(test): update the node exec tool property assertion to timeoutSeconds
src/gateway/tool-resolution.exclude.test.ts:413 asserts the exact property list
of the node-forced exec tool and still expected "timeout". Renamed to match
nodeExecSchema.
Found by checks-node-compact-large-5; my local core-test tsconfig does not cover
src/gateway, so it did not surface there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: accept the legacy exec timeout as an undocumented alias
Removing the field from the schema does not reject it at runtime: the exec
schema accepts unknown properties, so a call built against an older schema was
accepted and then silently ignored, taking the default deadline instead of the
one it asked for.
Route every read through resolveExecTimeoutSeconds, which prefers the canonical
timeoutSeconds and falls back to a numeric timeout. The alias stays out of
execSchema, nodeExecSchema and ExecToolArgs, so no new caller can adopt it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* docs: update the background-process guide to timeoutSeconds
The exec parameter table and the inheritance note still taught `timeout` and
`timeout: 0`, which kept the retired spelling publicly discoverable and
contradicted the exec guide.
`poll`'s own millisecond `timeout` is left alone; it is a different tool and a
different unit, which is the collision this rename removes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(exec): reject removed timeout field
* chore: regenerate plugin SDK API baseline
---------
Co-authored-by: Marvinthebored <262704729+Marvinthebored@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* feat(agents): rename spawn_task tool to suggest_task
Models across vendors read the name spawn_task as 'starts background
work' and refuse to call it when the user has not asked to start
anything (Claude Sonnet 5, verbatim: 'spawn_task (which actively starts
background work)... Since you didn't ask me to start work, I'm flagging
this as a follow-up instead'). The tool only records a suggestion card,
so the name defeated the feature.
Rename the model-facing tool to suggest_task everywhere (catalog,
descriptors, presets, display config, OpenClawKit tool-display, docs)
and migrate persisted operator state via doctor: config tool policies
and cron SQLite toolsAllow entries that reference spawn_task are
rewritten to suggest_task. No runtime alias remains; runtime exposes
only the canonical name.
Live clean-room matrix on a dev gateway (virgin session per cell):
suggested-task-card phrasing now yields exactly one card on
anthropic/claude-sonnet-5, claude-haiku-4-5, openai/gpt-5.6-sol,
gpt-5.6-luna, and google/gemini-3.1-pro; pre-rename the same bait
produced prose-only follow-ups.
* chore(doctor): keep legacy tool-name predicate module-local
isLegacyTaskSuggestionToolName has no external consumers; the exported
form tripped the knip unused-export gate (deadcode:exports).
* fix(doctor): keep tool-name migration out of plugin-owned config
The rename migration traversed the whole raw config, so any object
carrying a toolsAllow key was rewritten - including opaque
plugins.entries.*.config, which core must not mutate. Scope the apply
pass to the typed core roots (tools, agents, channels, gateway) and pin
plugin config preservation in the test. Plugin-owned tool lists migrate
via the owning plugin's doctor contract instead.
* fix(ci): heal main deadcode and stale Swift protocol mirror
Unrelated main breakage blocking this PR's merge gate, healed here per
landing policy:
- #121653 left dead exports in src/skills/workshop: make
removeSkillCollectionDirectory and SkillCollectionRestoreResult
module-local; register collection-review.ts under the documented knip
ignoreIssues test-only-export convention (production runs it via the
scheduled maintenance loop).
- #121673 added ApprovalResolveParams.reviewer without regenerating the
Swift mirror; regenerate GatewayModels.swift (additive only).
* fix(ci): satisfy generic approval-runtime request signature in resolver test
Third main-heal carried by this PR: #121673 (9935ca3b30) left the
scoped-request mock's concrete inferred type unassignable to the generic
GatewayNativeApprovalRuntime request signature, breaking check-test-types
on main. Cast the mock at the runtime literal; assertions keep the Mock.
* chore(ci): drop approval-test cast superseded by main's typed mock rewrite
* chore(ci): drop duplicate knip entry superseded by main's heal
Replace divergent per-skill curation with one daily isolated collection review that can consolidate, rewrite, create, and drop writable skills atomically.
Autonomous auto mode remains automatic. Adds bounded review, transactional recovery, durable cadence, shared locking, and reuse-first /learn behavior.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Keep raw commands, paths, and provider errors out of ordinary chat while preserving explicit raw diagnostics and structured admin history.
Default command progress is status-only; `/verbose full` and `commandText: "raw"` retain diagnostic detail.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(skills): repair used skills in turn
* fix(skills): bind repair to used skill receipt
* fix(skills): preserve explicit repair review
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Teach the semantic reviewer to improve skills the agent actually used, keep review input provider-bound, and preserve bounded deterministic retries.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Use a manifest-first inventory with independent coverage for manifest-only bundled capabilities.
Retire the undocumented thread-ownership plugin while Doctor removes stale references.
Document Talk voice and persist only provider-scoped voice selection.
Closes#121353
* fix(subagents): wake the parent when a follow-up finishes a yielded child
A sub-agent that calls sessions_yield on its own behalf parks its run and
correctly withholds the parent's announce. But a later follow-up to that same
child session registered a sibling registry row instead of continuing the paused
one, so the requester defaulted to the child's own main session and the original
parent — itself idle behind sessions_yield — was never woken. The paused row also
stayed an unsettled descendant, deferring the parent's settle batch forever with
nothing recorded explaining the silence.
Follow-up dispatch now adopts the paused row through the existing post-steer
replacement seam, inheriting the requester identity and carrying the settle-wake
credential forward with its frozen batch membership remapped to the new run id.
A follow-up that names its own requester keeps registering separately, since an
explicit requester is a delivery opt-in that adoption would silently drop.
Also stops frozen-result refill from targeting paused rows: a yield clears the
result on purpose, so refilling from the session would attribute a later turn's
text to the paused run.
Closes#120157
* fix(subagents): select the paused owner past a requester-bound sibling
Adoption looked up the newest run for the child session and adopted it only
when that row was itself paused. A requester-bound follow-up deliberately stays
a sibling, but it registers at a higher generation and becomes that newest row,
so any later default follow-up saw an unpaused newest row, declined adoption,
and registered yet another sibling. The original requester stayed parked behind
a paused row that can never announce -- the same silent stall this fix exists to
remove, reached through a valid mixed-delivery sequence.
The latest-run query now takes an optional predicate applied before the
generation comparison, so a caller that owns a specific row class selects the
newest row of that class. Adoption asks for the newest `sessions_yield` row
directly instead of inferring it from generation order.
Docs now state that continuation applies to default delivery, since a follow-up
carrying its own requester runs as a sibling by design.
* test(qa): prove post-yield follow-up delivery through the gateway boundary
The unit and gateway-method tests for paused-run adoption assert on registry
rows, which proves the bookkeeping but not that an operator ever sees the
result. This adds the boundary proof: a real gateway child, the QA mock channel,
and the mock provider driving a subagent that pauses itself and finishes only on
a later follow-up.
A fixture plugin owns both legs. Its `before_dispatch` hook spawns the child with
`completionDelivery: "current-requester"`, so the announce has the operator turn
as its audience. An HTTP route then dispatches the follow-up to that same paused
session using default delivery -- the path adoption is meant to catch. A
requester-bound follow-up would opt into its own audience and run as a sibling
instead, so the two legs must differ here.
The mock provider gains a child that yields on its own behalf. Both of its turns
match on the current prompt rather than the shared transcript, so the yielded
kickoff cannot make the follow-up turn yield a second time.
The scenario asserts both sides of the invariant: no outbound traffic while the
child is paused, and exactly one announce carrying the follow-up marker once it
ends.
Reverting the adoption call site fails this test in the way that matters: the
child still produces its marker and the run still ends with stopReason=stop, but
nothing reaches the requester and the wait times out. The result is computed and
then silently dropped -- which is the failure this repair exists to remove.
* fix(ci): match QA Lab fixture plugin entries as a group in knip
The all-exports pass listed one fixture entry by name, so every new QA Lab
fixture plugin lands as an unused file and turns check-dependencies red until
someone remembers this file. Nothing imports these entries by design: the
Gateway E2E loads them through plugin config paths.
* docs(subagents): scope yield continuation to plugin runtime follow-ups
Adoption is gated on plugin_subagent task tracking, which only
createGatewaySubagentRuntime().run sets, so api.runtime.subagent.run is the
sole route into it. Writing that as one example implied other follow-up paths
to a paused session continue the run too; they are not tracked as sub-agent
runs and announce nobody.
* fix(subagents): reject undurable paused-run adoption
Fail plugin follow-up admission closed when the paused-run ownership swap cannot be persisted, while retaining the existing restart-recovery return-false contract. Trim duplicate tests and keep boundary coverage for requester routing, wake-batch remapping, repeated yield, and persistence rollback.
Co-authored-by: zhou.huanfeng <woundfongv3@163.com>
* docs(subagents): clarify yielded-run steering
Co-authored-by: zhou.huanfeng <woundfongv3@163.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): make subagent hard-deny list non-overridable and deny message tool
The always-deny list for subagent sessions (gateway, cron, message, sessions_send,
conversations_*) could be overridden by ordinary allow/alsoAllow config entries,
letting a configured subagent profile re-enable direct user delivery outside the
announce chain. The hard-deny layer now applies unconditionally; message joins the
list so resumed/visible subagent sessions cannot send directly either (hidden
launches already disabled it at spawn time).
* chore: re-fire CI
* chore: re-fire CI against fixed main baseline
* test(agents): workspace authority reflects non-overridable subagent deny list
The delegating-worker rejection case relied on alsoAllow bypassing the
subagent hard-deny list; with the bypass closed the policy owner blocks
sessions_spawn and the worker stays confined, so the guard has nothing to
reject.
* feat(plugins): support the Agent Plugins bundle format
* docs(plugins): document the Agent Plugins bundle format
* test(agents): preserve agent bundle runtime discovery
* fix(plugins): isolate Agent Plugins data-dir failures and align MCP support reporting
* docs(plugins): list Agent Plugins in the canonical plugin-format guides
* fix(plugins): gate Agent Plugins detection on schema, pure inspection, root-relative cwd
* fix(plugins): record Agent Plugins data-dir ownership explicitly
* docs(plugins): cover Agent Plugins in the CLI install detection guide
* fix(plugins): carry Agent Plugins data-dir and transport contracts through external MCP projections
Self-learning reviewer edits existing skills via targeted patches: it quotes the exact live text (or appends a section) and the service composes the full body inside the receipt-pinned read that hash-binds the proposal — untouched content survives by construction, and patches auto-apply through the scanner-gated pipeline. Full-body rewrites and oversized-skill edits stay pending for the operator. The review prompt shifts to active capture within the existing evidence gates, and shallow same-sender turns accumulate per session (provider-identity scoped, zero-iteration and duplicate-run contracts honored, aborted provenance carried, bounded state) so quick corrections get reviewed with their own transcripts. Replaces closed#119856.
Deletes the deterministic regex capture path that templated raw chat text into skill proposals (junk like a proposal whose whole procedure was one slugified user message). All autonomous learning now flows through the isolated experience reviewer: it sees a bounded workspace skill list, prefers revising pending proposals or updating the governing skill over creating new ones, and treats durable user corrections as first-class evidence. Update proposals are reviewer-only (explicit opt-in) and never auto-apply, since the reviewer drafts them without the live skill body. Removes the producerless pending-suggestion session machinery. Regression test proves the junk path is gone; real-Telegram E2E verdict in the PR body.
Defer Telegram final-mode text until speech settles, then deliver one captioned voice note or a proven-safe text fallback.
Co-authored-by: Jerry-Xin <jerryxin0@gmail.com>
* feat(browser): relay CDP compat for Puppeteer clients
Answer Target.getBrowserContexts with the always-empty context list so
puppeteer.connect() (chrome-devtools-mcp) can drive the paired Chrome
through the extension relay without the remote-debugging prompt. Serve
DevTools-style /json/list descriptors (id + type), which also makes the
HTTP tab-list fallback see extension-profile tabs, and add
`openclaw browser extension cdp` to print the relay endpoint and auth
header for external CDP clients.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browser): avoid map-spread in relay target descriptors
oxc(no-map-spread): build the /json/list descriptor object explicitly
instead of spreading RelayTabInfo inside map().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>