* 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
* fix(plugins): own packed entry lifecycles by package
Persist package ownership per runtime child, route lifecycle actions through one closed resolver, reconcile removed child policy during updates, and retain rollback generations until durable config/index commit.
* fix(plugins): break uninstall policy import cycle
* test(plugins): model package ownership in lifecycle fixtures
* refactor: use canonical re-export names
* fix(test): classify suite support as test source
* fix(agents): retarget gateway stub session-entry import
* test(gateway): retarget session-utils mock keys after alias removal
* 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.
* 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>
toSorted/flat in the labeler coverage test, drop no-op empty-object spread
fallbacks in the telegram command-menu e2e config, and attach the caught
cause when rewrapping the trusted-workflow parse error.
* fix(workers): persist placement terminal failures
* fix(workers): refresh placement protocol clients
* refactor(workers): isolate error formatting
* fix: integrate cloud terminal state with current main
* chore(plugin-sdk): refresh API baseline
* refactor(ui): inline one-use cloud terminal-reason banner helper
Keeps the Control UI startup JS bundle inside its 317 KiB gzip budget
(the helper + type-only import tipped it by 16 bytes).
* refactor(ui): trim terminal-reason lookup to type-erased optional access
Recovers the last gzip byte of the Control UI startup budget
(324609 B vs the 324608 B limit).
* fix(gateway): flush trailing chat deltas
Keep the fixed-deadline wake-up on the existing chat run record so terminal, abort, and shutdown cleanup cancel it at the lifecycle owner. Normalize voice runs onto their unique per-turn ID instead of compensating for a stale client alias downstream.
Fixes#119557
Co-authored-by: Serghei <43180231+xyrolle@users.noreply.github.com>
* test(gateway): drop obsolete delta length assertions
Remove stale fixtures and assertions for the write-only chat delta length field deleted by the owner-boundary repair.
Co-authored-by: Serghei <43180231+xyrolle@users.noreply.github.com>
* fix(gateway): preserve chat run state type contract
Keep the optional delta length field and its cleanup semantics in the exported ChatRunState closure without restoring runtime writes.
Co-authored-by: Serghei <43180231+xyrolle@users.noreply.github.com>
* refactor(gateway): hide chat delta timer from SDK shape
Keep the trailing wake physically on each run record while exposing it only through internal state-module accessors, preserving the public ChatRunState closure.
Co-authored-by: Serghei <43180231+xyrolle@users.noreply.github.com>
* refactor(gateway): narrow chat delta timer access
Use one Gateway-internal record accessor so the run-owned timer remains outside the public ChatRunState shape without extra state maps.
Co-authored-by: Serghei <43180231+xyrolle@users.noreply.github.com>
* refactor(gateway): keep chat timer casts private
Keep the run-owned timer invisible to the generated Plugin SDK closure by using module-private casts in each owning Gateway module.
Co-authored-by: Serghei <43180231+xyrolle@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* perf(agents): keep turn-path model catalog reads off the full live build
First agent turns (embedded and cron) resolved thinking capability through
loadPreparedModelCatalogSnapshot without readOnly, which materialized the
full live model-runtime catalog: ambient synthetic-auth discovery fanned out
to every registered provider and loaded plugin discovery modules through
jiti source transform (3,172 TS modules, 36s event-loop block, +600MB heap,
58.7s model-selection on a cold gateway).
- add loadProviderScopedThinkingCatalog: manifest metadata first, then a
provider-scoped read-only static catalog, then scoped live discovery only
for runtime-discovery providers (preserves #116584 Ollama semantics)
- route scopedLiveProviderDiscovery through the scoped read-only loader
- scope live-mode ambient synthetic-auth refs to the requested providers
- bound the last-resort synthetic-auth sweep to discovery entry modules
- memoize per-turn plugin skill dir resolution/republish (single-slot,
lifecycle-cleared; was a full walk + symlink republish every turn)
Cold first turn 72.7s -> ~22s wall (remaining cost is provider prefill of
the ~19.5k-token default prompt); model-selection 58,726ms -> 124ms.
* test(agents): align model-catalog.runtime mocks with scoped thinking catalog seam
Explicit vi.mock factories must export every binding prod touches; the new
loadProviderScopedThinkingCatalog export is now mocked everywhere the module
is stubbed, and the live-model-switch Ollama hydration test asserts the new
provider-scoped seam instead of the retired unscoped snapshot call shape.
* test(agents): export scoped thinking catalog from every prepared-catalog mock; split synthetic-auth helpers
- add loadProviderScopedThinkingCatalog to all explicit prepared-model-catalog
and model-catalog.runtime mock factories (vi.mock factories must export every
binding prod touches)
- move synthetic-auth ref scoping/resolution into
prepared-model-runtime.synthetic-auth.ts; keeps facts under the max-lines cap
* test(agents): prove scoped thinking hydration for runtime-only models
Boundary proof for the ClawSweeper review gap: the three-tier helper stops at
manifest or scoped-static when they resolve, and runs provider-scoped live
discovery (no broad fanout) only for runtime-only models; cron selection
hydrates through the same scoped helper and skips it entirely for thinking=off.
* test(agents): accept rest args in scoped thinking catalog mocks
* fix(telegram): prioritize configured commands under menu pressure
* fix(telegram): preserve custom commands under localized menu pressure
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: ayeshakhalid192007-dev <ayeshakhalid192007@gmail.com>
* feat(agents): record run-end worktree cleanup outcome
Persist removed, retained, and failed run-end cleanup outcomes on managed worktree records. Operators and QA can inspect the durable fact through worktrees.list and openclaw worktrees list --json.
Release note: Managed worktree run-end cleanup now records why a checkout was removed or retained in worktree list JSON.
* test(qa): prove dirty worktree retention outcome
* chore(protocol): regenerate swift gateway models
* fix(agents): harden worktree cleanup recovery
Register run_end_cleanup_json as a lazy compatible column so same-version v6 index repair and read-only doctor migration can recover databases created before the column existed.
Type removal contention at the registry boundary; unexpected claim failures now best-effort record a bounded failed outcome and rethrow the original error.
* fix(ci): clear repo-wide lint debt blocking merge gates
The red-main landing rule requires this PR to repair repository-wide merge-gate debt instead of bypassing it. Apply the current lint contracts mechanically and split turn-transition coverage into a concept-named sibling with per-file-safe test state.
Exact line delta: +676/-574 (net +102) across 44 test/support files.
* fix(ci): preserve cached health refresh proof
Require the public refresh call to exist before accepting that sensitive fields were omitted, so the boundary proof cannot pass on a missing call.
* fix(ci): correct test typing left by the lint sweep
Literal-widened totalTokensVersion fixtures, a WebSocket RawData overload
mismatch, and the protocol schema document cast broke check-test-types
after the repo-wide lint repair. Aligns the fixtures with SessionEntry,
narrows Buffer handling per RawData, and keeps the JSON-shaped undefined
omission under structuredClone.
* test(agents): reuse upstream resource-loader test support
The session-loop split and #120463's helper extraction landed the same
createResourceLoader/createCompactionHandlers twice; the rebase kept both,
orphaning main's agent-session-loop-resource-loader.test-support.ts and
failing the dead-code gate. Import the upstream helpers and delete the
duplicates.
* fix(agents): reject finalized rows at the worktree removal claim
Address the accepted ClawSweeper late-claim finding by rereading and rejecting missing or finalized worktree rows inside the synchronous removal-claim transaction.
Preserve the authoritative cleanup invariant: finalized contenders record nothing, while retained-busy is written only while the row remains live.
* refactor(agents): reuse registry update for busy outcomes
Keep the live-row conditional write in the canonical registry update path so the finalized-claim repair stays below the registry max-lines ratchet without weakening the authoritative-outcome invariant.
* test(agents): drop session test duplicates after rebase
Keep current main as the canonical owner of next-turn lifecycle coverage and correctness test support after replaying the older lint-debt split.
* fix(agents): guard post-abort cleanup outcomes against finalization
After abortWorktreeRemoval releases a stale remover's claim, its retained or
failed write raced a finalizing remover and could overwrite the authoritative
removed-lossless fact. Route every retained/failed write through the live-row
condition; only the finalizing remover's own removed-lossless write stays
unconditional.
* fix(agents): persist the removal outcome atomically with finalization
A delayed removed-lossless write after remove() finalized could race a
restore plus newer cleanup and overwrite the newer operator-visible fact.
The run-end outcome now rides remove()'s finalization update; every other
cleanup write stays live-row conditional, so no post-finalize write path
remains.
* test(qa): restore strict cached-health contract assertions
The lint sweep's Boolean() coercions let truthy non-booleans satisfy the
wire-typed cached-meta contract. Assert the literal boolean for unknown-typed
fields and use nullish-coalesced strict equivalents for boolean chains.
* fix(agents): clear the stale cleanup outcome when restoring a worktree
A restored checkout begins a new lifecycle; leaving the removed-lossless
fact on the live row showed operators a stale result until the next
cleanup. Restore clears the recorded outcome and the regression asserts
the cleared state before the next cleanup records fresh truth.
* fix(agents): scope stale cleanup outcomes to their observed lifecycle
A stale remover's retained/failed write raced a concurrent remove-plus-
restore: the revived row is live again, so the live-row condition alone
could stamp a prior-lifecycle outcome. Condition those writes on the
activity stamp the remover observed; restore bumps lastActiveAt, making
any prior-lifecycle write a no-op.
* fix(agents): advance the restore activity stamp within one millisecond
Stale cleanup writes fence on the activity stamp they observed; a restore
completing in the same millisecond could revive the row with an identical
stamp and let the fence match. Restore now always advances past the
stored value, and the ABA regression pins the clock to prove the
same-millisecond case.
* 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.
* [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>