Completes docs/plan/runners.md milestone 1c by moving node-role pairing behavior and state access under device-pairing-owned modules while preserving shipped migrations.
* fix(system-agent): keep inference available across routes
Accept provider-owned equivalent response model identities and route every new OpenClaw chat through the configured/authenticated inference fallback ladder. Malformed replies may fall through while provider/model and execution-owner uncertainty remain fail-closed.
Fixes#120711
* fix(system-agent): keep malformed fallback route-scoped
Continue to later configured routes from the same provider after empty or malformed model output. Timeout and unavailable results remain provider-wide, while owner and identity uncertainty remain fail-closed.
Addresses ClawSweeper P1 on #120712.
* fix(gateway): target requestHeartbeat to the hook's agentId
Two requestHeartbeat calls in the hook handler (reportHookFailure and
the announce path) did not pass value.agentId or sessionKey, so a hook
targeting one agent woke heartbeats on every configured agent. Pass
the hook's agentId and resolved sessionKey so the wake is scoped to
the agent the hook actually names. (#119808)
* fix(gateway): align fallback hook wake target
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* fix(gateway): preserve global hook wake routing
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
---------
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The rewind-only confirmation popover kept "delete" naming from the removed
local-only hide feature (#120681). Rename identifiers and DOM/CSS classes to
confirmed-action, drop the dead right-side placement variant, collapse the
single-caller renderConfirmedActionButton indirection into renderRewindButton,
and localize the hardcoded Cancel / Don't ask again strings.
The openclaw:skip-rewind-confirm localStorage preference key is intentionally
unchanged so users' "Don't ask again" choice survives.
* fix(codex): pin catalog CODEX_HOME onto terminal resume spawns
The Codex session catalog lists threads through the plugin's supervision
app-server connection, which can run on the agent-scoped Codex home
(homeScope: "agent"). The terminal resume plan spawned a bare
'codex resume <threadId>' under the operator login shell, so the CLI
resolved its default ~/.codex and failed with "No saved session found".
Terminal plans now carry the catalog connection's CODEX_HOME (local and
paired-node paths), the gateway merges plan env into the PTY spawn, and
the triplicated home-scope resolution collapses into one shared
resolveCodexAppServerLocalHomeDir.
* feat(ui): add main-content terminal placement
The terminal panel gains a third dock placement, "main", filling the
content region next to the sidebar like the transcript pane. The panel
header gets a bottom/right/main placement switcher (persisted in the
existing openclaw.terminal.panel.v1 store), and opening a Codex/Claude
session from the session catalog selects main mode so resumed sessions
appear as a full window instead of a bottom strip.
Claim-owned cancellation now stops pre-adoption media hydration, allowing later same-chat messages to proceed. Webhook handling now uses the canonical Telegram adoption stall timeout.
Fixes#120704.
* fix(exec): acknowledge notify-on-exit after process poll
* test(exec): mock completion event enqueue
* fix: retire stale exec heartbeat wakes
Prevent acknowledged process completions from turning their queued wake into an unrelated heartbeat. Preserve coalesced task work and keep stale wakes out of scheduler cadence and commitment follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: isolate stale exec heartbeat coverage
Keep existing heartbeat suites below the max-lines ratchet while retaining coverage for stale wake coalescing, scheduler bookkeeping, and commitment fan-out.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(heartbeat): preserve scheduled cadence for stale exec wakes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(heartbeat): keep stale wake coverage within lint limits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(heartbeat): retire stale exec wakes before busy gates
Preserve scheduled cadence and cron work when coalesced exec wakes are acknowledged, while retiring stale wakes before retryable busy checks.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(heartbeat): accept inferred wake sources
Allow stale-wake preflight to handle the optional source produced by reason inference without weakening the exec-event check.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cloud-workers): honor Crabbox SSH fallback ports
Select a currently reachable advertised SSH endpoint before returning a Crabbox worker lease, while preserving fail-closed teardown. Allow deletion of failed cloud sessions only after the environment record proves the provider lease is gone, and align the Cloud Workers docs with the shipped configuration and Control UI flow.
* fix(cloud-workers): retry advertised SSH endpoints
Persist provider-advertised SSH fallback ports and retry them through the shared pinned transport used by bootstrap, tunnels, reconnects, and workspace transfers. Keep candidate selection provider-neutral, preserve identity and host-key fencing, and add the same-version SQLite column through the existing additive migration path.
* fix(crabbox): recheck SSH readiness after setup
Treat provider setup as a mutating lifecycle phase: refresh authoritative inspect state and wait for SSH readiness again before returning the lease. This prevents bootstrap from racing setup-triggered SSH restarts on fresh and replayed workers while preserving security attestation and fail-closed teardown.
* fix(cloud-workers): retry bootstrap transfers across ports
Use the shared advertised-port candidate runner for bundle transfer, install, and best-effort upload cleanup as well as preflight. This keeps fresh bootstrap connections on pinned identity and host-key semantics when a previously selected SSH port disappears between phases.
Proof passed: 517 focused tests (1 skipped), final autoreview/TruffleHog clean, targeted lint/format. Full check-changed reached the unrelated max-lines baseline drift on current origin/main: this branch's merge base contains ui/src/pages/chat/components/chat-model-controls.ts while current origin/main removed that baseline entry; do not touch the unrelated baseline.
* fix(cloud-workers): persist fallback ports compatibly
Create the fallback-port companion table lazily as an additive surface, preserving downgrade tolerance for binaries that still expect the older shared-state schema.
* fix(cloud-workers): preserve bundle artifact narrowing
Bind the immutable discriminated artifact before the SSH retry callbacks so bundle-only fields remain narrowed across fresh candidate invocations. Use the shared temp-directory owner in the tunnel fallback test for deterministic cleanup.
* fix(cloud-workers): retire deleted placements
Terminal placement rows retire by exact CAS after session deletion, and managed AWS docs now pin the Gateway SSH CIDR.
* fix(crabbox): await pending AWS attestation
missing authoritative metadata is pending only while the lease is non-ready; ready leases still require explicit false.
* fix(cloud-workers): fence remote command retries
require explicit idempotent/never classification; remote worker launch never retries after ambiguous SSH loss.
* fix(cloud-workers): bound SSH fallback operations
Remove the localStorage-backed per-browser hide action ("Hide in this browser only") so the transcript mirrors agent context. The action was un-undoable because restore() had no UI path.
Stale openclaw:deleted:* localStorage keys become inert.
Question panels now use normal footer flow. Resize following respects explicit backscroll intent, and the latest-message affordance stays above footer surfaces.
resolveMediaMaxBytes treated any number as an explicit cap, so
channels.matrix.mediaMaxMb: 0 became a literal 0-byte limit and every
outbound media send failed before upload, while inbound media on the same
account kept working because monitor/index.ts already floors the same field
with Math.max(1, mediaMaxMb).
Only a positive value is a cap now, matching CommonMediaMaxMbSchema; other
values fall through to the existing unset path (per-kind defaults). matrix
is one of three bundled channels that hand-roll this field without a range
constraint, so the config loads without complaint today.
* docs(discord): drop retired ui.components.accentColor and fix heartbeat key name
* docs(discord): remove the retired accent-color example block
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cli): refresh running gateway auth state after models auth order set/clear (#114989)
The order set/clear commands wrote the agent auth-profile store from the
CLI process but never notified a running gateway, which keeps serving its
pre-mutation auth snapshot until restart. Sibling auth mutations
(login/logout/profile add) already call refreshRunningGatewayAuthState;
align order set/clear with that contract.
* style: format auth-order.test.ts with oxfmt
Validate token response envelopes once in the shared parser used by authorization-code exchange and refresh. Prove null and array responses through both production entry points over real loopback HTTP.
Refs: #120174
Co-authored-by: 毛宇豪0668001457 <mao.yuhao@xydigit.com>
* perf(plugins): declare doctor contract surfaces
* perf(doctor): slim migration import closures
* perf(plugins): narrow doctor declaration record surface and wire owner-test lane
Registry records carry only the doctorContract declaration instead of the whole
parsed manifest, and check:changed now selects the src/plugins-owned declaration
honesty and closure-guard tests for extension module/manifest changes so
cross-lane drift cannot pass PR classification.
* fix(doctor): keep control-plane dist imports require-safe
Keep doctor and channel control-plane chunks off exec-class dependencies, and enforce native require(esm) loading during postbuild.
* chore(plugin-sdk): regenerate API baseline
* chore(plugin-sdk): sync export ordering
* fix(plugins): satisfy doctor contract CI boundaries
* perf(doctor): make qqbot doctor closure dependency-light
qqbot was the last plugin above 5s in doctor state-migration enumeration
(~8s under tsx/jiti). The cost was not the state-key builder (already a
leaf): its doctor closure value-imported the runtime-doctor SDK barrel,
whose plugin-state-store/state-db re-exports pull kysely (~330 modules),
plus security-runtime for one fileExists (~200 modules), all resolved
per-module by jiti during enumeration.
Split the migration-define helpers and light re-exports into a new
private-local plugin-sdk/runtime-doctor-migrations subpath; runtime-doctor
re-exports it so its public surface is byte-identical (API baseline hash
unchanged). qqbot's doctor-contract and state-migrations now import only
the light subpath, swapping fileExists for the equivalent async
legacyStateFileExists already in the closure.
qqbot enumeration: ~8.0s/531 modules -> ~0.25s/18 modules.
* chore(plugin-sdk): drop private-local subpath from API baseline
runtime-doctor-migrations is private-local-only; the baseline tracks public
modules, and the earlier line was generated before the classification.
* fix(plugins): register runtime-doctor-migrations boundary paths
The private-local subpath list feeds the extension package boundary map;
the shared paths config and xai's derived overrides must carry the same
entry or the boundary contract test fails.
* fix(doctor): skip legacy main transcript check for SQLite-owned sessions
After the SQLite session import, the main session's legacy .jsonl is
archived and 'sessions cleanup' prunes it as unreferenced, but doctor
still warned 'Main session transcript missing' because the main-session
check required the legacy file. The sibling recent-session check already
skips SQLite-owned keys; apply the same ownership guard to the main
session so doctor and cleanup agree. (#119926)
* style(doctor): condense SQLite ownership comment
Co-authored-by: 李琪0668001400 <li.qi16@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
listSlackDirectoryPeersLive/listSlackDirectoryGroupsLive hand-rolled
do/while loops with no repeated-cursor detection and no page bound,
while every other users.list/conversations.list consumer goes through
collectSlackCursorPages. A Slack API or proxy edge that keeps
returning the same non-empty next_cursor made directory queries
paginate forever, growing the member/channel arrays without bound.
Both loops now go through collectSlackCursorPages, which throws on a
repeated cursor and caps total pages.
* fix(llm-core): settle result() when a stream ends without a terminal event
EventStream.end() with no argument and no prior terminal event left the final
result promise pending forever; any consumer awaiting result() dead-ended
silently for the whole run budget (the agent loop's fall-through in
streamAssistantResponse awaits exactly that promise). All in-repo pumps push
terminals or pass end(result), so this only fires on producer contract
violations — which previously hung and now reject loudly with a named error.
A pre-attached catch keeps iterate-only consumers free of unhandled
rejections. Also dedupe the agent loop's two identical final-message blocks
into one finalizeAssistantMessage helper (net-negative production delta).
Follow-up from #120426's turn-liveness investigation.
* fix(llm-core): void the intentional rejection pre-catch
ClawSweeper P1: the pre-attached no-op catch returns a Promise that would
float under typescript/no-floating-promises.
* fix(browser): bound batch action nesting depth in act request normalization
normalizeActRequest recursed over batch nesting with no depth bound, so a
~1MB POST /act body with tens of thousands of nested batch levels parsed
fine and then crashed normalization with RangeError: Maximum call stack
size exceeded before the ACT_MAX_BATCH_ACTIONS count check could run,
surfacing an internal stack overflow as the 400 validation message.
Thread the existing ACT_MAX_BATCH_DEPTH limit through normalizeBatchAction/
normalizeActRequest and reject deeper nesting up front with a clear
'batch nesting exceeds maximum depth of 5' error, matching the bound the
Playwright executor already enforces at dispatch time.
* fix(browser): match batch depth executor boundary
Accept the six wrapper levels supported by the Playwright executor and reject the seventh during request normalization.
Co-authored-by: 万拥 0668000723 <wan.yong@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>