* test(qa): add managed-worktrees CLI lifecycle scenario coverage
Managed worktrees had zero QA scenario-pack coverage despite being a
headline feature. Mint agent-runtime.managed-worktrees-lifecycle in the
taxonomy, add a runtime scenario, and prove the real child CLI through
create with .worktreeinclude provisioning and the .openclaw setup hook,
dirty removal pinning a snapshot ref, restore rebuilding tracked,
untracked, and provisioned files with their modes, and gc preserving
manual worktrees.
* fix(qa): align model-switch catalog assertion with expectedAlternate flow
qa/scenarios/models/model-switch-follow-up.yaml switched to
expectedAlternate.model in 5a795f4dda but the catalog test still greps
for the retired alternate?.model literal; the test is outside the PR
change-classification lanes, so the break only surfaces on direct runs.
* test(qa): narrow managed-worktrees taxonomy description to proven manual-owner gc
ClawSweeper P2 on #120335: the scenario proves manual-owner gc retention
only; session and Workboard cleanup lifecycles are not exercised, so the
coverage description must not claim them.
Prevent retained plugin closures from recreating, adopting, or stopping successor runtimes across shutdown and restart.
Add deterministic lifecycle regressions for pending startup, exact-owner stop, retained tools, and generation restart.
* fix(discord): thread archive/delete closes sessions in each agent's store
closeDiscordThreadSessions resolved the sessions store with the Discord
account id as agentId ('default' on the default path), which points at a
nonexistent agent's store — archiving or deleting a thread silently closed
nothing. The store now resolves per routed agent via listAgentIds and every
agent's matching sessions are deleted.
* fix(discord): type thread session cleanup across agent stores
* fix(discord): scope thread-session scan per agent and keep it read-only
* test(discord): cover thread deletion across agent stores
* fix(agents): session quota suspension resolves the real owning agent
suspendSession derived the agent id from path.basename(agentDir), which is
always the literal 'agent' in the default <state>/agents/<id>/agent layout —
the quota-suspension marker landed in a nonexistent agent's store and the
suspension silently no-opped. Callers now pass the explicit agentId through
runWithModelFallback/throwFallbackFailureSummary, with the agent-dir registry
as fallback for dir-only callers.
* fix(agents): embedded failure suspensions carry the run's agent id
* fix(imessage): text echo matching honors message-id conflicts; own sends stop feeding the loop limiter
Two silent-drop paths in the iMessage monitor:
1. The persisted 12h echo cache matched inbound user text against old
outbound text even when the GUIDs conflicted — a user sending 'ok' within
12h of the agent sending 'ok' was dropped as an echo. The text branch now
applies the same hasConflictingMessageIds guard the media branch has;
genuine reconnect echoes still match by id or id-less entries.
2. Every from-me row fed the echo loop rate limiter, so a normal outbound
burst (agent replies, multi-chunk sends, operator phone traffic) could trip
the limiter and silently suppress the next legitimate inbound message.
'from me' is no longer counted as a loop signal, and a tripped limiter now
logs a default-level warning once per conversation instead of verbose-only.
* fix(imessage): preserve delayed echo guards
…$ref anchors
resolveLocalRef decoded plain-anchor fragments with a bare
decodeURIComponent while the pointer branch already tolerated bad
escapes via decodePointerSegment's try/catch. A schema carrying
$ref: "#%" (a single typo'd hand-written anchor) made
findJsonSchemaShapeError throw a raw URIError that escaped
validateJsonSchemaValue, crashing plugin/channel config validation,
doctor, and gateway startup with a message that pointed nowhere near
the offending schema.
Decode anchor fragments with the same tolerance: a malformed escape
now resolves to { found: false } and surfaces as the intended
"<path>.$ref: unresolved ref" diagnostic.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* refactor(test): exercise real session rewind policy
* refactor(test): use real Telegram media policy
* refactor(test): exercise real HTTP cancellation lifecycle
* test(gateway): prove mutations clear queued session work
* feat(status): render /status as native rich tables on channels that support them
* feat(status): tighten rich /status layout into a titled native table card
* feat(status): trim rich /status tail to one clock-and-uptime context line
* feat(status): context meter, hot-window warning, and default-noise trim in rich card
* test(status): satisfy SessionEntry sessionId in meter fixture
* fix(telegram): gate rich table islands off legacy HTML sends and cover payload sends
* test(telegram): split outbound-adapter presentation tests to satisfy max-lines
* fix(outbound): make presentation capability resolution formatting-aware
* fix(telegram): defer presentation canonicalization on rich accounts until send
* feat(status): one fact per line in the plain status body
* feat(status): group the plain status body into blank-line sections
* feat(status): lead the rich status card with the version title
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* [AI] fix(diagnostics-otel): keep telemetry exporting across in-process restarts
Owned diagnostics-otel generations previously registered their OpenTelemetry
providers globally and only shut the providers down on stop. The pinned
sdk-node registers globals without override and never unregisters them, so a
second in-process generation (config-watcher reload, OPENCLAW_NO_RESPAWN=1,
containers) kept exporting through the first, already-shutdown providers and
telemetry silently stopped.
Switch owned mode to private BasicTracerProvider + MeterProvider instances
whose handles are injected directly into the existing recorder runtime, and
keep the preloaded (OPENCLAW_OTEL_PRELOADED=1) mode on the host's global
providers without ever registering or replacing globals. Resource detection
honors the pinned NodeSDK OTEL_NODE_RESOURCE_DETECTORS contract (unset
defaults to env+process+host; none/subset/all are respected), and the unused
@opentelemetry/sdk-node dependency is removed with a minimal lockfile update.
Adds a real-SDK two-generation restart regression, real-SDK resource-detector
selection coverage, migrates the unit mocks to the provider lifecycle, and
updates the OpenTelemetry docs.
Fixes#119997
Co-Authored-By: glm-5.2 <noreply@anthropic.com>
* [AI] test(diagnostics-otel): restore OpenTelemetry globals after restart test
The restart regression disables global context, metrics, propagation, and
trace APIs and forces OPENCLAW_OTEL_PRELOADED=0, but its cleanup only reset
diagnostic events, so a later test in the same worker could inherit no-op
providers. Snapshot the prior global registrations and preloaded env at
module load, and re-register/restore them in afterEach, matching the
existing exporter-health integration test pattern.
Co-Authored-By: glm-5.2 <noreply@anthropic.com>
* [AI] fix(diagnostics-otel): reconcile private providers with OTEL_SDK_DISABLED
Rebase onto main after #119961 (honor OTEL_SDK_DISABLED) changed the
disabled admission path to return before SDK construction. Restore the
removed getBooleanFromEnv import, drop the stale ownedNodeSdkDisabled
guard, and update disabled-mode tests to the new semantics (all routes
off, disabled runtime registered) plus the private-provider signal
path for the integration suite.
Related to #119997
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(diagnostics-otel): isolate restartable provider generations
* test(diagnostics-otel): prove same-pid generation routing
* test(diagnostics-otel): use supported watcher reload mode
* test(diagnostics-otel): satisfy ownership proof gates
* test(diagnostics-otel): use managed proof cleanup
* test(diagnostics-otel): verify injected trace ancestry
---------
Co-authored-by: glm-5.2 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* 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.
* fix(gateway): redact credentials from gateway URLs in status and logs diagnostics
Gateway URLs can carry basic-auth credentials or token query params. Status
output, status-all JSON, and logs-cli error diagnostics now project connection
details through a single diagnostics projection (projectGatewayConnectionDetailsForDiagnostics /
projectGatewayUrlForDiagnostics) so no diagnostic surface prints a raw URL;
the probe itself keeps using the unredacted URL.
* chore: re-fire CI
* fix(gateway): close remaining credential-leak paths in status diagnostics
ClawSweeper P1s: probe failure text (close reasons/transport errors) now
redacts URL-like credentials before status renderers print it; the status-all
remote-missing fallback line projects the environment gateway URL; gateway
transport-error JSON redacts the remote-controlled close reason and derived
message.
* fix(cron): retry on-exit watcher spawn/wait failures with backoff
A transient supervisor failure while arming or waiting on an on-exit watch
previously dropped the watch with only a log line: the job would never fire
and nothing recorded why. Failures now persist lastError/consecutiveErrors on
job state via the gateway cron service and re-arm with bounded backoff
(1s/5s/30s/5m); cancel clears any pending retry timer.
* chore: re-fire CI
* fix(cron): guard watcher-state writes against replaced on-exit jobs
ClawSweeper P1: an old watcher's failure write could land on a job that was
edited or converted to a different schedule, pushing the replacement into
failure backoff or auto-disable. Watcher-state persistence now uses the same
identity precondition as persistCompletion (enabled, on-exit, updatedAtMs
match) and treats a mismatch as a no-op.
* fix(slack): scope thread-starter cache per workspace and persist channel-id migration durably
Thread-starter cache keys now always include accountId+teamId so multi-workspace
installs cannot cross-read cached thread starters. channel_id_changed migration
previews against the persisted config snapshot and only mutates the in-memory
monitor config after the durable write succeeds; new-channel ingress traffic
serializes behind the migration lane via new_channel_id.
* chore: re-fire CI
* chore: re-fire CI against fixed main baseline
* 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
* fix(ai): enforce message_stop only for direct Anthropic models
The Anthropic SSE transport threw 'stream ended before message_stop' only when
a refusal buffer happened to be non-empty, so a truncated first-party stream
could pass silently while compatible proxy providers that legitimately omit
message_stop failed intermittently. The invariant now keys off the transport
contract owner: direct Anthropic models always require message_stop; proxy
providers are exempt.
* test(ai): cover proxy streams ending without message_stop
ClawSweeper P1: the proxy exemption had no focused test, so the lenient
branch could regress silently. A non-anthropic provider through a custom
endpoint now proves stopReason=stop with no error when the stream ends
without message_stop.
* fix(install): Windows installer surfaces doctor migration failures
Invoke-OpenClawCommand ignored the child exit code, so a failed
'openclaw doctor --non-interactive' still printed '[OK] Migration complete'.
The wrapper now throws on nonzero exit and Run-Doctor reports the failure
with the exact command to rerun instead of claiming success.
* chore: re-fire CI
* chore: re-fire CI against fixed main baseline