* refactor(config): retire dead streaming.progress.render key
The key had zero runtime consumers after #122552. Core doctor now strips it via stripRetiredTuningKnobs, and production LOC is net -13.
* fix(tooling): pin plugin SDK surface counts to the reduced export set
The retired progress-draft render reader counted twice via channel-outbound and channel-message's wildcard re-export.
* fix(skills): keep ClawHub publisher identity from search through install
ClawHub search returns one entry per publisher, so several results can share a
slug. Every client collapsed the selection to that bare slug before calling
skills.detail and skills.install, and ClawHub answered 409 AMBIGUOUS_SKILL_SLUG
with no in-product way forward.
searchClawHubSkills now records the publisher-qualified reference once, on the
result that carries it, and the Gateway protocol documents it. skills.detail
parses the same reference grammar skills.install already accepted, so review and
install cannot resolve to different publishers. Control UI carries that one
reference through row actions, detail, busy state, and acknowledgement retries,
and shows it so otherwise identical rows are distinguishable.
Fixes#117633
* fix(apps): send the ClawHub publisher reference from native skill browsers
macOS, iOS, and Android read the qualified reference from search results and use
it for skills.detail, install, busy state, installed matching, and list identity,
so two publishers sharing a slug stay distinct instead of collapsing into one
ambiguous request.
* fix(skills): refuse external-source skill detail instead of reading a same-slug skill
ClawHub has no source-qualified read endpoint, so a skills-sh reference parsed
down to its bare slug would have returned a registry skill's card while install
resolved the external artifact. Review and install could name different skills.
skills.detail now fails closed on any reference that carries a source, and the
macOS and AgentPro rows show the publisher reference next to the summary instead
of only when a summary is missing, so same-slug rows stay distinguishable.
* chore(apps): refresh native i18n source baseline for the skill row references
* refactor(skills): drop the unread search-result ownerHandle field
installRef is the one reference clients send back, and no client reads the
publisher handle separately, so the protocol and Control UI carry one field
instead of two.
* fix(skills): name the next step when external skill detail is refused
Clients that gate install behind a successful review would otherwise see only a
refusal, so the error names the direct install path and the CLI equivalent.
* fix(macos): use a doc comment on the ClawHub row subtitle
swift-format's docComments rule requires doc comments on declarations; the
subtitle property carried a regular comment and failed macos-swift.
* fix(skills): carry ClawHub trust state to clients that can install
Forwarding installRef let clients install the exact publisher the operator
picked, including external skills-sh sources. It did not forward the trust
state that says ClawHub never scanned that source, so iOS AgentPro — the one
surface that installs in a single tap with no review step — could install an
unscanned artifact with nothing on screen saying so. The CLI already labels
these (docs/clawhub/cli.md, docs/cli/skills.md); native clients could not,
because trustState was never on the wire.
trustState becomes an optional field on SkillsSearchResultSchema. It is purely
additive: older clients ignore an unknown key and the field is absent for
registry results, so downgraded readers are unaffected and no protocol version
moves.
Every client that renders a search row now shows "Not scanned by ClawHub",
matching the CLI wording exactly: iOS AgentPro in the row above the install
button, macOS and Android beside the review action, and Control UI on the row
that explains why review is refused for these sources.
Covered by a wire assertion that the state reaches clients for an external
source and stays absent for registry rows, plus decode-and-label tests on the
shared Swift kit and the Android parser, and a Control UI render assertion.
* fix(ui): size the ClawHub detail dialog to a refusal message
Refusing detail for an external source made an error-only dialog reachable.
The shared preview panel reserves a tall reader height for skill documents, so
a two-line refusal rendered in a mostly empty dialog and read as broken rather
than deliberate. Found by inspecting the review captures.
* revert(ui,apps): drop the ClawHub trust label layer
Maintainer product decision: skills.sh runs its own scanners, so OpenClaw does
not add a second alert layer in the apps. Removes the label from Control UI,
iOS, macOS and Android, and drops the trustState wire field that nothing would
render. The CLI keeps its existing label; changing that is a separate call.
Publisher identity, the fail-closed detail refusal, and the message-only dialog
are unchanged. Splits the oversized skills view test file to satisfy max-lines
without a suppression.
* test(ui): fix ClawHub skill fixture checks
* chore(plugin-sdk): refresh API baseline
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix(gateway): cascade session stop to subagents
Cascade exact and session-wide aborts through the subagent registry and keep Control UI Stop visible while descendants remain active.
* refactor(gateway): absorb stop cascade orchestration
* chore(gateway): refresh plugin SDK API contract for abort refactor
Regenerates the export-closure contract after the stop-cascade refactor
moved gateway abort orchestration exports, and drops a redundant Boolean()
wrapper flagged by lint. Public SDK surface gate is unchanged.
* chore(gateway): regenerate plugin SDK API baseline
* refactor(plugins): retire deactivate hook alias
* refactor(plugin-sdk): prune retired facade exports
* test(logging): isolate logger test controls
* refactor(logging): internalize file transport controls
* test(plugin-sdk): preserve retired facade coverage
* test(auto-reply): remove stale diagnostic imports
* refactor(logging): delete dead config-read guard
shouldSkipMutatingLoggingConfigRead had no production caller even on main;
it survived the dead-export scan only via logger's testApi re-export. The
test-isolation commit removed that mask, exposing the fossil. Delete the
guard, its test-only re-export, its mock entry, and its dedicated test file.
* refactor(plugin-sdk): retire due compatibility subpaths
* test(plugin-sdk): type group policy predicates
* refactor(plugin-sdk): split removed subpath records
* refactor(secrets): remove retired collector barrel
* test(plugin-sdk): tighten wildcard surface pin
* refactor(plugin-sdk): retire matrix facade metadata
* style(plugin-sdk): format facade metadata
* fix(ci): load channel setup contracts from source
Repair the main-owned regression from 99d662473c (Peter Steinberger): the new env-contract test could consume stale ignored dist metadata instead of the checked-in plugin declaration.
* test(plugin-sdk): refresh API baseline after rebase
* 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(channels): validate headless channel setup
* docs(channels): document headless provisioning
* fix(channels): repair setup metadata typing
* chore(channels): regenerate official channel catalog for env metadata
* fix(slack): keep mode-conditional env contract plugin-owned
Static --use-env declaration keeps only the unconditional SLACK_BOT_TOKEN;
socket-vs-HTTP conditional requirements (app token, signing secret) stay in
Slack's own setup validation so HTTP mode no longer demands an irrelevant
SLACK_APP_TOKEN.
* chore(sdk): regenerate api baselines and catalog after rebase
* fix(slack): align manifest env declaration with runtime contract
* chore(sdk): regenerate api baselines after rebase
* chore(sdk): regenerate api baselines after rebase
* chore(sdk): regenerate api baselines after rebase
* refactor(voice): promote realtime voice policy to talk owner
Move the exact-speech protocol (typed consult-call outcome instead of
string scraping), agent-proxy session instructions, and consult/wake-name/
barge-in policy defaults from the Discord plugin into src/talk, exported
additively via openclaw/plugin-sdk/realtime-voice. Wake-name enablement now
keys on a supportsActivationNameGating provider capability instead of a
hardcoded provider id. Follow-up to the voice split (#122479).
* fix(talk): bind exact-speech bypass to retained session state
Per ClawSweeper review on #122668: the protocol marker is untrusted model
tool-call text; only a parsed answer matching a currently retained
exact-speech text may select the privileged replay path. Unretained
marker calls route to a normal consult.
* feat(gateway): recover channels and health promptly after host sleep
A dependency-free thaw detector rides the existing 30s maintenance tick:
when the process resumes after being frozen >=45s beyond cadence (laptop
sleep, VM pause, SIGSTOP), the gateway restarts running channel accounts
(dead sockets otherwise take up to ~35 minutes to notice), refreshes
health/presence, and resets the event-loop histogram so the freeze does
not read as degradation. Admission is rechecked before every recovery
side effect; a suspension beginning mid-recovery re-pends the thaw, and
timed-out channel stops complete their two-call restart in one pass.
The macOS app cooperates: NSWorkspace sleep/wake observers in
GatewayConnectivityCoordinator best-effort prepare a local gateway
suspension before sleep and resume it on wake, never blocking sleep.
The lease is bound to the route that prepared it and always cleared on
wake; route or mode changes across sleep drop it to self-expiry.
Live proof: SIGSTOP 85s on an isolated dev gateway -> 'host thaw
detected: process was frozen ~57683ms', channels restarted, health ok,
eventLoop degraded=false after thaw.
* fix(macos): resume a sleep lease whose prepare response arrives after wake
A prepare completing after didWake previously discarded the lease id,
fencing the gateway until the two-minute expiry after micro-sleeps; the
late response now resumes immediately. Document the conservative
route-token drift tradeoff.
* fix(macos): retry wake resume after refreshing the dead post-sleep transport
After real sleep the WebSocket is usually dead exactly when resume runs;
refresh the endpoint first, then attempt resume up to three times with
bounded delays, clearing the lease only on success or exhaustion. A new
sleep cycle aborts in-flight retries.
* fix(gateway): bound plugin stopAccount so channel stops cannot wedge recovery
stopChannel awaited plugin stopAccount unbounded; a never-settling stop
hung the thaw restart (and health-monitor sweeps) and held the
single-flight recovery guard forever. Race it against the existing
5s stop timeout; the timed-out path flows into the established
recoveryStopTimedOut two-call restart contract. Regression wedges
pre-fix.
* refactor(gateway): move thaw channel restart off ChannelManager and fence mid-pass
restartRunningChannelAccounts is a standalone helper over the public
manager surface with a shouldContinue probe checked before every stop
and start, so a suspension committing while an account stop is awaited
leaves later accounts untouched. Regression covers the mid-pass close.
* fix(gateway): sanitize late writes from an abandoned stopAccount
An abandoned (timed-out) stopAccount can settle after its replacement
started; route its late setStatus writes through the existing
stale-task sanitizer so they cannot repaint or tear down the
replacement. Regression fails pre-fix.
* feat(gateway): add gateway-host desktop source behind desktop.host lab
Introduce the host as a first-class desktop source so operators can view
the machine OpenClaw runs on, not just cloud-worker environments:
- protocol: desktop.observe / desktop.launch with a discriminated
DesktopSource union (host | environment) plus an additive auth hint;
EnvironmentSummary gains a top-level desktop flag
- config: desktop.host { enabled, port?, passwordFile? }, Labs-gated
- rfb-probe: pure RFB version/security-type parser used to detect an
already-running loopback VNC server and classify its auth
- host-source: attaches to 127.0.0.1:<port>, refuses unauthenticated
(None) and unsupported (VeNCrypt) servers, and refuses ARD with the
supported alternative until the macOS milestone
- host-guidance: per-OS enablement text so no path dead-ends
- doctor + status report host desktop availability and auth type only
worker.desktop.observe/launch stay as delegating aliases with identical
behavior. Also drops the now-unused WorkerDesktopTunnels type export.
Live-verified against macOS Screen Sharing: probe reads RFB 003.889,
returns security types [30,33,36,35], classifies ard-account.
* test(gateway): probe RFB handshakes through the socket boundary
The probe's banner and security-offer parsers were exported solely so
unit tests could call them, which the dead-export gate rejects and which
tests internals rather than behavior. Keep them module-local and drive
the probe through a scripted loopback server instead.
The boundary tests also cover what pure-function vectors could not:
handshakes split across packets, legacy RFB 3.3 single-word security,
server-rejected handshakes, early hangups, and connect timeouts.
* feat(ui): let the Desktop panel view this machine, not just cloud workers
The Desktop panel was gated on a cloud-worker session placement, so an
operator running OpenClaw locally had no way to see the machine hosting
their main session even with a VNC server running on it.
Availability now follows the advertised desktop.observe method plus
operator.admin instead of session placement, and the picker lists every
environment whose summary reports a desktop, with the gateway row shown
as "This machine". Sources are passed to the generic desktop.observe /
desktop.launch RPCs; the app launcher stays worker-only. When a host
attach needs a password the gateway did not supply, the panel prompts and
keeps the value in memory for that connection only.
Adds the hostDesktop Labs toggle for desktop.host.enabled.
* fix(scripts): keep the env-var ratchet usable in shallow checkouts
The env-var budget check resolved its base ref, then hard-failed when
`git merge-base` found no shared ancestor. Shallow clones and grafted
agent checkouts resolve origin/main but truncate the history behind it,
so an advisory growth ratchet took down the whole check:changed gate
with "Could not resolve env-var count merge base for: origin/main".
Only the growth comparison needs a baseline, and the script already has
a no-baseline path. Treat git's exit 1 with empty output (no shared
ancestor) as that case and say so on stderr; a genuine failure still
exits 128 and still throws, and the absolute count-vs-budget check runs
either way.
* test(ui): measure the inline-code chip against its line box
The inline-code spacing test compared the chip's height to a prose text
rect, so it silently measured the monospace font's default line spacing.
That is ~17px on macOS and several px shorter on Linux, so the assertion
passed on CI and failed locally at 4.5 against a 3.75 bound -- after the
bound had already been widened once to chase browser font metrics.
Compare the chip to the paragraph's CSS line box instead, which is what
"the chip must not disrupt the line" actually means and is platform
independent. The horizontal gap stays as-is: it is em-derived padding
plus border, and it is the assertion that catches detached punctuation.
Verified both directions on macOS: the file is fully green, and
restoring the pre-fix 0.15em/0.35em padding still fails the gap
assertion at 5.41.
* feat(gateway): view macOS Screen Sharing from the Desktop panel
Modern macOS only offers ARD account authentication for Screen Sharing,
so the host desktop source refused every Mac. The Gateway now performs
the ARD handshake itself against the loopback server and hands the
browser a plain RFB 003.008 no-auth handshake, so the operator's macOS
account password authenticates the desktop without ever reaching the
browser, the observe result, a URL, or a log.
- rfb-preauth: ARD (type 30) Diffie-Hellman with MD5-derived AES-128-ECB
credentials, and VncAuth (type 2) bit-reversed DES, both under a single
10s negotiation deadline; Apple's RFB 003.889 maps to 3.8
- observe-bridge: runs pre-auth before splicing and starts the view-only
filter at clientInit, since the browser handshake is consumed here;
worker tokens keep the original version start phase
- host-source: attaches ARD, requiring per-observation credentials that
live only in the one-shot observer token and are dropped after use
- doctor: offers an explicitly confirmed sudo launchctl repair when
Screen Sharing is off, and prints the System Settings path otherwise
Live-verified against this Mac's Screen Sharing: the DH exchange and
credential framing are accepted and the server returns SecurityResult.
The VncAuth DES vector is confirmed against OpenSSL independently.
* fix(agents): resolve session-stable reply mode for direct resolver turns
Heartbeat wakes and other direct getReplyFromConfig callers bypass
dispatch, so no session-stable delivery mode reached their CLI binding
facts and prepare computed no messageToolPolicyHash, while dispatched
chat turns hashed the stable mode. resolveCliSessionReuse treats that
one-sided hash as a policy change, hard-invalidating the binding on
every chat<->heartbeat transition and leaving heartbeats to run in
fresh CLI sessions with no conversation history (historyPrompt=none).
Extract the synthetic-turn stable-mode resolution that
prepareAgentCommandExecution already used into
resolveSessionStableReplyMode, and apply it in prepareReplyRunContext
whenever a synthetic turn arrives without dispatch's injected mode, so
every turn kind on a session derives the same binding facts and hash.
Fixes#121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): derive synthetic binding facts from the dispatch policy owner
Consolidate the synthetic-turn stable-mode resolution onto dispatch's own
policy pieces instead of a simplified parallel resolver:
- resolveVisibleRepliesPolicy is extracted from dispatch prepare-context
(configured visible-replies guard + harness default chain) and shared, so
the fallback and dispatch cannot drift on harness defaults, live-vs-entry
chat facts, or ExplicitDeliverRoute.
- The stable fact now applies dispatch's messageToolAvailable downgrade via
the canonical tool-policy resolvers: tool-only delivery with a policy-denied
message tool records automatic, matching dispatch.
- Synthetic turns no longer fall back to their effective turn mode: a
response-tool heartbeat's message_tool_only is per-turn enforcement, not
session policy, and previously bypassed the stable fallback entirely.
Regression coverage: response-tool heartbeat facts match dispatched turns,
and a message-tool-denied config downgrades the synthetic stable fact.
Refs #121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): keep the session-stable reply mode sender-independent
Dispatch's stable-mode downgrade used the turn's sender-aware message-tool
availability, so a sender-scoped message denial hashed the stable policy as
automatic on chat turns while sender-less synthetic turns hashed tool-only —
the same binding reset loop on another axis.
resolveStableMessageToolAvailability becomes the one sender-independent
owner: dispatch passes it as sessionStableMessageToolAvailable for the
stable-mode resolution only (effective per-turn enforcement keeps the
sender-aware verdict), and synthetic binding facts already consume it.
Computed only when the visible-replies candidate is message_tool.
Refs #121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): derive stable reply facts from session surfaces, not wake plumbing
Third-pass review fixes on the #121485 consolidation:
- Strip system-event wake providers ("heartbeat", "cron-event") from the
stable context and resolve origin-less entries as internal, so synthetic
facts take the same internal-channel branch dispatch's live webchat turns
do instead of falling through to harness defaults.
- Fall back to persisted session facts (delivery channel/origin, groupId,
groupChannel/subject, accountId) for the sender-independent availability
stack, so bare-ctx callers like command prepare resolve the same
group/account-scoped policies as dispatched turns; dispatch now passes its
session entry too.
- Collapse the redundant second mode resolution into the availability
downgrade, surface dispatch's injected stable mode separately from
resolvePromptSourceReplyMode so the synthetic slot has one owner, and
reuse resolveTurnModelOverride instead of an inline copy.
- Refresh docs/.generated/plugin-sdk-api-baseline.jsonl: closure hashes only
(17 exports, zero declaration changes, verified) — plugin-sdk reply/meeting
entrypoints transitively bundle the reply pipeline this branch refactors.
Regression: origin-less entry heartbeat resolves internal-automatic facts.
Refs #121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: adapt to main module moves and split API baseline
Mechanical rebase refresh onto d3696f8d76: subagent-capabilities moved to
src/agents/subagents/spawn/, and the plugin-sdk API baseline is now
per-entrypoint content hashes (#122082) — regenerated for the reply-pipeline
closure this branch touches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(ui): commit-scoped session diff viewer with file, sync, and view menus
The Control UI session diff panel becomes a dense Amp-style viewer: per-file
menus (copy path, open file, reveal in file tree, open in editor), a Sync
Locally popover with a copyable git fetch command, view options (collapse all,
wrapping, split/unified layout), and a sticky footer that reports how far the
branch is ahead of its merge base and switches between all changes,
uncommitted work, and individual commits.
sessions.diff gains an additive scope param (all | uncommitted | commit) plus
commits, aheadCount, and mergeBase metadata. Commit-scope diffs read only the
object database, skip untracked collection, and bypass session-start baseline
filtering; unknown commits surface a typed unavailableReason. Offscreen file
bodies use content-visibility so large diffs stay responsive.
* fix(ui): satisfy session diff CI gates
* fix(gateway): fence commit-scoped session diffs to the advertised branch history
ClawSweeper found that commit-scoped sessions.diff accepted any commit resolvable in the checkout. Fence operator.read commit reads to the advertised merge-base..HEAD history and cover sibling-branch and base-history commits.
* feat(gateway): projects.list groups known checkouts by repo identity
Implements docs/plan/runners.md milestone 4 derived projects read model.
* feat(ui): regroup the Where picker by gateway, devices, and cloud
* feat(ui): placement chip shows where a session runs with reclaim
Implements docs/plan/runners.md milestone 4 placement display and reclaim.
* refactor(agents): delete dead model-selection surface, consolidate compaction target assembly
Removes production surface with zero live callers: buildConfiguredAllowlistKeys
(orphaned since fallback allowlist filtering was split out), the
retryTransientProviderRuntimeMiss resolveModelAsync option (its gateway startup
prewarming caller was replaced by prepared runtime snapshots), the ignored
useAsyncModelResolution plumbing (kept only as a deprecated no-op field on the
plugin-SDK-shipped prepareSimpleCompletionModelForAgent), and dead facade
re-exports (inferUniqueProviderFromCatalog, ThinkLevel, ModelRefStatus).
Consolidates resolveEmbeddedCompactionTarget's five hand-built result sites into
one assembleTarget helper owning the auth-profile-drop-on-provider-change rule;
resolution precedence is unchanged and now pinned by new table-driven cases
(unique-provider inference, ambiguous literal, profile-suffix preservation).
Net -93 production LOC, -106 test LOC.
* chore(sdk): regenerate plugin SDK API baseline after facade export removals
* fix(agents): scope quota failures to auth profiles
* test: repair provider suspension CI coverage
* test: keep suspension reset fixture internal
* fix(agents): spend cooldown probe only on transient candidates
Consume the one-run cooldown probe only when the candidate’s own unavailable reason is transient, so a billing-disabled pin cannot block a recoverable backup.\n\nFinding from the ClawSweeper review on openclaw/openclaw#121278.
* refactor(sessions): deprecate QuotaSuspension.laneId instead of removing
The shipped plugin-SDK surface deprecation policy requires keeping the inert field until the next surface window.
* fix(agents): extend transient probe policy to plugin-harness auth path
* fix(agents): keep provider overload from cooling auth profiles
* fix(agents): exhaust rotation candidates without cooldown records
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* test(agents): align auth rotation mocks with current main
* docs: regenerate plugin SDK API baseline
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* style(agents): format session-suspension test after rename resolution
---------
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Use the selected harness for isolated title generation, including native Codex auth.
Retry failed dashboard titles from the first user message without overriding manual names.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Exact-reconciliation sends skipped pre-send promotion, so dispatch cleared the producer claim without publishing the platform send attempt identity. The post-send ack then failed its ownership guard with "Delivery platform claim was lost", settling provably delivered sends as unknown; since #121833 that ambiguity surfaces as a spurious "couldn't confirm my previous reply" recovery notice. Dispatch now atomically promotes the producer claim into the active attempt and publishes the attempt identity, so successful sends settle and lease renewals keep working. Adds a boundary regression pinning unknown_after_send evidence across a later unclaimed batch dispatch.
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* refactor: consolidate coercion ownership
Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.
The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.
* fix: guard integer option ownership
Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.
* fix: keep integer helpers on numeric facade
Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.
* fix: point numeric coercion to number runtime
Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
* refactor(qqbot): remove bundled extension source
Mechanical deletion half of the #107295 squashed rebase; the catalog
repoint and host integration land in the follow-up commit.
Co-authored-by: sliverp <870080352@qq.com>
* refactor(qqbot): install plugin from Tencent package
Squashed rebase of #107295 onto current main. Repoints the official
external channel catalog at @tencent-connect/openclaw-qqbot@2.0.1 and
adapts onboarding, doctor migrations, secrets, build guards, and tests.
Documents the known limitation that the external package does not
support structured SecretRef clientSecret values; operators move those
to QQBOT_CLIENT_SECRET or clientSecretFile before upgrading.
Co-authored-by: sliverp <870080352@qq.com>
* fix(doctor): reuse shared hasOwnKey record helper
The rebased QQBot migration carried its own hasOwnKey export, colliding
with the one main now ships in legacy-config-record-shared.ts.
Co-authored-by: sliverp <870080352@qq.com>
* fix(plugins): carry catalog integrity through the update bridge
The externalized-bundled-plugin bridge dropped the official catalog's
expectedIntegrity pin, so bundled-user updates installed the external
npm package without integrity verification. The bridge now carries the
pin for the catalog's exact npm spec and both bridge install calls pass
it through; update-channel spec overrides intentionally skip the pin
since it only covers the pinned version.
Co-authored-by: sliverp <870080352@qq.com>
* chore(plugin-sdk): refresh per-entrypoint API baselines
The QQBot compat export and bundled-type removal shift 26 entrypoint
closure hashes in the new split baseline layout.
Co-authored-by: sliverp <870080352@qq.com>
* refactor(qqbot): drop helper reintroduced during rebase
Main's coercion consolidation added this file after the deletion
commit's base; its only consumers were the removed qqbot sources.
Co-authored-by: sliverp <870080352@qq.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): stop advertising trigger-gated fields when cron triggers are disabled
When cron.triggers.enabled is off (the documented default posture), the
cron tool still advertised job.trigger/patch.trigger, schedule.kind
"stream" with its stream-only fields, and payload.kind "script" — all of
which the scheduler unconditionally rejects. Schema-eager models attach
junk triggers to plain reminder requests, get rejected, and retry-loop
without ever creating the job.
Gate those surfaces on the resolved config so they are only advertised
when the runtime can actually accept them, and swap the trigger guidance
for a short unavailable notice so the model tells the user instead of
polling or silently degrading the request. Config-less construction
paths keep the full surface.
Related: #119455
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cron): restore config type import and regenerate schema contracts
Repairs rebase fallout and CI-contract consequences of the trigger gating:
- Re-import OpenClawConfig from config/types.openclaw.js; the rebase crossed
#121768 which removed the re-export from config/config.js, stranding
resolveCronTriggersEnabled's signature.
- Guard the destructured tool in the MCP gating test for
noUncheckedIndexedAccess.
- Collapse the hookContext merge object to one line: the added config
pass-through tipped openclaw-tools.ts over the max-lines ratchet (701/700).
- Regenerate prompt snapshots (cron description narrowed under
triggers-disabled harness config; ~3k chars per prompt) and the plugin-sdk
API baseline (CronToolOptions gained the optional config member, visible
via codex-mcp-projection).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(cron): align schema contracts with current main
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
Per-requester OAuth for HTTP MCP servers: in shared channels each trusted sender connects their own account. New config: mcp.servers.<name>.oauth.identity ("shared" default, byte-identical behavior) and gateway.publicOrigin (HTTPS except loopback) for the new GET /oauth/mcp/callback served on the Gateway HTTP server. Requester tokens are isolated per (channel, account, sender) in mcp_oauth_stores rows (no schema bump); callbacks correlate through a durable state-keyed one-time index with a 10-minute TTL; per-requester servers are fail-closed out of static/scheduled runtimes; unauthenticated senders get a sign-in link with a portable URL button; MCP config mutations clear requester credentials at the canonical owner. mcp status --json keeps the legacy authStatus fields additively. Proven by a two-user self-hosted-Executor E2E through the real gateway callback. Part of #122034.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>