* fix(telegram): clean up thread bindings to stale/failed ACP sessions on startup
When loading persisted thread bindings on manager creation, validate each
ACP session against the session store. Remove bindings where:
- Session entry doesn't exist (deleted externally)
- Session status is failed/killed/timeout
- ACP runtime state is 'error'
This addresses issue #60102 where Telegram DMs remained routed to stale
ACP sessions even after restart, because the binding file persisted
across restarts without validating the target session was still valid.
* fix(telegram): guard against null session entry and transient store read failures
Address review comments on PR #67822:
1. Skip bindings when readAcpSessionEntry returns null or when
session store is temporarily unreadable (storeReadFailed: true).
Without this, a transient I/O error would mark all ACP bindings
as stale and delete them on every startup.
2. Only set needsPersist when bindings were actually removed.
Previously, stale session keys from OTHER accounts could set
needsPersist=true even when zero bindings were removed for
the current account — causing spurious disk writes.
Also clean up redundant optional chaining on entry.status now
that we guard against undefined/nullable sessionEntry.
* perf(telegram): dedupe ACP session reads in startup cleanup
Cache readAcpSessionEntry calls by targetSessionKey. Multiple bindings
to the same ACP session now result in a single session store read instead
of one read per binding.
Addresses chatgpt-codex-connector P2 review comment on PR #67822.
* fix(telegram): skip non-ACP session keys in stale binding cleanup
Address chatgpt-codex-connector P1 review comment on PR #67822:
Plugin-bound Telegram conversations use "plugin-binding:*" keys
with targetKind === "acp", but these are NOT ACP runtime sessions.
readAcpSessionEntry() returns no entry for them, so !sessionEntry.entry
would classify them as stale and delete them on every startup.
Now checks isAcpSessionKey(binding.targetSessionKey) to skip plugin-bound
sessions from the stale session cleanup scan.
Also clarifies the comment to explain why we use targetKind === "acp"
// together with isAcpSessionKey() check.
* fix(telegram): import isAcpSessionKey from sessions/session-key-utils
isAcpSessionKey is not re-exported from openclaw/plugin-sdk/routing.
Fix import to use the correct subpath: openclaw/sessions/session-key-utils.
Addresses chatgpt-codex-connector P1 review comment on PR #67822.
* fix(telegram): import from relative path, remove unused variable
- Import isAcpSessionKey from relative path ../../sessions/session-key-utils.js
(not openclaw/sessions/session-key-utils which doesn't exist)
- Remove unused 'bindings' variable in for-of loop
Addresses CI failures on PR #67822.
* fix(telegram): export isAcpSessionKey from plugin-sdk/routing
isAcpSessionKey lives in src/routing/session-key.ts, which is already
exported via openclaw/plugin-sdk/routing. Re-export it from routing.ts
so extensions can import via the public plugin-sdk path.
Fixes chatgpt-codex-connector P1: relative path ../../sessions/session-key-utils.js
doesn't exist in the build output, making the Telegram extension fail
module resolution before startup cleanup can run.
* test(telegram): cover startup ACP binding cleanup
* fix: clear stale telegram ACP bindings on startup (#67822) (thanks @chinar-amrutkar)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Rely on the lint wrapper to prepare extension package-boundary artifacts during pnpm check instead of invoking the same prep script again at the end.
Add a script regression so the duplicate check path does not return.
Make the Matrix QA CLI single-shot exit contract symmetric: artifact-backed failures now print the preserved error, flush stdio, and exit with code 1 instead of waiting on Matrix native handles.
Keep an opt-out for direct test harnesses with OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT.
Add the Matrix subagent-thread scenario and route it through the contract runner while preserving the current missing-hook failure as an explicit scenario result.
Give E2EE scenarios isolated rooms and storage keys so lifecycle tests do not reuse stale encrypted state across scenarios.
Refresh published cross-signing keys before bootstrap imports secret-storage keys, add sync-filter plumbing for QA E2EE clients, and document the remaining upstream key-backup cache noise without suppressing SDK logs.
Move mock and live provider behavior behind provider-owned definitions so suite, manual, Matrix, and transport lanes share defaults, auth staging, model config, and standalone server startup.
Add AIMock as a first-class local provider mode while keeping mock-openai as the scenario-aware deterministic lane.