* perf(doctor): keep telegram doctor enumeration off the runtime graph
Telegram's built doctor artifact reached execa through dist chunking, so a
source-run host (pnpm dev, tsx CLI, vitest) could not require it and silently
dropped all 9 telegram legacy config rules plus its state migration. The
artifact also pulled telegram's runtime stores, making it a 674-chunk outlier
that dominated doctor enumeration.
Root cause: `src/token.ts` took the broad `plugin-sdk/provider-auth` barrel for
`resolveDefaultSecretProviderAlias`, dragging the auth-profile store, provider
runtime, and plugin install graph (execa, kysely, commander) into the closure.
The alias now has a narrow `plugin-sdk/secret-provider-alias` leaf, and
provider-auth re-exports it so its runtime surface is unchanged.
Thread-binding, sent-message, and sticker-cache row shapes, keys, and legacy
sidecar readers move to `*.legacy-state.ts` leaves. The doctor closure keeps
the rows and drops the ACP, session-binding, send, logger, and plugin-runtime
graphs the stores also load.
The postbuild control-plane verifier only required each artifact in a plain
Node child, the one host where these graphs resolve fine, so it proved nothing
about the invariant that broke. It now also walks each built doctor artifact's
static import closure and fails when it reaches the process-spawn graph, which
is the dist-level analogue of the source closure guard.
Guard rules added for provider-auth, acp-runtime, and conversation-runtime; the
telegram boundary test became a real closure assertion instead of a string grep.
* fix(doctor): drop dead export surface from the telegram legacy-state split
Knip and oxlint caught leftovers from the split: the leaves exported helpers
only they use, the store modules re-exported constants nobody imports from them
anymore, and thread-bindings kept a `testing` barrel whose last production
caller was the migration path that now reads the leaf directly. Tests import the
constants from the leaf that owns them, and the reset helper directly.
The closure gate's failure message still interpolated a `host` field left over
from a probe-host approach that was reverted before commit; the existing verifier
test caught it. The gate now has its own coverage: a transitive chunk edge to a
forbidden dependency is reported, while dynamic imports and non-doctor contract
surfaces are not.
* fix(doctor): adopt the upstream telegram thread-binding store split
`main` landed an equivalent thread-binding leaf as `thread-bindings-store.ts`
while this branch was open, so the branch-local `thread-bindings.legacy-state.ts`
is dropped rather than kept as a second path for the same rows.
`state-migrations.ts` now reaches token.js through the lazy import `main` added,
so `token.ts` is no longer in the doctor closure at all. The narrow
`secret-provider-alias` leaf still matters: telegram's contract-api closure
reaches `provider-auth` through `token.ts` on current `main`, which is the same
execa/kysely/commander graph, so the barrel is repaired at its source instead of
being deferred a second time.
* fix(scripts): type the built doctor closure gate for the TypeScript migration
The gate was authored against the `.mjs` script and landed in the `.mts` file
`main` migrated to, so its parameters were implicitly `any` and `check:test-types`
failed. Adds the explicit signatures plus the violation type.
Regenerates the plugin-sdk API baseline: `provider-auth` re-exports the default
secret-provider alias from the new leaf, so its module hash moves while its
runtime export surface stays identical.
* fix(imessage): harden remote Mac transport
Route SSH-backed iMessage actions through JSON-RPC, preserve remote database paths, and stage outbound files on the Messages Mac with bounded cleanup. Keep local action semantics intact while failing closed on ambiguous wrappers and surfacing the remaining imsg v0.13.4 limits.
* fix(imessage): remove test-only exports
* fix(plugins): keep OpenCode Go bundled
* fix(plugins): mark OpenCode Go dist bundled
* fix(docs): show OpenCode Go as bundled
* fix(release): defer bundled plugin publication
* fix(ui): hide inert task-suggestion chips and strip bidi controls
- render no suggestion card for operators who can neither accept nor dismiss (matches TUI behavior)
- strip RTL/bidi control characters from chip title, summary, project path, and instructions before rendering
- document the git-checkout requirement for spawn_task project directories (gateway enforcement landed in #120940)
* fix(ui): gate task-suggestion actions on negotiated gateway methods
Mirror the TUI capability rule: accept requires the taskSuggestions.accept method plus operator.admin, dismiss requires taskSuggestions.dismiss plus operator.write. List-only gateways render no chip; e2e fixtures now advertise the mutation methods they exercise, and a new regression proves a pending suggestion renders no chip when only listing is advertised. Addresses the ClawSweeper P2 on this PR.
* feat(cli): openclaw resume attaches the TUI to a recent session
Implements docs/plan/runners.md milestone 2.
* fix(cli): resume covers global sessions and preflights TTY
* fix(cli): require a terminal before resume
* fix(cli): adapt resume gateway disconnect state
* fix(cli): preserve global resume ownership
* test(cli): prove qualified global resume crosses the gateway boundary
Assign queue-owned opaque IDs to modern system-event snapshots so copied stale snapshots cannot acknowledge a later identical event. Keep structural matching only for shipped legacy ID-less consumers.
* fix(macos): let AI setup proceed when the login keychain is unavailable
A missing/locked login keychain made GatewayActivationBindingKeyStore
unable to mint the activation binding key, and onboarding refused every
candidate and manual-key activation with 'Secure storage is unavailable'.
The fingerprint only protects the crash/relaunch resume receipt, so
degrade instead of refusing: write an ownerless pending record (still
matched exactly, never as a wildcard), keep the full activation-ambiguity
window, and skip only restart reconciliation, which needs fingerprint
proof by design. A relaunch then repeats activation rather than trusting
the receipt.
Reported by Peter Steinberger.
* fix(macos): render proper provider brand icons in onboarding AI setup
Nearly every provider icon URL the gateway sends is a simpleicons.org
SVG, which AsyncImage cannot decode, so the Connect your AI page fell
back to generic symbols for every tool and provider; the two GitHub
avatar PNGs that did render clashed as full-color rasters.
Bundle nine monochrome template marks (Simple Icons CC0; xAI from
LobeHub icons, MIT) and resolve them local-first by brand id/kind,
including composed choice ids like xai-oauth. Remote icons now load via
NSImage(data:), which decodes SVG, with vector payloads tinted as
templates so plugin-supplied icons blend too. Every glyph sits in a
uniform rounded well; install cards show host-only links, hover states,
and a softer translucent surface shared by all setup sections instead
of controlBackgroundColor.
Requested by Peter Steinberger.
* fix(macos): use failable UTF-8 decode in icon vector sniffing (swiftlint)
* fix(macos): address ClawSweeper review of onboarding icon/keychain changes
- Refuse relaunch handoff from ownerless (keychain-unavailable) completed
receipts at the reconciliation trust boundary; such receipts can belong
to replaced credentials, so setup repeats a fresh activation instead.
- Decode and pass the canonical brandId the gateway sends for candidates,
auth options, and manual providers so bundled marks resolve for opaque
choice ids.
- Scan the bounded XML prolog (comments, declarations, doctype) when
sniffing remote SVG payloads; comment-prefixed vectors now tint as
templates.
* refactor(macos): move setup error enum to support file (swiftlint file length)
* chore(i18n): refresh native source inventory
* fix(macos): give keychain-unavailable activations attempt-specific unbound leases
A nil owner made concurrent unbound attempts indistinguishable: a stale
attempt's delayed response could complete or clear a newer attempt's
record. Unbound attempts now mint a random per-attempt lease id with a
sentinel fingerprint — live matching stays attempt-exact, restart
reconciliation's fingerprint guard rejects them, and relaunch
verification refuses unbound receipts before any handoff.
* chore(macos): ship third-party artwork notices with provider icons
Simple Icons (CC0) attribution and the LobeHub Icons MIT license text
for the xAI mark now travel inside the bundled ProviderIcons directory.
* fix(ci): refresh plugin-sdk API manifest for private-type hashing
#120975 changed the baseline generator to hash private types but landed
without regenerating the manifest; the check-plugin-sdk-api-baseline
lane was gated off on that PR and first failed here. Regenerated via
pnpm plugin-sdk:api:gen with no SDK surface change in this branch.
Propagate the receipt-acknowledgment steps from the canonical
ClawSweeper dispatch template (openclaw/clawsweeper#1080): mint a
minimal issues:write App token and post an idempotent
clawsweeper-pr-ack marker comment for non-draft opened and
ready_for_review pull requests, before review dispatch.
Use channels.telegram.groups as the shared default when an account omits groups. Explicit account maps, including {}, remain full replacements. This keeps chat admission and sender restrictions on one policy path and fixes silent multi-account authorization failures.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(cloud-workers): close lifecycle ownership gaps
Own bootstrap cleanup at the operation boundary and make fallback workspace sync converge across retries. Re-establish tunnel readiness per connection, retire placements before destructive session mutation, and keep operator diagnostics lightweight and redacted. Cover destructive lifecycle paths in their original execution order.
* fix(cloud-workers): drain local claims before retirement
delete/reset drain admitted local work, re-read exact identity, retire before destructive cleanup; active-claim/race tests.
* fix(cloud-workers): bind retry cleanup to workspace owner
Attest canonical HOME and the exact managed path.
Revalidate ownership before recursive fallback cleanup.
Cover malicious paths and ownership drift with tests.
* fix(cloud-workers): fence fallback workspace receivers
* refactor(plugin-sdk): delete the heavy runtime-doctor barrel
Nothing may pull the state-db/kysely graph through a doctor barrel anymore.
The barrel's remaining heavy exports move to two narrow private-local
subpaths, each with a single purpose:
- doctor-repair-runtime: install-path diagnosis, plugin config removal, and
state-database schema detect/repair (matrix doctor, voice-call lazy import)
- plugin-state-store-runtime: the sync keyed-store factory. It stays out of
plugin-state-runtime because hot channel entrypoints import that at module
load and opening a store pulls the state-database graph.
Doctor closures also stop pulling ssrf-runtime (fetch-guard + gateway net)
for two legacy private-network helpers that live in the lighter ssrf-policy
subpath: mattermost, nextcloud-talk, tlon, matrix.
The closure guard now forbids the two new heavy subpaths instead of the
deleted barrel, so the invariant keeps being enforced where it still applies.
* perf(doctor): keep heavy graphs out of every doctor closure
Doctor enumeration cold-loads each declaring plugin's contract closure, so
one heavy import in a closure is paid by the whole sweep. Four barrels were
still dragging unrelated graphs in for trivial helpers; each is repaired at
the leaf rather than by caching downstream:
- Legacy private-network config migration moves to a config leaf. It only
reshapes records, but lived beside the SSRF runtime (DNS, proxy, logging),
costing mattermost ~2.7s. ssrf-policy re-exports it, surface unchanged.
- Streaming config readers move to a leaf. They read two config keys, but
streaming.ts also formats tool aggregates, pulling tool-display/logging/
acp-core; that cost slack ~2.3s.
- signal took the channel-secret barrel for isRecord; the canonical plugin
record guard is string-coerce-runtime (root AGENTS.md).
- llm-task took the provider-model barrel for parseModelRef, now a narrow
model-ref-parse subpath.
Full doctor enumeration of all 42 declaring plugins, built mode:
legacy config rules 6668ms -> 1265ms, state migrations 184ms -> 127ms.
No plugin remains an outlier; the slowest is now ~380ms against a ~200ms floor.
Public export surfaces of every touched SDK subpath are byte-identical
(verified by diffing built module exports before/after); the API baseline
hashes move only because re-exported declarations emit differently.
The closure guard gains rules for each repaired barrel so the invariant
holds for future closures.
* fix(release): exclude new private-local declarations from the published package
Same pack-path rule as c41da3759f: private-local subpaths ship without d.ts.
* fix(doctor): repair the closure guard violations that break main
The landed guard fails on main: three closures import heavy barrels for one
symbol each. Two more surfaced once the guard learned about the provider-model
barrel. Each gets a narrow subpath at the leaf:
- telegram sent-message-cache + state-migrations took the session-store barrel
(session accessor + state-db) for resolveStorePath -> session-store-paths
- discord thread-bindings.state took the channel-outbound barrel (reply
pipeline + channel registry) for one identity write -> outbound-echo-runtime
- discord model-picker took the provider-model barrel for normalizeProviderId,
which model-ref-parse now exposes beside parseModelRef
The guard also stops walking artifacts of plugins whose manifest declares no
doctor surface. Such a declaration gates the artifact off every enumeration
path exactly as resolvePluginDoctorContracts does, so its closure cost is never
paid; anthropic ("doctorContract": {}) was being held to a cost it cannot
incur. Absent declarations still load eagerly and stay enforced.
Side effect worth naming: discord's built doctor contract now loads again.
On main both discord and telegram fail to require in packaged builds (an
ESM-only transitive dep) and silently lose their repairs; this restores
discord and takes enumerated legacy config rules from 87 to 99. Telegram's
built artifact still pulls execa through dist chunking - a build-level defect
with a different owner, filed as follow-up.
* 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>