Store resolved Telegram media references on observed messages so repeated quoted-photo turns reuse retained bytes instead of downloading them again. Preserve uncached external-reply media for the active turn and keep cache metadata out of model context.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Ayaan Gazali <ayaangazali.work@gmail.com>
* fix(telegram): validate command menu language codes
* fix(telegram): enforce exhaustive menu languages
* fix(telegram): harden command menu locale state
Carry canonical direct_messages_topic identity through authorization and config selection so channel DM topics use the same scope as delivery. Keep that identity distinct from message_thread_id.
Follows #120916 by removing duplicate outbound preparation and transport harness code while preserving channel Direct Messages, forum, and bot-private routing.
* 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.
Keep quiet Telegram tool progress on the transient draft lane when it exists. Preserve durable delivery for media, execution approvals, and ask-user prompts across direct and queued turns.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* perf(plugins): close the last kysely closure chains and guard reachability transitively
Follow-up to #120698/#120811/#120882: the closure guard's enumerated barrel
bans cannot catch new heavy edges, and two closures still statically reached
kysely on main.
- guard: add a transitive kysely-reachability test that walks static value
imports from every doctor-contract and legacy-setup closure through plugin,
plugin-sdk, and relative core graphs, failing with the full import chain;
type-only and lazy dynamic imports stay allowed
- llm-task/model refs: manifest-model-id-normalization reads snapshots
through a registration-slot runtime bridge (snapshot modules register at
eval; require fallback covers cold processes) and
current-plugin-metadata-state moves its process-scoped facts onto a
globalThis singleton so dual module instances share published state
- telegram: split thread-bindings-store.ts (pure record shapes + legacy-file
readers) out of the acp-runtime-heavy manager, delete the consumer-less
testing export, move the pure bot-user-id token parse to
token-fingerprint.ts, and lazy-import token.js in the async update-offset
detector
llm-task enumeration drops to ~0.8s/157 modules cold; every closure is now
statically kysely-free and stays that way by construction.
* fix(telegram): repoint the native-command menu state at the token-fingerprint parser
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>
Replies, previews, and media in channel Direct Messages topics now remain in their originating topic. Bot-private and forum topic routing remains unchanged.
* 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>
* perf(doctor): keep bundled doctor contract closures dependency-light
Doctor contract enumeration cold-loads each plugin's doctor-contract-api
closure via jiti, so a static value import of openclaw/plugin-sdk/runtime-doctor
pulled the state-db/kysely graph (~4.3s per closure) into
listPluginDoctorLegacyConfigRules / listPluginDoctorStateMigrationEntries.
- migrate all light doctor-contract closures (66 files) to the
dependency-light openclaw/plugin-sdk/runtime-doctor-migrations subpath
- voice-call: load detect/repairOpenClawStateDatabaseSchema* lazily inside
the migration bodies; keep only a type-only static runtime-doctor import
- matrix: split pure credential record shapes/normalizers into
credentials-state.ts so the doctor closure no longer imports the sync
plugin-state store through credentials-read
- guard: doctor-contract-closure-guard.test.ts now forbids static value
imports of runtime-doctor in closures alongside agent-runtime
* fix(matrix): keep credential revocation record type module-local
Knip production scan flags the export as consumer-less; the type is only
referenced by the exported union and revocation guard signature.
Claim-owned cancellation now stops pre-adoption media hydration, allowing later same-chat messages to proceed. Webhook handling now uses the canonical Telegram adoption stall timeout.
Fixes#120704.
* perf(plugins): declare doctor contract surfaces
* perf(doctor): slim migration import closures
* perf(plugins): narrow doctor declaration record surface and wire owner-test lane
Registry records carry only the doctorContract declaration instead of the whole
parsed manifest, and check:changed now selects the src/plugins-owned declaration
honesty and closure-guard tests for extension module/manifest changes so
cross-lane drift cannot pass PR classification.
* fix(doctor): keep control-plane dist imports require-safe
Keep doctor and channel control-plane chunks off exec-class dependencies, and enforce native require(esm) loading during postbuild.
* chore(plugin-sdk): regenerate API baseline
* chore(plugin-sdk): sync export ordering
* fix(plugins): satisfy doctor contract CI boundaries
* perf(doctor): make qqbot doctor closure dependency-light
qqbot was the last plugin above 5s in doctor state-migration enumeration
(~8s under tsx/jiti). The cost was not the state-key builder (already a
leaf): its doctor closure value-imported the runtime-doctor SDK barrel,
whose plugin-state-store/state-db re-exports pull kysely (~330 modules),
plus security-runtime for one fileExists (~200 modules), all resolved
per-module by jiti during enumeration.
Split the migration-define helpers and light re-exports into a new
private-local plugin-sdk/runtime-doctor-migrations subpath; runtime-doctor
re-exports it so its public surface is byte-identical (API baseline hash
unchanged). qqbot's doctor-contract and state-migrations now import only
the light subpath, swapping fileExists for the equivalent async
legacyStateFileExists already in the closure.
qqbot enumeration: ~8.0s/531 modules -> ~0.25s/18 modules.
* chore(plugin-sdk): drop private-local subpath from API baseline
runtime-doctor-migrations is private-local-only; the baseline tracks public
modules, and the earlier line was generated before the classification.
* fix(plugins): register runtime-doctor-migrations boundary paths
The private-local subpath list feeds the extension package boundary map;
the shared paths config and xai's derived overrides must carry the same
entry or the boundary contract test fails.
Resolve reply mode through the selected Telegram account so account overrides and top-level inheritance reach outbound reply context.
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>
Preserve native Telegram quote attribution in agent-visible input and terminate quotes before following ordinary text.
Co-authored-by: Peter Steinberger <steipete@macos.shared>
Keep Telegram native and typed callback routes under their owning handlers. Expired or malformed typed buttons now clear their controls and return visible unavailable feedback while legacy raw plugin callbacks remain compatible.