Replace the drain waiter array + release loop with one lazily created
shared idle deferred (same semantics: concurrent waiters share it, it
resolves on the first all-trackers-zero publish), and fold the tracker
update into publishInboundPendingWork.
Collapse expectInboxPairingReplyText into expectPairingPromptSent: the
mocked pairing upsert always issues PAIRCODE, so the regex code
extraction and code-override parameter were dead generality. Net -20
lines of test-support code, no behavior change.
* test(whatsapp): give pairing-prompt waits the saturation budget
The monitor-inbox.policy pairing test waited on vi.waitFor's 1s default
while sibling pairing tests use the suite-standard 5s/5ms saturation
budget. Under full-project no-isolate runs the worker can stall ~1.5s
mid-flow (sync module fetches against the saturated shared transform
queue), starving waitFor's interval timers so the prompt lands just
after the deadline. Probes showed every mock firing correctly on the
right sock — not the shared-worker mock-defeat class.
Centralize waitForPairingPromptSent in the monitor-inbox harness, use
it at all three call sites (dropping the duplicated inline waits), and
align inbound.media's 2s delivery wait to the same 5s budget.
* test(whatsapp): event-driven inbound drain for pairing waits
Replace the wall-clock pairing wait with waitForInboundWorkDrained: the
harness now tracks each listener's onPendingWorkChanged signal and
resolves when pending inbound work returns to zero, so no saturation
stall can outlast a deadline. settleInboundWork keeps its yield-ticks
semantics for tests that observe intermediate states (held handlers,
parked debounce batches).
Route the policy and media-and-session monitor helpers through the
harness startInboxMonitor so their listeners are drain-tracked, collapse
media-and-session's bespoke pending-work machinery into the shared
helper, upgrade the policy negative assertions to drain-backed (non-
vacuous), and drop the now-unused getMonitorWebInbox export.
Preserve admitted external direct-message sender metadata at the transcript boundary so Control UI can show the contact label while gateway-local and from-me turns stay attributed to the operator.
Co-authored-by: SunnyShu0925 <shu.zongyu@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(channels): preserve gateway context for inbound turns
* fix(plugin-sdk): type bound channel reply dispatcher
* fix(channels): carry bound reply dispatchers
* fix(channels): keep reply carrier internal
* fix(auto-reply): consolidate dispatcher type imports
* fix(channels): keep reply dispatch typing internal
* fix(channels): derive inbound reply dispatcher types from turn plan
Keep ChannelRuntimeSurface identical to main: adapters read the bound
dispatchReplyFromConfig through the existing PluginRuntime["channel"]
wiring type and derive its type from the public ChannelInboundTurnPlan
contract, so the compatibility surface no longer grows a Gateway-bound
reply member.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(channels): hoist bound channel runtime casts for assertion ratchet
The assertion-safety ratchet collects SAFETY comments with a plain
ts.Scanner pass, which desyncs at the first template literal with a
substitution — comments after that point in a file are invisible, so
deep casts in monitor files cannot be SAFETY-covered. Hoist one shared
cast per call-site scope (absorbing the pre-existing buildContext
casts) and retype Discord's internal channelRuntime chain as
PluginRuntime["channel"] so dispatch reads need no assertion.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(channels): verify gateway dispatcher ownership
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* test(codex): assert sequenced node process notifications
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* docs(changelog): preserve release-owned root changelog
Channel Gateway tools preserve the owning dispatcher for Telegram, Discord, iMessage, Signal, and WhatsApp so terminal tools remain available. Thanks @VACInc.
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* test(codex): avoid shadowing process notification bindings
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
---------
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Centralize untrusted audio transcript framing across channel plugins and keep Discord voice transcripts out of typed command provenance.
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Split Signal approval routing into its concept-owned module and call the shared SDK binding helpers directly.
Reject persisted reaction targets when any allowed decision is invalid or duplicated, rather than retaining a valid subset from a corrupt transient record.
* fix(whatsapp): label voice transcripts as untrusted
* fix(whatsapp): preserve audio provenance in group history
Frame deferred voice transcripts while retaining their structured audio facts for later model-visible replay.
Refs #87269
---------
Signed-off-by: zhifu gao <lauragpt@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: zhifu gao <lauragpt@users.noreply.github.com>
* 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.
Native approval delivery and resolution now stay bound to the originating or explicitly targeted channel account. Unbound requests fail closed across multiple eligible accounts; trusted reviewer-less SDK callers remain compatible.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* 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.