Recheck the request signal after an async response hook settles so a concurrent abort cannot reach response body handling. Related to #125802 and #125834.
Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
Keep drain-owned recovery state authoritative when the pre-restart run reaches a normal terminal event, so startup continues the original request without requiring a manual retry.\n\nRelated: #57425
`openclaw approvals allowlist add|remove` printed "Writing local approvals."
from the shared target-resolution helper, before the mutation decision was
made. Both idempotent paths ("Already allowlisted.", "Pattern not found.")
returned without saving, so the CLI announced a write that never happened.
`approvals set` had the same problem: it announced the write and then rejected
unparseable input.
Move the announcement from `loadWritableSnapshotTarget` into the local branch
of `saveSnapshotTargeted`, the function that owns the write. Every caller of
the shared seam is fixed at once and exit codes are unchanged: idempotent add
and remove still leave the requested end state satisfied and exit 0.
* test(ui): seed mock activity sessions across owners
* feat(ui): consolidate activity filters into toolbar
Also restores the shared .input styling contract for standalone form controls.
* test(ui): update activity feed e2e for toolbar people popover
The Gateway owns start-or-steer at admission (6515f6a255) and no
client produces expectedRunId anymore (d84a910fc8). The field shipped
only in v2026.8.1-beta.2 - never a stable tag - so it is removed rather
than deprecated. Steer sends resolve the selected session's current
operation; the exact-match branch, the operation|run target identity
discriminator, run_mismatch rejection, and the suggestion producers'
active-run-id selection (with its ambiguity failure) are deleted.
Provider-native turn fencing (Codex expectedTurnId) is unchanged:
the backend-captured runId on the injection target remains.
Preserve browser availability and profile ownership across node and extension routes, close session-owned node tabs, honor screenshot refs, and hide unavailable tab-bound actions.
The retryable-send and retryable-history tests asserted the resend side
effect immediately after `vi.advanceTimersByTimeAsync(100)`. Sinon's async
tick yields exactly one real macrotask after firing the last timer, and the
retry chain (`void scheduleStoredChatOutboxDrain` -> `chat.history` ->
`chat.send`) consumes exactly that one boundary, so the assertions had zero
slack and flaked under CI contention.
Wait for the outcome with `waitForFast` instead, and fold the outbox-empty
assertion into the wait since retirement lands after the send call.
* fix(gateway): harden hook admission and shutdown
Flush structured 413 and 408 hook responses before closing oversized or timed-out request sockets, and reject deferred wake requests that target an explicit session.
Direct Gateway close now marks startup and readiness as draining immediately and attempts every HTTP listener shutdown before reporting an incomplete close.
* test(gateway): model request destruction in fuzzing
Give the HTTP request fuzz double the IncomingMessage destroy contract exercised after 413 and 408 responses finish.
* fix(gateway): preserve response and shutdown bounds
Route installed webhook body-limit failures through the shared response-first close owner and require complete Feishu 413/408 responses before terminal connection closure.
Retain the current-main grace bound for agent-harness disposal so one stuck harness cannot block listener teardown.
* test(telegram): model response-close request cleanup
Keep the slow-body request alive through response finish, then assert cleanup when the mock response transport closes.
Preserve completed tool work when native Codex compaction fails, close failed compaction progress, and bypass unrelated model/auth failover before isolated finalization. Fixes#125789.
* test(control-ui): add --operator-scopes flag to the mock dev server
* feat(control-ui): simplified settings experience for non-admin operators
Non-admin browsers previously saw every settings page, many of which
dead-ended or rendered enabled controls whose RPCs fail with
'missing scope: operator.admin'.
- config.schema drops from operator.admin to operator.read: the schema is a
static document describing options whose values are already readable via
read-scoped config.get; admin-only schema only broke read-only settings
rendering (Automation/Infrastructure/AI Agents/Communications showed
'Schema unavailable. Use Raw.').
- Settings sidebar and settings search hide admin-only routes (custodian,
labs, updates, automation, infrastructure, mcp, security, secrets,
cloud-workers, communications, ai-agents, model-setup) for non-admin
viewers; legacy gateways without advertised scopes keep the full UI.
- Channels, Devices, Worktrees, Memory Import, Profile gate their mutation
controls on actual scopes with 'Browsing only…' notices instead of
enabled-but-failing buttons; Devices no longer fires device.pair.list /
exec.approvals.get without the scopes to call them (kills the two red
error callouts on page load).
- Scope-upgrade banner: dismissing it in the guidance phase (no in-app
upgrade path) now hides it fully instead of leaving a permanent chip.
- Config write coordinator surfaces scope refusals as a visible
admin-required error instead of silently resolving false.
* test(control-ui): advertise config.schema in the mock dev gateway
ensureSchemaLoaded now checks method advertisement + scope before loading
the schema; the mock harness must advertise config.schema like a real
gateway does or schema-driven settings pages render empty in the mock.
* fix(control-ui): close the worktree create draft on scope downgrade
* perf(doctor): isolate memory health artifact
Doctor lint loaded the broad Memory Core API barrel only to register health checks and read isolated check IDs. That synchronously pulled the full memory public graph into the first lint run, consuming most of the 120-second test budget.
Load a dedicated doctor-health public artifact instead and verify it is packaged. The bisect boundary was 9de3ca5fc9 (#125571); because that commit only adds upgrade-test assets, it exposed a pre-existing runner-sensitive cost rather than introducing the expensive import path.
* test(control-ui): restore device lifecycle test boundary
* perf(control-ui): lazy-load settings sidebar
* fix(ui): recheck access after confirmations
* fix(control-ui): gate presence-driven device reloads on pairing access
The presence connectivity-change path still called device.pair.list without
operator.pairing, the same invariant the pair-event and poller paths already
guard; a limited browser got a doomed RPC on every connectivity change.
* fix(control-ui): fail open on schema loads for legacy scope-less gateways
canCallGatewayMethod hardened to strict advertisement+scope checks (#125478),
which made the new ensureSchemaLoaded gate silently skip config.schema for
legacy hellos without advertised scopes or a method list. Schema loads now
skip only on a definitive denial (method advertised absent, or advertised
scopes without operator.read), reusing the fail-open hasOperatorReadAccess
semantics the rest of the non-admin UI uses; regression test pins the
legacy snapshot path.
* test(control-ui): split schema-access coverage into its own file
runtime-config-capability.test.ts crossed the max-lines cap; the legacy
fail-open regression and its denial counterpart move to a colocated
schema-access test file.
* fix(scripts): keep mapped Vitest lanes at their measured no-output floor
The codex extension shard legitimately works in silence beyond 300s under
the default reporter (measured 61s import + 293s testing at ~95% CPU); the
CI-wide OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=300000 env override shrank the
lane below that and the watchdog killed healthy runs, flipping with
incidental flake output (#125825). Per-config entries in
VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS now act as measured silence floors: a
global env value may widen a mapped lane's window but no longer shrinks it;
unmapped configs and the explicit '0' disable keep env verbatim. Adds the
codex extension lane to the map at the extra-long tier (same class as the
discord entry from #123025).
The `## Delegation` guidance added in #125691 lived only in
buildAgentSystemPrompt, so Codex-runtime agents never received it: the
Codex harness builds its own developer instructions in
extensions/codex/src/app-server/thread-prompt.ts and imports nothing
from the system-prompt builders. Live A/B on gpt-5.6-luna had the native
runtime answer "spawn a visible session" while the Codex runtime
answered "spawn a hidden subagent".
Move the policy into src/agents/delegation-guidance.ts, owning both the
main-session mode resolver and the section text, and export it through
the agent-harness plugin SDK barrel that the Codex harness already uses.
The hidden-delegation vocabulary is injected by each runtime, so core
never names a plugin-owned tool: native passes `sessions_spawn`, Codex
passes native `spawn_agent`. Visible sessions stay `sessions_spawn`
with visible=true on both runtimes because Codex-native children are
never OpenClaw sessions.
Also narrows the Codex line that told the model to use `sessions_spawn`
only for OpenClaw/ACP delegation; it now scopes that to internal
legwork, so user-facing deliverables still route to a visible session.
* feat(gateway): proxy channel conversation avatars
* feat(discord): capture conversation avatars
* feat(slack): capture DM sender avatars
* test(discord): bind guild avatar mock
* feat(ui): render channel conversation avatars
* fix(ui): align sidebar owner fixtures
* fix(gateway): version channel-avatar routes by media revision
A stable per-session URL let AuthenticatedAvatarRouteLoader's blob and
sticky-404 caches pin a mounted row to a stale or blank avatar after the
backing media changed. Append an opaque digest of the media reference so
replacement and 404-recovery change the route identity.
* test(ui): align sidebar owner facet
* fix(ui): keep owner chip until channel avatar loads
A session with a channelAvatarUrl suppressed its owner chip even while the
blob was loading, auth was not ready, or the route 404ed, leaving an empty
lead slot. The chip now rides as fallback content inside the avatar element
and yields only to a usable image. Covers 404 and auth-pending states;
avatar rows keep renderedOwnerId unset so an owner-viewer stays visible in
the facepile.
* perf(ui): keep channel avatar fallback within budget
* perf(ui): lazy-load the channel avatar element
The avatar element and its authenticated blob loader rode the startup
bundle through session-leading-indicator, pushing startup JS 51 B over the
CI gzip budget. Channel avatars are not startup-critical: register the
element on the first avatar row; the owner-chip fallback covers the
one-time upgrade window. Startup JS returns ~1 KiB under the ceiling.
* build(ui): raise startup baseline for channel avatars
CI-measured startup JS is 344379 B against a 343289 B baseline (+1090 B).
The avatar element and blob loader are code-split out of startup (previous
commit); the residual is the sidebar lead-slot render branch and row
plumbing, which cannot be deferred. Baseline updated via
check-control-ui-performance --update-baseline with CI bytes per the
script's contract; well inside the 4096 B ratchet step and 358400 B
ceiling.
Move GitHub markdown label formatting back behind the markdown loading boundary and exercise markdown-table interactions through the real delegated click owner. This restores startup bundle margin without changing the committed budget baseline and prevents the isolated table test from depending on leaked listeners.
* fix: honor inherited model pins over channel defaults
Child sessions now keep parent model pins consistently across reply, status, harness prediction, and agentCommand.
* test: align agent command model selection fixtures
Keep concurrent override and threaded parent-selection coverage aligned with canonical model normalization and runtime snapshot access.