* fix(telegram): isolate webhook/ingress-worker shutdown errors and guard all cleanup
- Wrap entire shutdown() body in outer try/catch so sync throws never
produce unhandled rejections from fire-and-forget abort listeners.
- bot.stop() rejection is caught and logged; finally block ensures
closeTransportOnce(), noteWebhookStop(), and stopDiagnosticHeartbeat()
always execute regardless of bot.stop() outcome.
- closeTransportOnce() rejection is caught independently in the finally
block so noteWebhookStop() and stopDiagnosticHeartbeat() are not skipped.
- Startup failure path: bot.stop() and closeTransportOnce() get
.catch(() => undefined) so cleanup rejections never mask the original
startup error.
- Both abort-listener call sites use consistent void shutdown() since
the never-reject contract is now internal.
- Ingress worker stop() shares an in-flight stopPromise for idempotency;
worker.terminate() rejections are observed via .catch(() => undefined).
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fix(telegram): continue webhook cleanup after sync server.close throw
Extract independently guarded shutdown phases so a synchronous
server.close() failure still runs bot.stop, transport close, status,
and diagnostic heartbeat cleanup. Add sync-throw regression coverage
and a loopback real-behavior proof (negative/positive/valid).
* fix(telegram): clear webhook CI lint, knip, and test-types
Restore curly-safe drain timer clear, stop exporting the unused
shutdown phases interface, and narrow the transport close spy for
tsgo test types.
* chore(telegram): drop committed webhook shutdown proof script
* test(telegram): inline webhook shutdown proof cases without proof script
* fix(telegram): drop stale reply-fence import after main rebase
telegram-reply-fence.ts was deleted on main (f7786a16cf) during the
core drain refactor. The rebased webhook.ts no longer calls these
functions; remove the dangling import so the module resolves.
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fix(telegram): continue webhook shutdown after phase failures
Keep each fallible Telegram-owned teardown phase independent so an early failure cannot skip transport, ingress, status, or diagnostic cleanup.
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
---------
Co-authored-by: nebulacoder-v8.0 <noreply@zte.com.cn>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(telegram): linkPreview:false is ignored on streamed replies
Replies delivered through draft streaming still unfurled the first URL
even with channels.telegram.linkPreview: false. Non-streamed sends
already honored the flag.
createTelegramDraftStream never received linkPreview and never set
link_preview_options, on either its initial sendMessage or its
editMessageText calls. Finalization could not clean it up: it skips the
final edit when the streamed draft text already equals the final text,
which is the common case, so the draft message stays as-is with the
preview attached.
Passes linkPreview from telegramCfg into the draft stream and applies
link_preview_options: { is_disabled: true } on the send and on every
edit, since an edit that omits the field re-enables the preview
server-side. Rich messages keep expressing this as skip_entity_detection
at render time, which already worked.
Call arity is preserved when no preview options apply, so existing edit
assertions are unaffected.
Closes#111525
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(telegram): cover streamed reply option parity
Co-authored-by: Zakaria Rahali <zakariarahali288@gmail.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(telegram): suppress message-tool-only reply fallbacks
Honor message-tool-only delivery when a non-silent Telegram reply is skipped in both ordinary group dispatch and native slash-command turns. Keep the existing visible fallback for genuinely empty native replies and cover both dispatch surfaces with regression tests.
Fixes#90091
* fix(telegram): preserve genuine delivery failure fallbacks
Suppress only non-silent skips owned by message-tool-only delivery. Preserve visible fallbacks when the same turn also encounters a real dispatch or final-delivery failure, with an explicit regression for the mixed failure path.
* fix(telegram): preserve native message-tool delivery errors
Track genuine native slash delivery failures so message-tool-only skip suppression never hides a failed response. Cover the exact mixed empty-skip and real-error path with a regression while preserving true-empty and intentionally cancelled fallbacks.
* test(telegram): preserve synchronous native delivery callbacks
Use the non-mutating dedupe-cache contract when checking unaccepted Telegram updates. Preserve active handler and completed-update deduplication while allowing a real update to be accepted after repeated handler skip probes.
Fixes#105192
Classify pinned Grammy blocked, kicked, and deactivated recipient 403 responses as permanently unreachable. Preserve retry behavior for recoverable permissions and flood control, inspect wrapped causes, and prove real SQLite ingress drain dead-lettering without duplicate dispatch.
Fixes#112893
Track full dispatch completion separately for error handling and shutdown drain while allowing same-session follow-ups to steer active runs. Fixes#113180.
Co-authored-by: Taksh <takshkothari09@gmail.com>
* fix(telegram): surface draft stream delivery failures at warn level
createTelegramDraftController wired the draft stream's warn callback to
logVerbose, so preview send/edit/cleanup failures ("telegram stream
preview failed: ...", "telegram stream preview cleanup failed: ...")
were only emitted when verbose logging was enabled. In the default
configuration a dying preview/draft stream left no operator-visible
trace: the bot just went quiet, especially in progress stream mode
where the activity window is the only delivery surface.
Route the warn callback through the telegram subsystem logger
(telegram/draft-stream) at warn level with lane, chatId, and threadId
context so draft delivery failures show up in default logs. The
verbose log callback is unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
* test(telegram): simplify draft warning logger proof
Co-authored-by: Arseniy Palagin <valeradzigurda3@gmail.com>
* fix(telegram): emit draft terminal delivery diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1bd0f71-7638-463a-958d-d5b3e2da1047
* fix(telegram): keep draft visibility change scoped
Remove the diagnostic expansion so this contributor PR remains focused on its original warn-level logging repair.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d1bd0f71-7638-463a-958d-d5b3e2da1047
---------
Co-authored-by: Arseniy Palagin <valeradzigurda3@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
Copilot-Session: d1bd0f71-7638-463a-958d-d5b3e2da1047
The previous commit patched me.id into 13 contexts in one suite. That treated the
symptom: the harness itself never passes botInfo, so resolveBotUserId has no
fallback when a test ctx carries only a username.
Fix the harness instead. Production always builds the bot from getMe(), so
botInfo is present in every real path; supplying telegramBotInfoForTest matches
that and drops the per-context ids.
Also revives bot.media.stickers-and-fragments.e2e.test.ts, which shares the
harness and failed 3/6 for the same reason and was not covered before.
The other ~84 bare me literals across 8 Telegram suites are fine: those tests
supply botInfo through their own harnesses. Verified, 371 pass, so no sweep.
PR #115401 fixed the album split by separating durable claim lifetime from lane
occupancy (deferredLaneOccupancy="release"), and stated the same change repairs
the text/forwarded-message debounce, which defers its spooled participant the
same way. The album path got a durable-drain regression test; the debounce path
did not, because the suite that would cover it fails 10/10 on main.
Restore that suite: its harness supplies me.username without me.id, which
resolveBotUserId has required since #114532. Production grammY populates ctx.me
from getMe(), so this is harness drift, not a runtime gap.
Then extend the durable-ingress regression file to both buffers and rename it
off media-group. Two new tests admit a forwarded burst through the spool and
core drain, live and from a restart backlog, asserting one turn carrying both
notes plus independent tombstones.
Both fail with deferredLaneOccupancy reverted to "hold" (second note lands in a
separate turn), so they pin the lane contract rather than restate it.
* fix(telegram): coalesce durable album ingress
Split durable claim lifetime from lane occupancy so Telegram can admit later album members while every deferred claim remains heartbeated, recoverable, and independently settled.
With deferredLaneOccupancy=release, an unrelated later same-lane Telegram message can reach reply admission before an album that is still inside its 500 ms flush window. That restores the pre-f7786a16 contract, not a new defect; grammY sequentialize still preserves handler-entry order and the reply lane serializes once a turn is admitted.
* fix(telegram): preserve deferred abort semantics
Release deferred claims when their owner aborts before settlement, while preserving adoption when settlement won the race. Supersede every accepted pre-adoption state on released lanes without admitting past a surviving lane owner.
* fix(telegram): separate participant rejection from settlement failure
The detached deferred continuation chained its rejection handler with .catch
after .then, so it observed not only a participant.task rejection but also any
error thrown by onFailed()/onAdopted() and re-drove that infrastructure error
through onFailed().
That applied the wrong disposition when the claim was still pre-adoption, and
silently discarded the error once it was not: onAdopted() sets phase to adopted
before its tombstone write, so a wedged write reached a re-entrant onFailed()
that returned early on the phase guard and never reached the logging handler.
Use the two-argument then form so task rejection and lifecycle settlement
failure stay on separate paths.
* fix(channels): satisfy ingress CI guards
* fix(telegram): persist update offset only after durable spool write in isolated polling
Closes#113315
In isolated polling ingress, the parent session now persists the Telegram
update offset only after the update has been durably written to the spool.
This prevents an update from being acknowledged (via the offset) before it
has been safely enqueued, which could lead to permanent data loss if the
process crashes between offset persistence and spool write.
Changes:
- Call persistUpdateId(updateId) after writeTelegramSpooledUpdate succeeds.
- Add structured logs for poll-start, update receipt, spool success/failure,
and offset persistence success/failure.
- In isolated mode, stop passing onUpdateId to the bot so the offset is only
persisted once, after the spool write.
- Add regression tests verifying spool-before-offset ordering and the
failure path when spooling fails.
* fix(telegram): persist isolated polling offset only across contiguous spooled updates
Closes#113315
- Track successfully spooled update ids and advance the persisted offset only
through a contiguous boundary. This prevents a later update_id from being
persisted before an earlier one has finished spooling, which would make the
earlier update unrecoverable after a restart.
- Serialize spooling while no durable offset floor exists, so the first
successful spool becomes the persisted floor.
- Coalesce duplicate worker deliveries for the same update_id and ack both
success and failure paths.
- Fix TypeScript cast errors in polling-session.test.ts.
* fix(telegram): serialize durable polling offsets (#113368)
---------
Co-authored-by: Carlos <carlos@example.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* refactor(prompt): plain inbound context labels with a provenance marker
Replaces trust-worded inbound context labels ("(untrusted metadata)",
"(untrusted, for context)") with plain labels plus a fixed provenance
marker suffix appended to every OpenClaw-injected context header.
Detection keys on the marker, not label text, so strippers stay correct
across UI, TUI, replay, /trace segmentation, memory recall, and the Swift
chat preprocessor. Drops sanitizeInboundSystemTags in favor of the marker
boundary plus trusted system-prompt narration.
Renames the untrusted-named plugin SDK context identifiers to
channel-provenance names, keeping deprecated aliases registered for
removal after 2026-09-08.
Adds `openclaw doctor --fix` migrations that rewrite legacy inbound
labels in stored SQLite transcripts and purge legacy envelope-
contaminated LanceDB recall rows.
* fix(ci): resolve gate failures for plain inbound context labels
- doctor sqlite readers: open read-only connections via openNodeSqliteDatabase
so the Kysely connection-boundary guardrail holds; unexport the now-internal
transcript snapshot type (Knip unused-export gate).
- compat registry: split the record table into registry-records.ts and
plugin-sdk-subpath-records.ts. The new compat record pushed registry.ts past
the 700-line oxlint cap; suppressions are disallowed, so follow the existing
sibling record-module pattern. Public exports and PluginCompatCode literals
unchanged.
- acp-runtime test: assert current finalization behavior (newline normalization
only). The bracket de-fang and System: rewrite it expected were removed with
sanitizeInboundSystemTags; forged system lines are neutralized at the
system-event queue, the single chokepoint feeding the System:-per-line render.
- regenerate docs_map and the plugin SDK API baseline manifest.
* fix(prompt): harden inbound context label migration and drop in-band sanitizer
Review follow-ups on the plain-label + provenance-marker change:
- Remove src/security/system-tags.ts. Rewriting inbound text to neutralize
look-alike `System:`/`[System]` markers corrupted legitimate user text and is
not a real injection boundary; role separation plus external-content wrapping
is. Explicit product decision, recorded at the system-event queue.
- Narrow the LanceDB legacy-row purge so it cannot delete benign memories. It
now requires a complete known legacy sentinel line, a legacy label followed by
a fenced JSON body, or the complete legacy external-content header. The prior
predicates matched ordinary prose such as `Notes (untrusted metadata):`, and
deletion is irreversible.
- Make explicit-empty canonical ChannelStructuredContext win over the deprecated
alias via a present/absent result instead of collapsing `[]` to undefined.
- Keep `\r?` in the active-memory doctor rule. It is the only rule spanning the
header's line break, migrated assistant rows skip newline normalization, and
without it the marked-header replace wins and the body strips to empty. Added
a CRLF regression test.
- Fix stale comments that described removed behavior, and cover the Swift
prose-block strip path.
Claude-Session: https://claude.ai/code/session_01WNzsPddQmxy9Y7jKD4wAxH
* test: consolidate OpenClaw test state fixtures
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test: use SDK test-state seam in extensions
Route bundled extension suites through the focused repo-local Plugin SDK test-state entrypoint and remove the Codex projector harness exports made stale by fixture consolidation. Keep the seam out of production builds and published package artifacts while auditing its real consumers in the full-tree deadcode scan.
* test(plugins): map test-state in package boundaries
* test(telegram): consolidate bot test harness
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test(telegram): use public test-state seam
* test(plugin-sdk): keep test state local-only
Match the existing channel test-helper boundary: bundled extension tests can import the focused SDK source entrypoint, while ordinary builds and the published npm package exclude it. The earlier public classification existed only on this unmerged PR branch and was never a shipped contract.
* test(plugins): map test-state in package boundaries
* refactor(channels): remove flat streaming compat
maintainer-approved early removal of v2026.7.2-gated compat
* refactor(channels): remove group intro hint adapter
maintainer-approved early removal of v2026.7.2-gated compat
Plugin SDK surface baseline update is maintainer-approved for this intentional removal.