Commit Graph

81255 Commits

Author SHA1 Message Date
Peter Steinberger b7fb951a94 fix(ci): let Crabbox own doctor readiness deadlines (#124725) 2026-08-16 11:30:24 -07:00
Peter Steinberger 86adb6e67e fix(gateway): stop broadcast serialization failures from storming gap detectors (#124726)
broadcastInternal consumed each client's seq before building the frame,
and swallowed any error in a bare catch. An unserializable payload
(circular reference, BigInt) threw identically for every client: the
event vanished for all of them, every seq advanced, and the next good
frame fired every client's gap detector simultaneously — a synchronized
full-reconnect storm with zero server-side evidence.

Root cause: seq consumption ordered before frame construction, and
serialization failure conflated with per-client send failure in one
catch. The frame now builds before the seq is consumed; a serialization
failure aborts the broadcast once with a logged error and touches no
seqs, while per-client send failures keep consuming the seq so that
client's gap detector still sees the loss.

Regression: new server-broadcast.serialization.test.ts — a circular
payload consumes no seqs, logs once, and the next broadcast is seq 1
for every client. Fails pre-fix (no log, seqs consumed).
2026-08-16 11:28:05 -07:00
Peter Steinberger 9d0c1f8f8c fix(tasks): skip session sweep when the cron store is unreadable (#124718)
* fix(tasks): skip session sweep when the cron store is unreadable

readRunningCronJobIds() swallowed any cron-store load error and returned
an empty running-job set. The session-registry sweep then treated every
running cron job's transcript as prunable and archived it — a corrupt or
locked cron store silently destroyed live transcripts on
'openclaw tasks maintenance --apply'.

Root cause: failure was collapsed into the 'no running jobs' success
shape instead of a distinct outcome. The loader now returns a closed
ok/error result; an unreadable store skips the sweep entirely and the
summary reports skippedReason (JSON + human output) so the operator sees
why nothing was pruned.

The sweep moved to tasks-session-registry-maintenance.ts: tasks.ts was
at its 700-line cap and the sweep is a coherent standalone concept; its
regression tests live beside it.

Regression: new colocated test fails on pre-fix semantics (empty-set
catch prunes the possibly-running transcript).

* chore(tasks): keep SessionRegistryMaintenanceSummary module-local

No external consumer; deadcode:exports gate rejects unused exports.
2026-08-16 11:25:53 -07:00
Peter Steinberger 23d4e6e496 fix(tui): report each reconnect failure instead of freezing on the first (#124700)
* fix(tui): report each reconnect failure instead of freezing on the first

The TUI gateway client deduped connect errors with a pendingConnectError
guard that only a successful hello cleared. Against a gateway that keeps
failing for different reasons (connection refused, then pairing
required), every error after the first was swallowed, so the pairing
approval hint never reached the operator — a silent dead-end.

Root cause: dedupe scope was per-connection-lifetime instead of
per-close-cycle. Clear the guard in the onClose suppression branch so
each reconnect attempt reports its own failure cause while repeated
errors within one cycle stay deduped.

Regression: new gateway-chat.reconnect-errors.test.ts fails pre-fix
(second error swallowed) and passes post-fix.

* test(tui): update connect-error dedupe test to per-close-cycle semantics

The prior assertion encoded the freeze-forever dedupe (second socket's
failure swallowed until hello). Dedupe is now per close-cycle, so the
retry socket's first failure is reported and duplicates within that
cycle stay deduped.
2026-08-16 11:23:42 -07:00
Peter Steinberger ffa0929c7f fix(agents): stop advertising unavailable tools (#124716)
* fix(agents): gate tool description cross-references

* test(gateway): include metadata copier in invoke mock
2026-08-16 11:20:45 -07:00
Leon-SK668 d0793caf38 fix(browser): reattach relay tabs after reconnect (#122177) 2026-08-16 14:19:54 -04:00
Peter Steinberger edf4c74601 fix(ci): prevent QA maturity scorecard timeouts (#124612)
* fix(ci): prevent QA maturity scorecard timeouts

* fix(ci): register QA evidence shard entrypoint

* fix(ci): isolate QA evidence jobs from shared caches

* fix(ci): separate QA selected revision from trusted harness

* fix(ci): protect every QA selected-code job

* fix(ci): bind QA workflow callers before checkout

* fix(ci): authenticate QA evidence caller chain

* fix(ci): remove dynamic QA checkout actions
2026-08-16 11:17:21 -07:00
Peter Steinberger 4fb27369ee perf(ci): keep the real-Gateway UI E2E lane on Blacksmith in hybrid mode
`checks-ui-e2e-real-gateway` folded `hybrid` into the `github` breaker clause
instead of taking the attempt-gated route its sibling `checks-ui-e2e` uses, so
it was the only Chromium lane pinned to hosted capacity on attempt 1. Measured
across 46 canonical main runs it averaged 205s (max 225s) against a 150-190s
plateau, making it the run's slowest job whenever the compact Node matrix
behaved.

Route it like `checks-ui-e2e`: Blacksmith 16-vCPU on attempt 1, hosted on rerun,
manual dispatch, fork, and same-repo PR retry. Cache inputs stay on the shared
pnpm store for both backends, matching the sibling lane.

The guard now derives both Chromium lanes' `runs-on` from one pinned template so
a hosted-only divergence cannot return unnoticed; mutation-checked by reverting
the workflow clause (2 failures).
2026-08-16 11:15:47 -07:00
Peter Steinberger 98805b8584 fix(auto-reply): reject redeliveries without recreating drained queues (#124723)
enqueueFollowupRun called getFollowupQueue (which inserts into the
process-global FOLLOWUP_QUEUES registry) before the recent-message-id
dedupe peek. A provider redelivery arriving after the original queue
drained and self-deleted recreated the registry entry, was rejected by
the peek, and left an empty FollowupQueueState (AbortController, arrays,
WeakSets) in the map forever — registry entries are only deleted when a
drain finishes, and a queue with no items never drains. Long-lived
gateways leaked one state object per redelivered session key.

Root cause: mutation-before-validation ordering. The dedupe key
derivation does not need the queue, so the peek now runs first and the
registry entry is only created for admitted work.

Regression: new dedupe test drains a queue to self-deletion, replays the
message, and asserts no registry entry reappears — fails pre-fix.
2026-08-16 11:15:22 -07:00
Peter Steinberger 8f23e68a10 docs(skills): note hosted state-dir and envelope media in session fetching
Two traps hit while reading a team-host session with images: hosted/systemd
installs keep state under the service user's home (root can carry a stray
install), and user-attached images live in message.__openclaw.media[] rather
than image content parts, so parts-only extractors find nothing.
2026-08-16 11:13:47 -07:00
Peter Steinberger 569835e8b0 fix(ci): align baseline ratchets across local and CI (#124720) 2026-08-16 11:07:31 -07:00
Peter Steinberger 6296777336 chore: remove stray agent report artifact from repo root (#124722) 2026-08-16 11:04:23 -07:00
Peter Steinberger d3a656602a perf(test): reuse Gateway for hosted wizard exits (#124721)
* perf(test): reuse Gateway for wizard exits

* test(gateway): compact wizard lifecycle coverage
2026-08-16 11:03:13 -07:00
Peter Steinberger 52ef157b5e test: preserve beta validation fixtures (#124717) 2026-08-16 10:58:59 -07:00
Peter Steinberger cae9ecaba4 fix(pr): substitute anchor-matching canonical wrapper for stale worktrees (#124710)
A linked worktree whose base predates (or carries) wrapper changes relative
to origin/main previously hit a hard refusal, even though the canonical
checkout held exactly the trusted origin/main wrapper the refusal message
told the operator to go run by hand. When the canonical checkout is clean
and byte-identical to fetched refs/remotes/origin/main, exec it with a loud
stderr notice instead; advisory dev-wrapper opt-in keeps precedence, and
the refusal remains when no anchor-matching wrapper exists on disk.

Also records the squash-merged stacked-branch rebase gotcha
(git rebase --onto origin/main <landed-branch>) in the PR maintainer skill.
2026-08-16 10:48:26 -07:00
sunlit-deng 0e7deeec45 fix(failover): classify transient Undici error codes (#123042)
* fix(failover): classify transient Undici error codes

* fix(failover): share transient network code policy

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-08-16 13:38:34 -04:00
Peter Steinberger aeff737da9 fix(agents): prevent invalid names from targeting the default agent (#124670)
* fix(agents): reject unrepresentable agent ids

* refactor(system-agent): split model selection setup

* chore: shrink assertion safety baseline

* docs: record strict agent id validation proof

* style: format strict agent id report

* chore: drop stray unrelated report artifact

* chore: restore REPORT.md to main state
2026-08-16 10:37:06 -07:00
Peter Steinberger 63401b730b fix: keep doctor security conditions as single findings (#124666)
* fix(doctor): record security finding severity

* refactor(security): keep audit severity internal

* fix(security): preserve channel finding severity
2026-08-16 10:24:40 -07:00
Peter Steinberger 6509a09ca7 fix(media): discover whisper.cpp models instead of a test fixture path (#124699)
The auto local-STT default hardcoded
/opt/homebrew/share/whisper-cpp/for-tests-ggml-tiny.bin — a CI
provisioning artifact — as the production whisper model. Real users with
brew-installed whisper-cpp and downloaded models got ready: false
('model file not found'), silently skipping local transcription; test-
provisioned machines transcribed real audio with the worst-quality tiny
fixture with nothing recording that a test artifact was chosen.

Discover ggml-*.bin models across standard install dirs, preferring
non-tiny models; WHISPER_CPP_MODEL stays the explicit override. Not
found now genuinely means no model is installed.
2026-08-16 10:22:46 -07:00
Peter Steinberger 2d6f8a79c4 fix(code-mode): advertise the nodes namespace only when the catalog has it (#124698)
Every other exec-description namespace hint (API, MCP, swarm, skills) is
gated on actual availability, but nodesGuidance was unconditional. The
nodes bridge resolves the owner-only openclaw:core:nodes tool, which
non-owner and policy-restricted runs filter out of the catalog — so the
prompt advertised a namespace whose every call fails with 'Unknown tool
id ... use tools.search', burning execs on guaranteed dead ends.

Gate the hint on the compacted catalog containing the nodes tool id,
matching the sibling guidance pattern (unknown catalog keeps the hint).
2026-08-16 10:20:36 -07:00
Peter Steinberger 66dc9d1c1e fix(doctor): show migration progress (#124692)
Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 10:06:53 -07:00
Peter Steinberger 715c379fd9 refactor(config): consolidate context budget to one per-model knob (#124665)
* refactor(config): consolidate context budget settings

* test(config): type legacy context fixtures

* test(config): align context budget fixtures

* fix(status): honor runtime context discovery

* docs(config): clarify context budget fallbacks

* fix(ci): resolve context budget lint failures

* test(ci): align context budget shard fixtures

* fix(models): preserve catalog context metadata

* fix(config): surface context migration diagnostics

* test(plugin-sdk): keep live catalog coverage focused
2026-08-16 10:05:01 -07:00
Peter Steinberger 4d872fbfbc feat(nodes): expose installed worker bundle status (#124640)
* feat(nodes): expose installed worker bundle status

* perf(nodes): defer bundle status validation

* fix(nodes): tighten bundle status type contracts

* docs(gateway): document node worker bundle status

* refactor(gateway): split runner inventory runtime

* test(ui): keep healthy device status quiet

* fix(ui): preserve steer target ordering

* test(ui): isolate catalog handoff lifecycle
2026-08-16 10:04:46 -07:00
Peter Steinberger 2aab6b8e37 refactor(reply): unify keyed FIFO leases (#124690)
Share one lifecycle-owned reservation primitive between reply admission and foreground delivery ordering, and remove the obsolete admission-wait callback plumbing.
2026-08-16 10:03:29 -07:00
Peter Steinberger 3ca7b038f0 test(azure-speech): make voices timeout proof deterministic (#124694) 2026-08-16 09:55:23 -07:00
Peter Steinberger 551c65e490 refactor(gateway): extract the pairing approval plan from requirePairing (#124691)
The requirePairing closure interleaved five non-interactive approval lanes
(silent-local, trusted-CIDR, trusted-proxy, setup-code bootstrap, Control UI
bootstrap) with request creation, inline approval, concurrent-approval
recovery, and SSH kickoff — the shape that let a silent-policy contradiction
(the removed scope-upgrade veto) hide for months. Lane eligibility now
resolves in one typed top-level function, resolvePairingApprovalPlan,
returning a closed plan the closure executes; behavior is intentionally
byte-equivalent and locked by the existing connect suites.
2026-08-16 09:52:12 -07:00
Peter Steinberger d91b290a05 fix(gateway): accidental gateway run against a live install no longer mutates its state before refusing (#124653)
* fix(gateway): refuse startup migrations when a live gateway owns the state directory

An accidental gateway run against a live-owned state dir (e.g. missing
OPENCLAW_STATE_DIR) ran legacy-dir relocation migrations before the runtime
lock acquisition refused readiness, mutating the owner's state on a startup
that never served. Probe the gateway lock identity before acquiring the
startup migration lease and refuse readiness first; the runtime lock stays
owned by the run loop. Regression test seeds a migratable legacy agent dir
under a live-locked state dir and asserts the refused run leaves it untouched.

* fix(gateway): refuse for a live state-dir owner before the first startup mutation

ClawSweeper found the initial gate still ran after mutation-capable work:
config-health recovery in gateway pre-bootstrap opens the shared state DB,
whose write admission quarantines orphaned SQLite sidecars (copies a nonempty
WAL/journal to .orphaned-* when the main DB is absent). Probe the live owner
at that first write boundary (pre-bootstrap suspicious-config recovery) and
again at preflight entry before state write admission; the lease-boundary
re-probe stays for the wait window. Regression now also seeds an orphan WAL
and asserts the refused run leaves the state DB directory byte-untouched.

* test(gateway): use a gateway-shaped child as the live lock owner

On Windows getFileLockProcessStartTime returns null, so the lock reader
validates the owner through process argv; the Vitest parent's argv is not a
gateway command, making the fixture read as a dead owner there. Spawn a
gateway-argv-shaped child and record its pid/start time in the lock instead.
2026-08-16 09:50:18 -07:00
Peter Steinberger 337933509d fix(android): stop wrapper subprocess trees on cancellation (#124686)
* fix(android): reap Gradle wrapper subprocesses

* fix(android): preserve wrapper spawn errors
2026-08-16 09:43:12 -07:00
Peter Steinberger eb63cb1481 fix: keep dirty changed checks scoped to local edits (#124685)
* fix(crabbox): preserve dirty changed-gate state

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

* fix(ci): cover WSL2 dirty changed gates

Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 09:40:48 -07:00
Peter Steinberger bdcc6836e0 refactor(gateway): retire the Control UI device-auth migration window (#124667)
* refactor(gateway): delete the retired Control UI device-auth migration window

The one-time remediation window for the retired
gateway.controlUi.dangerouslyDisableDeviceAuth break-glass threaded a pending
state machine through the connect pipeline, startup bootstrap, lifecycle,
request context, device management, security audit, the hello frame, and a
Control UI banner flow — steady-state runtime carrying a retired-shape shim
the architecture reserves for doctor. The window is closed: the retired key
is now fully inert, doctor still detects and removes it, and a browser that
never completed the migration pairs through the normal device flow (one
approval, no data loss).

Deleted with it: the config-machine-state import of the legacy flag, the
hello deviceAuthMigration field (optional; clients validate envelopes only,
so older gateways emitting it stay compatible), the migration-bound device
management authz states, the requireNoPairingCapableOperator approval mode,
the effective-operator pairing event emitter whose only subscriber was the
migration completion, and the Control UI banner, loader, overlay wiring,
i18n strings, and e2e scenario support. Swift and Kotlin protocol models
regenerated.

Live-verified on an isolated gateway with the retired key present in config:
clean boot with no migration warning, silent CLI pairing and silent local
scope widening unaffected, and doctor reporting the key as retired-and-inert.

Net -1111 production LOC.

* fix(ci): finish migration-window cleanup surfaced by the gates

The doctor migration for the retired key kept its "Preserved for remediation"
change text and describe; both now state plain removal, with the retired
tests updated. peekStoredDeviceIdentityId lost its only production consumer
with the deleted banner loader and is removed with its test mock. Also fixes
the unrelated no-unnecessary-boolean-literal-compare lint break that #124636
landed on main in scripts/check-changed.mts (truthiness is equivalent for the
boolean-or-undefined TTY probe).

* fix(ui): drop the retired-key device-auth reader from the security summary

Review findings on the migration-window removal: the Control UI security
summary still derived its "Device auth" row from the retired
dangerouslyDisableDeviceAuth key, rendering device auth as disabled on
configurations that merely retain the inert key. Device auth is now
unconditionally enforced, so the row and its derivation are removed rather
than pinned to a constant. The build-admission test also tracks and removes
its temporary device-identity databases after each run.
2026-08-16 09:39:15 -07:00
Peter Steinberger fa29984096 fix(cron): missing automations show recovery guidance (#124663)
* fix(cron): report missing automations as not found

* refactor(protocol): group gateway error exports
2026-08-16 09:25:03 -07:00
Peter Steinberger 9eb68f37cf fix(pr): support authenticated GitHub CLI wrappers (#124674)
Amp-Thread-ID: https://ampcode.com/threads/T-01a00adc-59a7-727f-8708-6623320ec2b2

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 09:24:36 -07:00
Peter Steinberger d258c6651a refactor: make dense test fixtures type-safe (#124625)
* test: replace assertion chains with typed fixture builders

* test: avoid generic mock lint suppressions

* test: preserve Telegram API mock signatures
2026-08-16 09:22:27 -07:00
Peter Steinberger 56f594aa97 fix(qa-lab): bound matrix probe re-polling by deadline (#124684) 2026-08-16 09:20:56 -07:00
Peter Steinberger 2b8e94ea4d fix(sessions): warn when a fire-and-forget disk-budget sweep fails (#124681)
The throttled budget kick swallowed every sweep failure into an empty
catch. Retrying on the next kick is right, but a persistently failing
sweep (corrupt store, permission loss) meant unbounded disk growth with
no operator signal — silent failure on the default maintenance path.
Warn with the error and store path before the retry.
2026-08-16 09:15:12 -07:00
Marvinthebored 2404d41de2 fix(codex): preserve delegation when image generation is denied (#124598)
Forward exact audited tool denies into agent harnesses so Codex can disable native image generation without dropping delegation. Fail closed when managed policy forces image generation and rotate retained threads when effective native config changes.

Co-authored-by: Marvinthebored <262704729+Marvinthebored@users.noreply.github.com>
2026-08-16 21:42:38 +05:30
Peter Steinberger 01a23bc8b9 test(gateway): assert the exact pinned-clock dispatch timeout (#124669)
The never-retry test pins Date.now specifically so the derived dispatch
timeout is deterministic, then asserted only <=777 — which also accepts
zero and would mask a broken deadline derivation. Assert the exact value
the pin guarantees.
2026-08-16 09:05:45 -07:00
wangyan2026 394ce62ef7 fix(compaction): use canonical session context projection for post-turn estimator (#117400)
* [AI] fix(compaction): use canonical session context projection for post-turn estimator

* test: focus CLI compaction regression

---------

Co-authored-by: Mason Huang <masonxhuang@icloud.com>
Co-authored-by: Mason Huang <masonxhuang@proton.me>
2026-08-17 00:00:50 +08:00
Peter Steinberger 421e319fb7 fix(llm-task): forward tool cancellation (#124673) 2026-08-16 08:59:40 -07:00
Peter Steinberger e0e9e272bc fix(pr): support split GitHub CLI auth (#124671)
Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 08:57:30 -07:00
Peter Steinberger 43e910142f fix(config): correct compaction mode help text; repair main lint + qa-lab flake (#124656)
* docs(config): correct compaction mode help text default claim

applyCompactionDefaults has written mode: "safeguard" for unset configs
since dd1b08b3e8, and docs/concepts/compaction.md documents that. The
schema help string still told operators to 'Keep "default"' as if it
were the shipped default — prompt/config text contradicting shipped
behavior. Align the help string with the actual default.

* test(qa-lab): tolerate the residual sleep race in stalled-probe bound

The stalled-versions-probe test asserted sleepImpl is never called, but
the inner AbortSignal.timeout (clamped to remainingMs) and the outer
deadline timer race at ~timeoutMs: when the inner timer fires first the
loop legitimately takes one residual sleep before Date.now() crosses the
deadline. Flaked on CI shard changed-extensions-config-27 for an
unrelated help-string PR. Protect the real invariant — bounded exit with
at most one deadline-clamped sleep — instead of the timer-ordering
artifact.

* test(qa-lab): type the stalled-probe sleep mock parameter

check-test-types rejects indexing an empty-tuple vi.fn mock call
(TS2493); give sleepImpl its real (ms: number) signature.

* fix(scripts): use the interactive flag directly in virtualization probe

no-unnecessary-boolean-literal-compare (type-aware lane) rejects
'(options.interactive ?? process.stdin.isTTY) === true'; both operands
are boolean, so use the expression directly. Broke check-lint on main
via 55240929f5a; fixed in this landing PR per the broken-CI default.
2026-08-16 08:56:00 -07:00
Peter Steinberger 76f4b942e0 fix(cli): report unknown subcommands instead of excess arguments (#124661)
* fix(cli): report unknown subcommands accurately

* style(cli): avoid map spread when collecting child command names
2026-08-16 08:51:33 -07:00
Peter Steinberger 5f561999a9 fix(scripts): drop redundant boolean comparison in worker detection (#124668) 2026-08-16 08:41:04 -07:00
Peter Steinberger 886e5c0490 fix(update): resolve detached dev status from origin (#124611)
* fix(update): resolve detached dev status from origin

* test(update): split update status coverage

* fix(update): preserve detached dev upstream

* fix(update): read detached upstream config
2026-08-16 08:33:55 -07:00
Peter Steinberger 61a3d3e7d9 perf(plugins): memoize failed facade module resolutions (#124664)
resolveFacadeModuleLocation only cached successful locations, so every
lookup for a plugin that is not installed re-walked the bundled-plugins
dir and registry on every request — request-time freshness polling of
process-stable install topology, which the architecture doctrine forbids.

Cache null results too. Install/reload/doctor flows already clear this
cache through registerPluginMetadataProcessMemoLifecycleClear, so a
newly installed plugin becomes visible exactly when the lifecycle owner
says so — the retry-on-appear test now pins that contract instead of
accidental per-call refresh.
2026-08-16 08:33:23 -07:00
Jason (Json) eb174bcaff fix(codex): candidate checks reject a mismatched app-server (#124137)
* fix(codex): validate managed app-server candidates

* docs(codex): document managed binary preflight

* test(codex): review managed preflight execution

* docs(codex): document managed app-server check

* fix(pr): support macOS system Bash operation locks

* fix(codex): scope managed preflight to Codex agents
2026-08-16 09:27:12 -06:00
Peter Steinberger b864da4d1f fix(gateway): recover remote-exec staged results after restart (#124619)
* fix(gateway): recover remote-exec staged results after restart

* fix(gateway): narrow pending result recovery placement
2026-08-16 08:25:16 -07:00
Peter Steinberger 55240929f5 fix: avoid nesting dedicated Linux workers (#124636)
* fix: avoid nesting dedicated Linux workers

* fix: preserve worker routing prerequisites

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-16 08:25:04 -07:00
Peter Steinberger eeffa53b20 improve(plugins): compile externalized plugins in source builds (#124639)
* build(plugins): compile externalized plugins into local dist

* test(plugins): assert native external plugin loading

* chore(plugins): keep source runner asset scan unchanged

* refactor(plugins): isolate external local dist builds

* test(plugins): codify external artifact precedence

* test(plugins): preserve contract path boundary
2026-08-16 08:21:17 -07:00
Peter Steinberger 146350f417 fix(nodes): keep MCP calls within the published catalog (#124652)
* fix(nodes): align MCP calls with published catalog

* chore(tests): shrink assertion safety baseline
2026-08-16 08:20:09 -07:00