Skip to content

fix(telegram): unify transport fallback chain#49148

Merged
obviyus merged 4 commits intomainfrom
codex/telegram-distill-fallback-chain
Mar 17, 2026
Merged

fix(telegram): unify transport fallback chain#49148
obviyus merged 4 commits intomainfrom
codex/telegram-distill-fallback-chain

Conversation

@obviyus
Copy link
Copy Markdown
Contributor

@obviyus obviyus commented Mar 17, 2026

Summary

Supersedes #48812 with a simpler cut.

Before:

  • resolveTelegramTransport() had one sticky IPv4 fallback path for normal API calls.
  • Media downloads consumed a separate pinnedDispatcherPolicy + fallbackPinnedDispatcherPolicy pair.
  • Adding a third Telegram fallback tier in only one path forced more booleans and duplicated control flow.

Insight:

  • These are not different cases.
  • Telegram transport wants one ordered retry chain, shared by both API calls and media downloads.
  • The third tier should be data in that chain, not a special branch in one caller.

After:

  • Replace the split primary/fallback transport shape with one ordered dispatcher-attempt chain.
  • Keep one sticky attempt index instead of separate fallback booleans.
  • Add pinned-host override support to PinnedDispatcherPolicy so the Telegram fallback IP is represented in the policy, not as bespoke runtime state.
  • Reuse the same exported attempt chain for media downloads, so polling/send/download paths stay aligned.

Testing

  • pnpm test -- extensions/telegram/src/fetch.test.ts src/media/fetch.telegram-network.test.ts src/infra/net/ssrf.dispatcher.test.ts extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts src/media/fetch.test.ts
  • pnpm tsgo

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR replaces the dual pinnedDispatcherPolicy / fallbackPinnedDispatcherPolicy shape on TelegramTransport with a single ordered dispatcherAttempts chain, and introduces a third fallback tier (pinned Telegram IP 149.154.167.220) alongside the existing IPv4-only fallback. A single stickyAttemptIndex replaces the old separate boolean flags. The same exported attempt chain is now reused by both API calls (resolveTelegramFetch) and media downloads (fetchRemoteMedia), eliminating duplicated control flow.

Key changes:

  • TelegramTransport.dispatcherAttempts replaces the two-field primary/fallback policy shape; callers pass the whole chain instead of individual policies.
  • createTelegramTransportAttempts encodes the three-tier retry chain as data rather than branching logic.
  • PinnedDispatcherPolicy gains an optional pinnedHostname override so IP pinning is expressed inside the policy instead of as bespoke runtime state.
  • createPinnedHostnameLookup (in fetch.ts) handles the per-tier DNS override at the dispatcher level.
  • fetchRemoteMedia generalises from a fixed primary + one fallback to an N-attempt loop using the shared FetchDispatcherAttempt type.
  • Two issues worth noting: (1) with 3+ attempts in fetchRemoteMedia, only the first and last errors are preserved in the combined error — the middle attempt's error is silently dropped; (2) createPinnedHostnameLookup has no guard if addresses is empty, relying entirely on a caller-side invariant.

Confidence Score: 4/5

  • Safe to merge — the refactor is well-scoped, tested, and the two issues found are minor/defensive rather than correctness-breaking for the current call sites.
  • The structural change is clean and the tests cover the new three-tier fallback including stickiness. The one P1 issue (intermediate error loss in fetchRemoteMedia) is a debugging regression rather than a functional bug — the chain still retries correctly and throws on exhaustion. The missing guard in createPinnedHostnameLookup is protected by an external invariant. No security or data-loss risks identified.
  • src/media/fetch.ts (error accumulation logic in the retry loop) and extensions/telegram/src/fetch.ts (createPinnedHostnameLookup with no empty-address guard).
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/media/fetch.ts
Line: 155-156

Comment:
**Intermediate attempt errors silently discarded in 3-attempt chains**

`primaryError ??= err` only records the first failure. With the new three-tier chain (default → IPv4-only → pinned IP), the second attempt's error (IPv4-only) is never stored anywhere: `primaryError` already holds attempt 0's error and is never updated after that.

When the chain exhausts, the combined error reports `primaryError = err[0]` and `cause = err[2]`, silently dropping `err[1]`. For debugging a multi-tier Telegram connectivity failure this can obscure the real sequence.

A simple fix is to accumulate all errors, for example as a chain:

```typescript
// instead of primaryError ??= err:
primaryError = primaryError === undefined ? err : new Error("retry attempt failed", { cause: err });
```

Or, if the combined-error message only ever needs to mark "there was a first error and a last error", the current shape is at least documented — but it should have a comment explaining the intentional loss of middle attempts.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 160-165

Comment:
**No guard for empty `addresses` in `createPinnedHostnameLookup`**

`selected[0]` can be `undefined` when `pinnedHostname.addresses` is an empty array, causing `first.address` to throw at runtime. Today this is prevented by the external guard in `createTelegramTransportAttempts`:

```typescript
if (TELEGRAM_FALLBACK_IPS.length === 0) {
  return attempts;
}
```

…but `createPinnedHostnameLookup` accepts any `PinnedDispatcherPolicy["pinnedHostname"]` and has no enforcement on its own. A caller that passes an empty `addresses` list (e.g. from a `PinnedHostnameOverride` with `addresses: []`) would get an opaque crash on the first DNS resolution.

Consider adding a guard before the `selected[0]` dereference:

```suggestion
    const first = selected[0];
    if (!first) {
      resolvedCallback(new Error(`No usable address for pinned hostname ${pinnedHostname.hostname}`) as unknown as null, "", 0);
      return;
    }
    (resolvedCallback as (err: null, address: string, family: number) => void)(
      null,
      first.address,
      first.family,
    );
```

Or at minimum a non-null assertion with an explanatory comment pointing at the invariant enforced by the caller.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/media/fetch.ts
Line: 158-160

Comment:
**Unreachable defensive guard**

After the loop, `result` is either set (successful attempt + `break`) or an error was thrown. Since `attempts` is guaranteed non-empty (the fallback `[{ dispatcherPolicy: undefined, lookupFn }]` ensures at least one element), the `if (!result)` branch can never execute. This makes `result` typed as `... | undefined` when in practice it's always defined at this point, which forces the `!result` check to satisfy TypeScript rather than guard against a real case.

Replacing `let result: ... | undefined` with a typed `let result!: ...` (definite-assignment assertion) or restructuring to assign at the point of success would make the intent clearer and remove the dead branch.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 2d71d48

@obviyus obviyus force-pushed the codex/telegram-distill-fallback-chain branch from 2d71d48 to efbeef1 Compare March 17, 2026 16:57
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d71d4861c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@obviyus obviyus force-pushed the codex/telegram-distill-fallback-chain branch from 8389e56 to ea0a1a7 Compare March 17, 2026 17:12
@obviyus obviyus merged commit e4825a0 into main Mar 17, 2026
9 checks passed
@obviyus obviyus deleted the codex/telegram-distill-fallback-chain branch March 17, 2026 17:14
@obviyus
Copy link
Copy Markdown
Contributor Author

obviyus commented Mar 17, 2026

Landed on main.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea0a1a7853

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +379 to +382
if (!normalizedOverrideHost || normalizedOverrideHost !== pinned.hostname) {
throw new Error(
`Pinned dispatcher override hostname mismatch: expected ${pinned.hostname}, got ${override.hostname}`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip override mismatch errors on redirected hosts

The new hostname equality check throws whenever dispatcherPolicy.pinnedHostname.hostname differs from the currently resolved host, but fetchWithSsrFGuard reuses the same dispatcher policy across redirect hops. In the new Telegram fallback chain, if the pinned-IP attempt receives a redirect to another host (for example a CDN hostname), this branch throws before the redirected request is attempted, so the final fallback can fail even though the redirect target is otherwise allowed and reachable.

Useful? React with 👍 / 👎.

mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 17, 2026
* main: (142 commits)
  fix(zalouser): fix setup-only onboarding flow (openclaw#49219)
  CI: add built plugin singleton smoke (openclaw#48710)
  update contributing focus areas
  docs(providers): clarify provider capabilities vs public capability model
  docs(refactor): align plugin SDK plan with public capability model
  docs(cli): add plugins inspect command reference
  docs(plugins): document public capability model, plugin shapes, and inspection
  Plugins: internalize diagnostics OTel imports
  Plugins: internalize diffs SDK imports
  Plugins: internalize more extension SDK imports
  Plugins: add local extension API barrels
  Plugins: add inspect matrix and trim export
  Plugins: add inspect command and capability report
  fix(telegram): unify transport fallback chain (openclaw#49148)
  Plugins: add binding resolution callbacks (openclaw#48678)
  fix(gateway): clear trusted-proxy control ui scopes
  refactor: narrow extension public seams
  test: stabilize memory async search close
  docs(hooks): clarify trust model and audit guidance
  feat(mattermost): add retry logic and timeout handling for DM channel creation (openclaw#42398)
  ...
nikolaisid pushed a commit to nikolaisid/openclaw that referenced this pull request Mar 18, 2026
* fix(telegram): unify transport fallback chain

* fix: address telegram fallback review comments

* fix: validate pinned SSRF overrides

* fix: unify telegram fallback retries (openclaw#49148)
ssfdre38 pushed a commit to ssfdre38/openclaw-community-edition that referenced this pull request Mar 18, 2026
* fix(telegram): unify transport fallback chain

* fix: address telegram fallback review comments

* fix: validate pinned SSRF overrides

* fix: unify telegram fallback retries (openclaw#49148)
brandontyler pushed a commit to brandontyler/clawdbot that referenced this pull request Mar 19, 2026
* fix(telegram): unify transport fallback chain

* fix: address telegram fallback review comments

* fix: validate pinned SSRF overrides

* fix: unify telegram fallback retries (openclaw#49148)
pholpaphankorn pushed a commit to pholpaphankorn/openclaw that referenced this pull request Mar 22, 2026
* fix(telegram): unify transport fallback chain

* fix: address telegram fallback review comments

* fix: validate pinned SSRF overrides

* fix: unify telegram fallback retries (openclaw#49148)
@obviyus obviyus self-assigned this Mar 23, 2026
ralyodio pushed a commit to ralyodio/openclaw that referenced this pull request Apr 3, 2026
* fix(telegram): unify transport fallback chain

* fix: address telegram fallback review comments

* fix: validate pinned SSRF overrides

* fix: unify telegram fallback retries (openclaw#49148)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant