feat(cli): add telegram channel adapter, scoped config, and diagnostics (replacement)#115
feat(cli): add telegram channel adapter, scoped config, and diagnostics (replacement)#115cdenneen wants to merge 81 commits intohappier-dev:devfrom
Conversation
|
@coderabbitai review |
|
@greptile review |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a Channel Bridge subsystem: provider-agnostic worker runtime, Telegram adapter and webhook relay, server KV transport and cache-backed binding store, scoped account config and overlays, CLI and daemon wiring, diagnostics/redaction utilities, extensive tests, and documentation including a Telegram guide and README "Channel Integrations" entry. Changes
Sequence Diagram(s)sequenceDiagram
participant Adapter as Telegram Adapter
participant Worker as Channel Bridge Worker
participant Store as Binding Store
participant KV as Server KV
participant Session as Session Service
rect rgba(0, 100, 200, 0.5)
Note over Worker: Tick begins
Adapter->>Worker: pullInboundMessages()
Worker->>Worker: deduplicateInbound()
alt Slash command (attach/session)
Worker->>Store: upsertBinding(conversation→session)
Store->>KV: write(binding)
else User message
Worker->>Store: getBinding(conversation)
Store->>KV: read(binding)
Worker->>Session: forwardUserMessage(sessionId, text)
Session-->>Worker: responseRows
Worker->>Adapter: sendMessage(conversation, agentText)
Adapter-->>Worker: deliveryAck
Worker->>Store: updateLastForwardedSeq(binding)
Store->>KV: write(binding.lastForwardedSeq)
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Greptile SummaryThis PR adds a full Telegram Channel Bridge to the Happier CLI: a provider-agnostic bridge worker ( The implementation is substantial (~12k lines added) and has received extensive prior review. Many hard correctness issues — permanent-delivery cursor stalls, reply-guard hardening, surrogate-safe truncation, typed error contracts, loopback validation at every write path, and daemon shutdown races — were addressed across multiple fix commits. Three issues remain unaddressed:
Confidence Score: 2/5
Last reviewed commit: 74ae08b |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (9)
apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts (1)
139-142: Loosen this warning assertion to a stable substring/prefix.This pins the full log copy, so harmless rewording will fail the test even if shutdown behavior is still correct. Assert on the error object plus a key substring or prefix instead. As per coding guidelines, "Avoid brittle 'content policing' tests that pin default config values, exact Markdown wording/format/length, or user-facing copy unless copy itself is the product requirement. Prefer stable identifiers, error codes/types, shapes, statuses, and key substrings."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts` around lines 139 - 142, The test pins the full log message in the assertion for warnSpy; replace the exact-string assertion in the expect(warnSpy).toHaveBeenCalledWith(...) call with a looser check that verifies the Error object and a stable substring or prefix (e.g., use expect.stringContaining('Error stopping webhook relay') or a regex like expect.stringMatching(/^\\[channelBridge\\] Error stopping webhook relay/)) so the assertion still checks the right log intent without brittle copy-level matching; update the assertion in the startupFailure test that currently references warnSpy accordingly.apps/cli/src/persistence.ts (1)
99-102: ReplacechannelBridge: unknownwith a typed settings envelope.This persisted subtree is now a first-class part of
Settings, and leaving it asunknownis already forcing downstreamas anyaccess in the new channel-bridge tests/helpers. Even a narrow interface for the top-level shape would keep settings reads/writes type-safe without freezing every provider-specific field. As per coding guidelines, "No untyped code in TypeScript - use strict typing with explicit parameter and return types" and "Preferinterfaceovertypefor defining object shapes in TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/persistence.ts` around lines 99 - 102, The channelBridge field currently typed as unknown should be replaced with a concrete settings envelope interface to avoid downstream any casts: define an interface (e.g., ChannelBridgeSettingsEnvelope) describing the top-level persisted shape (optional provider-specific nested fields allowed) and use it for the channelBridge property on the Settings/persistence type instead of unknown; update any reads/writes (places referencing channelBridge) to use the new interface so TypeScript enforces proper typing while still allowing provider-specific optional entries.apps/cli/src/channels/startChannelBridgeWorker.test.ts (1)
440-442: Assert the warning by stable substrings, not the full sentence.This locks the test to user-facing copy, so harmless wording tweaks will fail it. Matching the invariant fragments is enough to keep the fallback behavior covered.
♻️ Suggested assertion
- expect(warnSpy).toHaveBeenCalledWith( - '[channelBridge] Telegram webhook.enabled=true but webhook.secret is missing; falling back to polling mode', - ); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringMatching( + /webhook\.enabled=true.*webhook\.secret is missing.*falling back to polling mode/i, + ), + );Based on learnings "Avoid brittle 'content policing' tests that pin default config values, exact Markdown wording/format/length, or user-facing copy unless copy itself is the product requirement. Prefer stable identifiers, error codes/types, shapes, statuses, and key substrings."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/startChannelBridgeWorker.test.ts` around lines 440 - 442, The test currently asserts the full warning message string via warnSpy; change it to assert only stable substrings so wording tweaks don't break the test—update the assertion in startChannelBridgeWorker.test to use jest matchers (e.g. expect.stringContaining) with key fragments such as 'Telegram', 'webhook.enabled=true', and 'webhook.secret' when asserting warnSpy instead of the entire sentence.apps/cli/src/channels/channelBridgeConfig.ts (1)
1-1: Add the missing file/API JSDoc for the precedence rules.This module is now the public entry point for env/global/server/account channel-bridge resolution, but the file and exported resolver do not document that merge order or the runtime defaults. A short header plus resolver doc block would make future config changes much safer.
As per coding guidelines "Include comprehensive JSDoc comments as file header comments explaining file responsibilities" and "Add JSDoc comments for public APIs and complex logic".
Also applies to: 85-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeConfig.ts` at line 1, Add a short file-level JSDoc header to channelBridgeConfig.ts describing its responsibility as the public entry point for env/global/server/account channel-bridge resolution and listing the merge/precedence rules and runtime defaults; then add a JSDoc block above the exported resolver function (resolveChannelBridgeConfig) documenting the exact merge order (env > global > server > account or whatever the implementation uses), expected input shape (referencing the RecordLike type), return shape, and any default behavior for missing keys, so future changes to precedence are clear and discoverable.apps/cli/src/channels/channelBridgeServerConfigOverlay.ts (1)
18-23: Add JSDoc to the exported overlay helper.This function bakes in a non-obvious merge contract—server KV overlays only non-secret Telegram fields and may intentionally return the original settings object. A short JSDoc would make that behavior much easier to preserve.
As per coding guidelines "Add JSDoc comments for public APIs and complex logic".Suggested doc block
+/** + * Overlays server-KV Telegram fields onto scoped local settings. + * Secrets remain local-only; if the record has no usable non-secret fields, + * the original settings object is returned unchanged. + */ export function overlayServerKvTelegramConfigInSettings(params: Readonly<{🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeServerConfigOverlay.ts` around lines 18 - 23, Add a concise JSDoc to the exported helper overlayServerKvTelegramConfigInSettings describing its merge contract: explain that it overlays only non-secret Telegram fields from ChannelBridgeServerTelegramConfigRecord into the provided settings, that it may return the original settings object unchanged when no overlay is applied, and document the params (settings, serverId, accountId, record) and the unknown return type so callers understand side-effects and when identity is preserved. Include a short note about secrecy handling (secret fields are intentionally not merged) and reference the function name and ChannelBridgeServerTelegramConfigRecord type.apps/cli/src/cli/commands/bridge.test.ts (1)
55-57: Persist the updated settings state in this harness.This mock calls the updater and throws its return value away, so the suite never checks the scoped settings tree that
bridge.tswrites and falls back to asserting mock call order instead. Keep an in-memorysettingsStatehere and assert the resulting object after each command.Suggested harness tweak
+let settingsState: unknown; + beforeEach(async () => { vi.resetModules(); vi.clearAllMocks(); + settingsState = {}; … - updateSettingsMock.mockImplementation(async (updater: (current: unknown) => Promise<unknown> | unknown) => { - await updater({}); - }); + updateSettingsMock.mockImplementation(async (updater: (current: unknown) => Promise<unknown> | unknown) => { + settingsState = await updater(settingsState); + return settingsState; + });Based on learnings "Assert on observable outcomes (return values, state changes, HTTP responses), not internal call sequences."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/cli/commands/bridge.test.ts` around lines 55 - 57, The mock updateSettingsMock currently calls the updater and discards its return so the test never observes the bridge.ts settings changes; create an in-memory settingsState in the test harness and change updateSettingsMock.mockImplementation to call updater with the current settingsState, await its result, and assign the returned value back to settingsState (or merge if updater mutates), then update assertions to check settingsState after each command invocation instead of relying on mock call order—refer to updateSettingsMock, updater, settingsState and the bridge.ts behavior when making the change.apps/cli/src/channels/channelBridgeServerKv.ts (3)
510-548: Add backoff delay between retry attempts.The retry loop has no delay between attempts. Under contention, this could cause rapid-fire retries where multiple concurrent writers repeatedly collide. Adding exponential or jittered backoff would improve resilience.
Proposed fix with exponential backoff
+const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)); + export async function upsertChannelBridgeTelegramConfigInKv(params: Readonly<{ kv: ChannelBridgeKvClient; serverId: string; update: ScopedTelegramBridgeUpdate; }>): Promise<void> { if (!hasNonSecretTelegramConfigUpdate(params.update)) { return; } const key = telegramConfigKvKey(params.serverId); let current: Readonly<{ record: ChannelBridgeServerTelegramConfigRecord | null; version: number; }> | null = null; for (let attempt = 0; attempt < 4; attempt += 1) { + if (attempt > 0) { + await sleep(50 * Math.pow(2, attempt - 1)); // 50ms, 100ms, 200ms + } if (current === null) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeServerKv.ts` around lines 510 - 548, The retry loop in upserting the Telegram config (the for-loop that calls readChannelBridgeTelegramConfigFromKv, applyTelegramConfigUpdate, and writeJsonValue) needs an exponential-with-jitter backoff between attempts to avoid hot-looping on ChannelBridgeKvVersionMismatchError; after catching a ChannelBridgeKvVersionMismatchError and updating current (inside the catch branch that sets conflictRecord/current), compute a delay based on attempt (e.g., baseDelay * 2**attempt with random jitter) and await that delay before continuing the loop, ensuring you do not block the event loop (use a promise-based sleep) and keep the maximum delay capped to a sensible upper bound.
564-583: Apply the same backoff pattern here.This retry loop has the same no-backoff concern as
upsertChannelBridgeTelegramConfigInKv. Apply consistent backoff logic across both retry loops.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeServerKv.ts` around lines 564 - 583, The retry loop that calls writeJsonValue should include the same exponential/backoff delay used in upsertChannelBridgeTelegramConfigInKv: when a transient failure occurs (i.e., in the catch branch that does not throw ChannelBridgeKvVersionMismatchError), wait before retrying, and increase delay per attempt; keep the existing special-case handling for ChannelBridgeKvVersionMismatchError (return when currentVersion < 0 and update expectedVersion otherwise). Update the loop around writeJsonValue to sleep/backoff between attempts (using the same backoff helper or pattern used by upsertChannelBridgeTelegramConfigInKv) so retries are spaced instead of immediate.
21-70: Consider usinginterfaceinstead oftypefor object shapes.Per coding guidelines,
interfaceis preferred overtypefor defining object shapes. This provides better error messages and supports declaration merging when needed.Example conversion for one type
-export type ChannelBridgeServerTelegramConfigRecord = Readonly<{ - schemaVersion: 1; - tickMs?: number; - telegram: Readonly<{ - allowedChatIds?: string[]; - requireTopics?: boolean; - webhook?: Readonly<{ - enabled?: boolean; - host?: string; - port?: number; - }>; - }>; - updatedAtMs: number; -}>; +export interface ChannelBridgeServerTelegramConfigRecord { + readonly schemaVersion: 1; + readonly tickMs?: number; + readonly telegram: Readonly<{ + allowedChatIds?: string[]; + requireTopics?: boolean; + webhook?: Readonly<{ + enabled?: boolean; + host?: string; + port?: number; + }>; + }>; + readonly updatedAtMs: number; +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeServerKv.ts` around lines 21 - 70, Several exported object-shaped type aliases should be converted to interfaces per the coding guideline; replace the type aliases ChannelBridgeServerTelegramConfigRecord, ChannelBridgeServerBindingRecord, ChannelBridgeServerBindingsDocument, ChannelBridgeKvGetResponse, ChannelBridgeKvMutateResponse, ChannelBridgeKvMutation, and ChannelBridgeKvClient with equivalent interface declarations, preserving readonly fields and optional properties (use readonly modifiers and ? where used) and keep method signatures exactly for get/mutate on ChannelBridgeKvClient; ensure exported names and structural shape remain identical so callers are unaffected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/cli/src/channels/channelBridgeAccountConfig.ts`:
- Around line 229-240: The code unconditionally calls ensureRecord() for
providers and telegram, materializing a Telegram node even for tickMs-only
updates; update the logic in the channelBridge/account update path (around
channelBridge, byServerId, serverScope, byAccountId, accountScope) to only
create the providers subtree (and the telegram record) when the incoming
params.update contains Telegram-related fields (e.g., token or other provider
properties) or otherwise leave providers undefined; alternatively ensure
readScopedTelegramBridgeConfig() treats an empty providers.telegram node as
unconfigured—preferably perform a check for Telegram fields in params.update
before calling ensureRecord(providers,'telegram') so tickMs-only updates do not
create the provider node.
In `@apps/cli/src/channels/startChannelBridgeWorker.ts`:
- Around line 196-219: Both resolveLatestSessionSeq and
fetchAgentMessagesAfterSeq assume encrypted transcripts by calling
fetchEncryptedTranscriptPageLatest / fetchEncryptedTranscriptPageAfterSeq and
decryptTranscriptRows; change them to branch on the session's runtime.mode (use
resolveSessionContext(sessionId) or ctx.runtime.mode) and: for mode 'encrypted'
keep the current encrypted fetch+decrypt path, but for mode 'plain' call the
plain transcript fetchers (or the non-encrypted equivalents) and skip
decryptTranscriptRows — directly filter/map content.t/plain messages and
extractAssistantText. Ensure resolveLatestSessionSeq also acquires the session
context first and uses the mode to pick the correct fetch function and seq
parsing so plain sessions are handled without decryption.
In `@apps/cli/src/channels/telegram/telegramAdapter.ts`:
- Around line 176-185: The process-local update cursor (updateOffset) and the
5-minute TTL deduper (createChannelBridgeInboundDeduper) leave a window where
Telegram can replay updates after daemon restarts; persist and restore the
polling cursor or extend the deduper TTL to match Telegram's retention. Modify
the Telegram adapter to store updateOffset (and selfBotId if needed) to durable
storage on each successful poll/ack and reload it on startup, or increase the
deduper TTL in createChannelBridgeInboundDeduper to cover Telegram's retention
window (whichever strategy you choose), ensuring the adapter uses the persisted
offset before calling getUpdates so duplicates are not reprocessed after
restarts.
In `@apps/cli/src/channels/telegram/telegramWebhookRelay.ts`:
- Around line 33-36: The code currently sets secretHeaderToken =
String(params.secretHeaderToken ?? secretPathToken).trim(), which falls back to
the path token and nullifies the header check; change it to require an explicit
header token by using only params.secretHeaderToken (e.g.,
String(params.secretHeaderToken).trim()) and throw the existing Error if it's
empty, or alternatively derive/store a distinct header secret instead of reusing
secretPathToken; update references to secretHeaderToken,
params.secretHeaderToken, and secretPathToken accordingly so the header cannot
be satisfied by leaking the path token.
In `@apps/cli/src/cli/commands/bridge.ts`:
- Around line 257-275: The local settings are being persisted (via
updateSettings + upsertScopedTelegramBridgeConfig) before the server KV write
completes, causing possible divergence because split.localUpdate still contains
shared fields; change the flow to first perform the shared KV operation (use
hasSharedTelegramBridgeUpdate -> createAxiosChannelBridgeKvClient ->
upsertChannelBridgeTelegramConfigInKv) and only after it succeeds perform
updateSettings with a sanitized local-only payload (derive local-only update
from split via splitScopedTelegramBridgeUpdate or strip shared fields), and add
a compensating rollback: if the subsequent updateSettings fails after a
successful KV write, retry the settings write and if it continues to fail,
revert the KV change (or mark a reconciliation flag) so server and local remain
consistent; apply same change to the other block referenced (lines ~297-309).
In `@apps/cli/src/cli/dispatch.ts`:
- Around line 49-50: The guard that blocks disallowed commands uses subcommand
=== args[0] which fails when global flags like --tmux appear first; update the
logic that constructs subcommand (or add a small normalization step before the
disallowed check) to strip global flags/options from args and find the first
non-flag token (e.g., first arg not starting with '-' or matched against known
global flags) and assign that to subcommand before testing against the
disallowed Set; change the check around the disallowed Set (the variable named
disallowed and the existing if (subcommand && disallowed.has(subcommand))) to
use the normalized/resolved command so "bridge" is caught even when "--tmux" is
passed first.
In `@apps/cli/src/daemon/startDaemon.ts`:
- Around line 1154-1205: Current bridge startup (calls to readSettings,
readChannelBridgeTelegramConfigFromKv and startChannelBridgeFromEnv) runs
synchronously on the daemon readiness path and can block
ensureMachineRegistered; move the bridge bootstrap off the critical path by
invoking it after ensureMachineRegistered or as a fire-and-forget background
task with its own short timeout and error handling: remove awaits that block
readiness and instead spawn a detached promise (or schedule via setTimeout) that
performs readSettings, optional KV fetch (readChannelBridgeTelegramConfigFromKv)
and startChannelBridgeFromEnv while logging errors via
serializeAxiosErrorForLog; keep the same inputs (credentials, serverId,
accountId, settings) and preserve the existing catch/log-return-null logic
inside that detached task so daemon registration and writing of
daemon.state.json are not gated on bridge startup.
In `@apps/cli/src/ui/doctor.ts`:
- Around line 130-167: The redactSettingsForDisplay function only redacts
providers under channelBridge.byServerId[*].byAccountId[*].providers; extend it
to also walk and redact provider entries in channelBridge.providers (global
scope) and channelBridge.byServerId[*].providers (server scope) using the same
logic that replaces secrets.*, botToken, and webhook.secret with '<redacted>'
(refer to channelBridge, byServerId, byAccountId, providers, secrets, botToken,
webhook.secret to locate code paths), and add regression tests that exercise
global (channelBridge.providers) and server-scoped
(channelBridge.byServerId[*].providers) provider redaction so the CLI output
never prints raw credentials.
In `@docs/channel-bridge-uat.md`:
- Around line 83-85: Update the webhook auth section to explicitly document both
failure statuses: state that a header-token (Authorization or custom header)
mismatch returns 401, while an incorrect path token (path-based secret) returns
404; replace the phrase "typically 404 with path-token mismatch" with clear
sentences distinguishing the two cases and keep the existing steps about
changing allowedChatIds and using --allow-all unchanged.
In `@docs/channel-bridge.md`:
- Around line 53-57: The phrase "follow-up stacked PRs" is stale; update the
paragraph under the "Out of scope for this PR" section to remove that wording
and instead point readers to the companion docs that now cover the Telegram
adapter, scoped config sources, and bootstrapping/runtime details; specifically,
replace the sentence referencing "follow-up stacked PRs" with a concise pointer
to the companion documentation (or list the included topics) so readers know
where to find transport details, bridge runtime bootstrapping, scoped config
sources, and server-relay deployment modes.
In `@docs/README.md`:
- Around line 12-13: The docs index (README) is missing the new UAT guide entry;
update the docs index list in README.md to include "channel-bridge-uat.md"
alongside the existing entries (e.g., near "channel-bridge.md" and
"telegram-channel-bridge.md") so the new UAT validation flow is discoverable by
readers.
---
Nitpick comments:
In `@apps/cli/src/channels/channelBridgeConfig.ts`:
- Line 1: Add a short file-level JSDoc header to channelBridgeConfig.ts
describing its responsibility as the public entry point for
env/global/server/account channel-bridge resolution and listing the
merge/precedence rules and runtime defaults; then add a JSDoc block above the
exported resolver function (resolveChannelBridgeConfig) documenting the exact
merge order (env > global > server > account or whatever the implementation
uses), expected input shape (referencing the RecordLike type), return shape, and
any default behavior for missing keys, so future changes to precedence are clear
and discoverable.
In `@apps/cli/src/channels/channelBridgeServerConfigOverlay.ts`:
- Around line 18-23: Add a concise JSDoc to the exported helper
overlayServerKvTelegramConfigInSettings describing its merge contract: explain
that it overlays only non-secret Telegram fields from
ChannelBridgeServerTelegramConfigRecord into the provided settings, that it may
return the original settings object unchanged when no overlay is applied, and
document the params (settings, serverId, accountId, record) and the unknown
return type so callers understand side-effects and when identity is preserved.
Include a short note about secrecy handling (secret fields are intentionally not
merged) and reference the function name and
ChannelBridgeServerTelegramConfigRecord type.
In `@apps/cli/src/channels/channelBridgeServerKv.ts`:
- Around line 510-548: The retry loop in upserting the Telegram config (the
for-loop that calls readChannelBridgeTelegramConfigFromKv,
applyTelegramConfigUpdate, and writeJsonValue) needs an exponential-with-jitter
backoff between attempts to avoid hot-looping on
ChannelBridgeKvVersionMismatchError; after catching a
ChannelBridgeKvVersionMismatchError and updating current (inside the catch
branch that sets conflictRecord/current), compute a delay based on attempt
(e.g., baseDelay * 2**attempt with random jitter) and await that delay before
continuing the loop, ensuring you do not block the event loop (use a
promise-based sleep) and keep the maximum delay capped to a sensible upper
bound.
- Around line 564-583: The retry loop that calls writeJsonValue should include
the same exponential/backoff delay used in
upsertChannelBridgeTelegramConfigInKv: when a transient failure occurs (i.e., in
the catch branch that does not throw ChannelBridgeKvVersionMismatchError), wait
before retrying, and increase delay per attempt; keep the existing special-case
handling for ChannelBridgeKvVersionMismatchError (return when currentVersion < 0
and update expectedVersion otherwise). Update the loop around writeJsonValue to
sleep/backoff between attempts (using the same backoff helper or pattern used by
upsertChannelBridgeTelegramConfigInKv) so retries are spaced instead of
immediate.
- Around line 21-70: Several exported object-shaped type aliases should be
converted to interfaces per the coding guideline; replace the type aliases
ChannelBridgeServerTelegramConfigRecord, ChannelBridgeServerBindingRecord,
ChannelBridgeServerBindingsDocument, ChannelBridgeKvGetResponse,
ChannelBridgeKvMutateResponse, ChannelBridgeKvMutation, and
ChannelBridgeKvClient with equivalent interface declarations, preserving
readonly fields and optional properties (use readonly modifiers and ? where
used) and keep method signatures exactly for get/mutate on
ChannelBridgeKvClient; ensure exported names and structural shape remain
identical so callers are unaffected.
In `@apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts`:
- Around line 139-142: The test pins the full log message in the assertion for
warnSpy; replace the exact-string assertion in the
expect(warnSpy).toHaveBeenCalledWith(...) call with a looser check that verifies
the Error object and a stable substring or prefix (e.g., use
expect.stringContaining('Error stopping webhook relay') or a regex like
expect.stringMatching(/^\\[channelBridge\\] Error stopping webhook relay/)) so
the assertion still checks the right log intent without brittle copy-level
matching; update the assertion in the startupFailure test that currently
references warnSpy accordingly.
In `@apps/cli/src/channels/startChannelBridgeWorker.test.ts`:
- Around line 440-442: The test currently asserts the full warning message
string via warnSpy; change it to assert only stable substrings so wording tweaks
don't break the test—update the assertion in startChannelBridgeWorker.test to
use jest matchers (e.g. expect.stringContaining) with key fragments such as
'Telegram', 'webhook.enabled=true', and 'webhook.secret' when asserting warnSpy
instead of the entire sentence.
In `@apps/cli/src/cli/commands/bridge.test.ts`:
- Around line 55-57: The mock updateSettingsMock currently calls the updater and
discards its return so the test never observes the bridge.ts settings changes;
create an in-memory settingsState in the test harness and change
updateSettingsMock.mockImplementation to call updater with the current
settingsState, await its result, and assign the returned value back to
settingsState (or merge if updater mutates), then update assertions to check
settingsState after each command invocation instead of relying on mock call
order—refer to updateSettingsMock, updater, settingsState and the bridge.ts
behavior when making the change.
In `@apps/cli/src/persistence.ts`:
- Around line 99-102: The channelBridge field currently typed as unknown should
be replaced with a concrete settings envelope interface to avoid downstream any
casts: define an interface (e.g., ChannelBridgeSettingsEnvelope) describing the
top-level persisted shape (optional provider-specific nested fields allowed) and
use it for the channelBridge property on the Settings/persistence type instead
of unknown; update any reads/writes (places referencing channelBridge) to use
the new interface so TypeScript enforces proper typing while still allowing
provider-specific optional entries.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 759a4d35-8071-4de3-9196-26ffee860ae8
⛔ Files ignored due to path filters (3)
docs/assets/doctor-critical-summary.pngis excluded by!**/*.pngdocs/assets/doctor-missing-host-port-summary.pngis excluded by!**/*.pngyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (33)
README.mdapps/cli/src/channels/channelBindingStore.server.test.tsapps/cli/src/channels/channelBindingStore.server.tsapps/cli/src/channels/channelBridgeAccountConfig.test.tsapps/cli/src/channels/channelBridgeAccountConfig.tsapps/cli/src/channels/channelBridgeConfig.test.tsapps/cli/src/channels/channelBridgeConfig.tsapps/cli/src/channels/channelBridgeServerConfigOverlay.test.tsapps/cli/src/channels/channelBridgeServerConfigOverlay.tsapps/cli/src/channels/channelBridgeServerKv.test.tsapps/cli/src/channels/channelBridgeServerKv.tsapps/cli/src/channels/core/channelBridgeWorker.test.tsapps/cli/src/channels/core/channelBridgeWorker.tsapps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.tsapps/cli/src/channels/startChannelBridgeWorker.test.tsapps/cli/src/channels/startChannelBridgeWorker.tsapps/cli/src/channels/telegram/telegramAdapter.test.tsapps/cli/src/channels/telegram/telegramAdapter.tsapps/cli/src/channels/telegram/telegramWebhookRelay.test.tsapps/cli/src/channels/telegram/telegramWebhookRelay.tsapps/cli/src/cli/commandRegistry.tsapps/cli/src/cli/commands/bridge.test.tsapps/cli/src/cli/commands/bridge.tsapps/cli/src/cli/dispatch.tsapps/cli/src/daemon/startDaemon.automation.integration.test.tsapps/cli/src/daemon/startDaemon.tsapps/cli/src/persistence.tsapps/cli/src/ui/doctor.test.tsapps/cli/src/ui/doctor.tsdocs/README.mddocs/channel-bridge-uat.mddocs/channel-bridge.mddocs/telegram-channel-bridge.md
|
Addressed the latest review items in commit 8514428. Highlights:
Targeted regression tests passed locally:
@coderabbitai review |
|
@greptile review |
|
🧠 Learnings used✅ Actions performedReview triggered.
|
|
Added a follow-up fix in commit 1714afa to address daemon startup coupling feedback. Change:
Validation rerun after this patch:
All passed. @coderabbitai review |
|
@greptile review |
|
🧠 Learnings used✅ Actions performedReview triggered.
|
|
Addressed the latest Greptile findings in commit a264754 and resolved the corresponding threads. Summary:
@coderabbitai review |
|
@greptile review |
|
Triggering an incremental review of commit 🧠 Learnings used✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
apps/cli/src/cli/commands/bridge.ts (1)
266-282:⚠️ Potential issue | 🟠 MajorHandle the server-ahead-of-local rollback path too.
The ordering fix removed the local-ahead-of-server case, but these flows still diverge if the KV mutation succeeds and the subsequent
updateSettings()call fails. Capture the previous KV state and revert it, or persist an explicit reconciliation marker, before returning an error.Also applies to: 304-316
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/cli/commands/bridge.ts` around lines 266 - 282, When performing the KV upsert in the branch guarded by hasSharedTelegramBridgeUpdate (using createAxiosChannelBridgeKvClient and upsertChannelBridgeTelegramConfigInKv) you must capture the previous KV value before writing and, if the subsequent updateSettings call that runs upsertScopedTelegramBridgeConfig fails, revert the KV to that previous value (or write an explicit reconciliation marker) and then propagate the error; apply the same pattern in the other similar block that does a KV upsert followed by updateSettings so you avoid the server-ahead-of-local divergence.
🧹 Nitpick comments (2)
apps/cli/src/cli/commands/bridge.test.ts (1)
19-38: Run at least one bridge-command happy path against real persistence.Right now the suite mostly proves mocked call ordering. Because
@/persistenceis stubbed, it will not catch regressions in the actualsettings.jsonshape or local write behavior that this PR changes. Since you already create a tempHAPPIER_HOME_DIR, one higher-fidelity test using the real persistence module would give much better coverage here.Based on learnings "The ChannelBridgeKvClient is treated as a true system boundary (network/HTTP KV backend), so local in-memory doubles are acceptable here for deterministic unit tests under CI runtime constraints."
Also applies to: 46-68
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/cli/commands/bridge.test.ts` around lines 19 - 38, Remove the '@/persistence' mock (the vi.mock that stubs readCredentials, readSettings, updateSettings) so the test uses the real persistence module and exercise at least one bridge command happy-path against the temp HAPPIER_HOME_DIR already created in the spec; keep the ChannelBridgeKvClient-related mocks (createAxiosChannelBridgeKvClient, upsertChannelBridgeTelegramConfigInKv, clearChannelBridgeTelegramConfigInKv, readChannelBridgeTelegramConfigFromKv) as-is so the KV boundary remains mocked, and ensure the test asserts actual settings.json reads/writes by invoking the code paths that call readSettings/updateSettings (e.g., the bridge command execution flow) to validate real persistence behavior.apps/cli/src/channels/channelBridgeAccountConfig.ts (1)
46-64: Promote the normalized scoped config to a named interface.The exported update aliases, the accumulator literals in
splitScopedTelegramBridgeUpdate(), and theRecordLike | nullreturn fromreadScopedTelegramBridgeConfig()are all describing closely related parts of the same public config surface with anonymous shapes. AScopedTelegramBridgeConfiginterface (plus a nested webhook interface) would make downstream usage type-safe and reduce drift between the read/split paths.As per coding guidelines, "Prefer
interfaceovertypefor defining object shapes in TypeScript" and "Always use types for function parameters and return values in TypeScript."Also applies to: 74-91, 136-205
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeAccountConfig.ts` around lines 46 - 64, The update and shared types (ScopedTelegramBridgeUpdate, SharedTelegramBridgeUpdate) and the anonymous accumulator/return shapes used in splitScopedTelegramBridgeUpdate() and readScopedTelegramBridgeConfig() should be promoted to a named interface (e.g., ScopedTelegramBridgeConfig with a nested WebhookConfig interface) so downstream code gets a single authoritative shape; change the exported type aliases to interfaces, replace the anonymous accumulator literals in splitScopedTelegramBridgeUpdate() with the new interface types, and update readScopedTelegramBridgeConfig() to return the named interface (or null) and use the named webhook interface for webhook fields to keep type-safety and consistency across the read/split paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/cli/src/channels/channelBridgeConfig.ts`:
- Around line 45-52: parseStringArray currently turns an input like
[-1001234567890] into [] which the runtime treats as "allow all"; update
parseStringArray so it preserves numeric chat ids instead of discarding them: in
the mapping step, accept finite numbers by coercing them to strings (e.g., if
typeof entry === 'number' && Number.isFinite(entry) -> String(entry)), keep
trimming for string entries, and treat other types as invalid; then filter out
empty strings and return null only for completely invalid inputs (or keep
returning [] only when the final filtered array is truly empty), ensuring
parseStringArray (and any consumer of allowedChatIds) no longer collapses a
non-empty invalid array into the allow-all sentinel.
In `@apps/cli/src/channels/channelBridgeServerKv.ts`:
- Around line 95-98: The JSON parse in decodeBase64ToJson currently lets
malformed Base64 payloads throw raw SyntaxErrors; update decodeBase64ToJson to
catch Buffer/JSON.parse errors and throw or return a well-typed, recoverable
error (e.g., throw new ChannelBridgePayloadError or return null) so callers like
readChannelBridgeTelegramConfigFromKv (with allowUnsupportedSchema: true),
upsertChannelBridgeTelegramConfigInKv, clearChannelBridgeTelegramConfigInKv and
the bindings decoder can detect corrupted KV payloads and handle them (treat as
recoverable / overwriteable) instead of aborting; ensure the bindings decoder
maps the caught parse error to the channel-bridge payload error type so upstream
code can proceed with recovery.
- Around line 229-266: The parser is currently fabricating defaults for required
persisted fields (lastForwardedSeq, createdAtMs, updatedAtMs) instead of
failing; change the validation in the parsing logic so that if lastForwardedSeq,
createdAtMs, or updatedAtMs are missing or not finite numbers you throw
ChannelBridgeBadPayloadError (do not default to 0 or Date.now()). Keep the
existing type/finite checks but add presence checks (!== undefined) and remove
the fallbacks when building lastForwardedSeq, createdAtMs, and updatedAtMs so
the constructor/record creation for ChannelBridgeServerBindingRecord fails fast
on incomplete rows. Use the same error messages you already create for invalid
fields.
In `@apps/cli/src/channels/telegram/telegramAdapter.ts`:
- Around line 223-245: pullInboundMessages currently removes webhook items from
queuedWebhookUpdates and advances updateOffset as soon as parsing succeeds,
which causes drops if downstream handling fails; change it to only parse and
return the updates (including their original ids or raw update objects) without
mutating queuedWebhookUpdates or updating updateOffset, and add a separate
acknowledgment path (e.g., a new ackInboundMessages(ids) or have the caller call
a provided confirm function) that the worker invokes after attach/binding and
sendUserMessageToSession succeed; reference pullInboundMessages, parseUpdates,
queuedWebhookUpdates, updateOffset, parseHighestUpdateOffset, and
sendUserMessageToSession when making this change so consumption only happens
after successful processing.
In `@apps/cli/src/cli/commands/bridge.ts`:
- Around line 233-237: The current branch sets update.allowedChatIds = []
(meaning "allow all") when parseCsvList(allowedChatIdsRaw) returns an empty
array, which can happen for malformed inputs like ",,,"; change the logic in
bridge.ts so that when allowedChatIdsRaw is provided but
parseCsvList(allowedChatIdsRaw) yields an empty array you fail fast (e.g., throw
a CLI error or exit with a non-zero status) instead of assigning [], keep the
existing allowAll branch (when allowAll is true) that intentionally sets
update.allowedChatIds = [], and reference the symbols allowedChatIdsRaw,
parseCsvList, allowAll, and update.allowedChatIds when making the check and
error path.
In `@apps/cli/src/cli/dispatch.ts`:
- Around line 52-53: The disallowed Set used to guard tmux invocation (variable
disallowed and the if that checks disallowed.has(tmuxSubcommand)) is missing the
controller commands "bug-report" and "self-update", so those still bypass the
--tmux guard; update the Set initializer to include 'bug-report' and
'self-update' so the tmuxSubcommand check correctly rejects them (i.e., extend
the existing new Set([...]) used where disallowed is defined to contain
'bug-report' and 'self-update').
In `@docs/channel-bridge-uat.md`:
- Around line 5-10: The checklist's "clean local state" section only restarts
the daemon; update it to either require switching to a fresh HAPPIER_HOME_DIR or
add explicit cleanup commands: stop the daemon (`happier daemon stop`), remove
or rename the existing settings.json in HAPPIER_HOME_DIR, clear any bridge
config/bindings (delete bridge-related files or run the project-specific
unbind/cleanup command if available), then confirm no active servers (`happier
server list`) and start the daemon (`happier daemon start`) so the steps produce
deterministic results.
---
Duplicate comments:
In `@apps/cli/src/cli/commands/bridge.ts`:
- Around line 266-282: When performing the KV upsert in the branch guarded by
hasSharedTelegramBridgeUpdate (using createAxiosChannelBridgeKvClient and
upsertChannelBridgeTelegramConfigInKv) you must capture the previous KV value
before writing and, if the subsequent updateSettings call that runs
upsertScopedTelegramBridgeConfig fails, revert the KV to that previous value (or
write an explicit reconciliation marker) and then propagate the error; apply the
same pattern in the other similar block that does a KV upsert followed by
updateSettings so you avoid the server-ahead-of-local divergence.
---
Nitpick comments:
In `@apps/cli/src/channels/channelBridgeAccountConfig.ts`:
- Around line 46-64: The update and shared types (ScopedTelegramBridgeUpdate,
SharedTelegramBridgeUpdate) and the anonymous accumulator/return shapes used in
splitScopedTelegramBridgeUpdate() and readScopedTelegramBridgeConfig() should be
promoted to a named interface (e.g., ScopedTelegramBridgeConfig with a nested
WebhookConfig interface) so downstream code gets a single authoritative shape;
change the exported type aliases to interfaces, replace the anonymous
accumulator literals in splitScopedTelegramBridgeUpdate() with the new interface
types, and update readScopedTelegramBridgeConfig() to return the named interface
(or null) and use the named webhook interface for webhook fields to keep
type-safety and consistency across the read/split paths.
In `@apps/cli/src/cli/commands/bridge.test.ts`:
- Around line 19-38: Remove the '@/persistence' mock (the vi.mock that stubs
readCredentials, readSettings, updateSettings) so the test uses the real
persistence module and exercise at least one bridge command happy-path against
the temp HAPPIER_HOME_DIR already created in the spec; keep the
ChannelBridgeKvClient-related mocks (createAxiosChannelBridgeKvClient,
upsertChannelBridgeTelegramConfigInKv, clearChannelBridgeTelegramConfigInKv,
readChannelBridgeTelegramConfigFromKv) as-is so the KV boundary remains mocked,
and ensure the test asserts actual settings.json reads/writes by invoking the
code paths that call readSettings/updateSettings (e.g., the bridge command
execution flow) to validate real persistence behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 34124a2a-e2f8-4707-9851-6fd863a3eb6f
⛔ Files ignored due to path filters (3)
docs/assets/doctor-critical-summary.pngis excluded by!**/*.pngdocs/assets/doctor-missing-host-port-summary.pngis excluded by!**/*.pngyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (34)
README.mdapps/cli/src/channels/channelBindingStore.server.test.tsapps/cli/src/channels/channelBindingStore.server.tsapps/cli/src/channels/channelBridgeAccountConfig.test.tsapps/cli/src/channels/channelBridgeAccountConfig.tsapps/cli/src/channels/channelBridgeConfig.test.tsapps/cli/src/channels/channelBridgeConfig.tsapps/cli/src/channels/channelBridgeServerConfigOverlay.test.tsapps/cli/src/channels/channelBridgeServerConfigOverlay.tsapps/cli/src/channels/channelBridgeServerKv.test.tsapps/cli/src/channels/channelBridgeServerKv.tsapps/cli/src/channels/core/channelBridgeWorker.test.tsapps/cli/src/channels/core/channelBridgeWorker.tsapps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.tsapps/cli/src/channels/startChannelBridgeWorker.test.tsapps/cli/src/channels/startChannelBridgeWorker.tsapps/cli/src/channels/telegram/telegramAdapter.test.tsapps/cli/src/channels/telegram/telegramAdapter.tsapps/cli/src/channels/telegram/telegramWebhookRelay.test.tsapps/cli/src/channels/telegram/telegramWebhookRelay.tsapps/cli/src/cli/commandRegistry.tsapps/cli/src/cli/commands/bridge.test.tsapps/cli/src/cli/commands/bridge.tsapps/cli/src/cli/dispatch.tmuxDisallowed.test.tsapps/cli/src/cli/dispatch.tsapps/cli/src/daemon/startDaemon.automation.integration.test.tsapps/cli/src/daemon/startDaemon.tsapps/cli/src/persistence.tsapps/cli/src/ui/doctor.test.tsapps/cli/src/ui/doctor.tsdocs/README.mddocs/channel-bridge-uat.mddocs/channel-bridge.mddocs/telegram-channel-bridge.md
|
Addressed all current open threads in commit a54e4b5 and resolved each thread with per-item notes. Highlights:
@coderabbitai review |
|
Triggering an incremental review of commit 🧠 Learnings used✅ Actions performedReview triggered.
|
|
@greptile review |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts (1)
24-26:⚠️ Potential issue | 🟡 MinorRestore
exitSpyinafterAllto prevent leakage.
consoleErrorSpyis restored butexitSpyis not. This could cause the mock to leak into subsequent test files, leading to unexpected behavior or test failures in other suites.Suggested fix
afterAll(() => { consoleErrorSpy.mockRestore(); + exitSpy.mockRestore(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts` around lines 24 - 26, The test's afterAll block restores consoleErrorSpy but leaves exitSpy mocked, causing cross-test leakage; update the afterAll to also restore exitSpy (call exitSpy.mockRestore() or use jest.restoreAllMocks()) so the process.exit spy created in this spec is removed; locate the exitSpy reference in this test file (e.g., the variable named exitSpy created with jest.spyOn(process, 'exit')) and add its mockRestore() alongside consoleErrorSpy.mockRestore() inside afterAll.
♻️ Duplicate comments (2)
apps/cli/src/channels/core/channelBridgeWorker.ts (1)
712-736:⚠️ Potential issue | 🟠 MajorDon't ack the inbound event after
sendUserMessageToSession()fails.This catch sends an error reply, but
processedSuccessfullyis still set totrueafterward. The worker will ack/remove the original inbound event even though its text never reached the session, so transient session-send failures become permanent drops.💡 Suggested fix
try { await withTimeout( params.deps.sendUserMessageToSession({ sessionId: binding.sessionId, text: event.text, sentFrom: adapter.providerId, providerId: adapter.providerId, conversationId: event.conversationId, threadId: event.threadId, }), EXTERNAL_IO_TIMEOUT_MS, `sendUserMessageToSession(${binding.sessionId})`, ); + processedSuccessfully = true; } catch (error) { params.deps.onWarning?.( `Failed to forward channel message into session ${binding.sessionId} (provider=${adapter.providerId} conversation=${event.conversationId} thread=${event.threadId ?? 'null'} messageId=${event.messageId})`, error, ); await replyToConversation( adapter, ref, `Failed to send message to session ${binding.sessionId}.`, ); + continue; } - processedSuccessfully = true;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/core/channelBridgeWorker.ts` around lines 712 - 736, The code sets processedSuccessfully = true unconditionally after the sendUserMessageToSession() try/catch, causing failed forwards to be acked; move the processedSuccessfully = true assignment inside the try block immediately after the await withTimeout(params.deps.sendUserMessageToSession(...)) so it is only set when sendUserMessageToSession succeeds, and leave the catch to call params.deps.onWarning(...) and replyToConversation(adapter, ref, ...) without marking the event processed.apps/cli/src/channels/telegram/telegramAdapter.ts (1)
317-327:⚠️ Potential issue | 🟠 MajorDon't advance the polling offset before the worker confirms the batch.
Polling mode still sets
updateOffsetinsidepullInboundMessages(), whileackInboundMessages()immediately returns whenwebhookModeis false. Any later failure in/attach, binding reads, orsendUserMessageToSession()makes that Telegram update unrecoverable on the nextgetUpdatescall. Keep the highest polledupdate_idpending and move the offset advance into the ack path, mirroring the webhook flow.Also applies to: 329-362
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/telegram/telegramAdapter.ts` around lines 317 - 327, Currently pullInboundMessages() advances the global updateOffset using parseHighestUpdateOffset() (and similar code in the 329-362 block), which makes polled Telegram updates irrecoverable on later processing failures; to fix, remove any updateOffset = maxUpdateId + 1 assignment from pullInboundMessages() and the duplicate block, instead store the highest polled update_id in a pending variable (e.g., pendingMaxUpdateId) or return it with the parsed batch, and update updateOffset only inside ackInboundMessages() when webhookMode is false (mirror webhook flow) so the offset is advanced only after the worker successfully confirms the batch. Ensure you reference and update the existing symbols pullInboundMessages(), ackInboundMessages(), parseHighestUpdateOffset(), updateOffset, and webhookMode when implementing this change.
🧹 Nitpick comments (4)
apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts (1)
14-16: Add a brief rationale for theas anycast.Per coding guidelines,
as anycasts require a one-line justification.process.exithas complex overloaded signatures that don't match the mock implementation's return type.Suggested fix
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { - throw new Error(`process.exit(${code ?? 0})`); - }) as any); + // process.exit has overloaded signatures returning `never`; cast required to satisfy spy typing. + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code ?? 0})`); + }) as any);As per coding guidelines: "Broad
as anycasts are forbidden except in boundary fixtures with a one-line justification."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts` around lines 14 - 16, The mock of process.exit (created as exitSpy with vi.spyOn) uses an `as any` cast to satisfy TypeScript because process.exit has overloaded signatures that don't match the mock implementation; add a one-line justification comment directly above the mock (e.g. "// Cast to any: process.exit has overloads not compatible with our test mock implementation") and keep the `as any` cast, so reviewers see the permitted exception and why it is needed for the test. Ensure the comment mentions process.exit and that this is a test-only boundary fixture.apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts (1)
7-16: Tighten the split-secret regression coverage.The invalid-path test also omits
secretHeaderToken, and the only 200-path test uses the same value for both secrets. That leaves the recent hardening under-specified: the suite never proves “bad path token with a valid header token” or “distinct path and header tokens still succeed.” Adding isolated cases for those two behaviors will catch a future re-coupling much more reliably.Also applies to: 18-51
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts` around lines 7 - 16, Add two targeted tests in telegramWebhookRelay.test.ts using startTelegramWebhookRelay: (1) a case where secretPathToken is invalid (e.g., 'bad$token') but secretHeaderToken is a valid value (alphanumeric/_-), and assert the call rejects with the same "Webhook secret token must match [A-Za-z0-9_-]" error; (2) a case where secretPathToken and secretHeaderToken are distinct but both valid (different strings) and assert startTelegramWebhookRelay resolves/succeeds (and the onUpdate handler can be invoked or the server starts successfully), ensuring the code paths for independent path/header secrets are covered; reference startTelegramWebhookRelay, secretPathToken, and secretHeaderToken when adding these tests.apps/cli/src/channels/channelBridgeAccountConfig.ts (2)
70-72: Consider adding JSDoc comments to exported functions.The file header documents overall responsibilities, but individual exported functions would benefit from brief JSDoc comments describing their purpose, parameters, and return values for improved API discoverability.
As per coding guidelines: "Add JSDoc comments for public APIs and complex logic"
Also applies to: 132-134, 136-140, 207-212, 300-304
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeAccountConfig.ts` around lines 70 - 72, Add JSDoc comments to the exported function splitScopedTelegramBridgeUpdate describing its purpose, parameters (the params.readonly.update: ScopedTelegramBridgeUpdate) and return value (an object with localUpdate: ScopedTelegramBridgeUpdate and sharedUpdate: SharedTelegramBridgeUpdate); follow the project's JSDoc style (brief one-line summary, `@param` and `@returns`) and apply the same treatment to the other exported functions in this module so all public APIs have concise JSDoc for discoverability.
46-64: Consider usinginterfacefor these object shapes.Per coding guidelines,
interfaceis preferred overtypefor defining object shapes. However, since these useReadonly<>wrapper and represent parameter contracts rather than extensible entities, this is acceptable.♻️ Optional: Convert to interfaces
-export type ScopedTelegramBridgeUpdate = Readonly<{ +export interface ScopedTelegramBridgeUpdate { + readonly tickMs?: number; + readonly botToken?: string; + readonly allowedChatIds?: readonly string[]; + readonly requireTopics?: boolean; + readonly webhookEnabled?: boolean; + readonly webhookSecret?: string; + readonly webhookHost?: string; + readonly webhookPort?: number; +} - tickMs?: number; - botToken?: string; - allowedChatIds?: string[]; - requireTopics?: boolean; - webhookEnabled?: boolean; - webhookSecret?: string; - webhookHost?: string; - webhookPort?: number; -}>; -export type SharedTelegramBridgeUpdate = Readonly<{ +export interface SharedTelegramBridgeUpdate { + readonly tickMs?: number; + readonly allowedChatIds?: readonly string[]; + readonly requireTopics?: boolean; + readonly webhookEnabled?: boolean; + readonly webhookHost?: string; + readonly webhookPort?: number; +} - tickMs?: number; - allowedChatIds?: string[]; - requireTopics?: boolean; - webhookEnabled?: boolean; - webhookHost?: string; - webhookPort?: number; -}>;As per coding guidelines: "Prefer
interfaceovertypefor defining object shapes in TypeScript"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/cli/src/channels/channelBridgeAccountConfig.ts` around lines 46 - 64, Replace the exported Readonly type aliases with exported interfaces: change ScopedTelegramBridgeUpdate and SharedTelegramBridgeUpdate from "export type ... = Readonly<{ ... }>" to "export interface ScopedTelegramBridgeUpdate { readonly tickMs?: number; readonly botToken?: string; readonly allowedChatIds?: string[]; readonly requireTopics?: boolean; readonly webhookEnabled?: boolean; readonly webhookSecret?: string; readonly webhookHost?: string; readonly webhookPort?: number }" and similarly for SharedTelegramBridgeUpdate (omit botToken/webhookSecret), i.e., remove the Readonly<> wrapper and mark each property as readonly and optional to preserve immutability and shape semantics while following the guideline to prefer interface over type for object shapes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/cli/src/channels/channelBridgeServerKv.ts`:
- Around line 198-200: When handling telegram.allowedChatIds in
channelBridgeServerKv.ts, don't silently convert a present-but-malformed
non-empty array into an empty array (which is interpreted as "allow all");
instead call normalizeStringArray(telegram.allowedChatIds) and if the original
array was non-empty but the normalized result has length === 0, treat it as
invalid payload and reject (e.g., throw an error or return a validation failure)
rather than setting out.telegram.allowedChatIds; otherwise set
out.telegram.allowedChatIds to the normalized result. This uses
normalizeStringArray and checks telegram.allowedChatIds /
out.telegram.allowedChatIds to locate and implement the validation.
- Around line 281-287: The KV key builders telegramConfigKvKey and bindingsKvKey
currently only include serverId and must be extended to include accountId so
keys are scoped by server+account; change the function signatures
(telegramConfigKvKey(accountId: string, serverId: string) and
bindingsKvKey(accountId: string, serverId: string)) and include accountId in the
key string (using CHANNEL_BRIDGE_KV_PREFIX as the prefix, e.g. adding an
:account:${accountId}: segment) and then update all callers to supply the
accountId so per-account Telegram config, bindings and cursor state do not
collide across different authenticated accounts on the same server.
In `@apps/cli/src/channels/telegram/telegramWebhookRelay.ts`:
- Around line 70-85: The webhook handler (app.post) uses fastify({ logger: false
}) so exceptions from params.onUpdate are currently converted to 5xx with no
local logs; import the file logger (e.g., add import { logger } from
'@/ui/logger') and wrap the call to params.onUpdate(request.body) in a try/catch
inside the app.post handler, on error log the exception with context (include
providedToken/path) using logger.error, then reply with an explicit 500 response
(reply.status(500).send({ ok: false, error: 'Internal Server Error' })) so
failures are logged and a clear error response is returned; keep existing
security check (secureCompareToken) and behavior for 401 intact.
- Around line 64-66: Change the port parsing so non-finite values (NaN/Infinity)
cause an error instead of being treated as 0: first check if params.port is
present and not Number.isFinite(params.port) and throw a descriptive Error when
that happens, otherwise set requestedPort by truncating the finite value
(Math.trunc(params.port)) or defaulting to 0 when params.port is absent; update
the logic around the requestedPort variable and the preexisting range check to
use this new validation (referencing params.port and requestedPort).
In `@docs/telegram-channel-bridge.md`:
- Around line 141-146: The documentation currently shows a single webhook secret
(--webhook-secret / webhookSecret) reused for both the public path token and
Telegram secret_token; update the docs around the examples (including the
earlier block at lines ~184-216) to either show two distinct values (e.g., a
path token and a header/Telegram secret_token) or explicitly state that reusing
one secret weakens security and is not recommended, referencing the flags/fields
(--webhook-secret, public path token, Telegram secret_token) so readers know to
generate and configure separate secrets for URL path and header validation.
---
Outside diff comments:
In `@apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts`:
- Around line 24-26: The test's afterAll block restores consoleErrorSpy but
leaves exitSpy mocked, causing cross-test leakage; update the afterAll to also
restore exitSpy (call exitSpy.mockRestore() or use jest.restoreAllMocks()) so
the process.exit spy created in this spec is removed; locate the exitSpy
reference in this test file (e.g., the variable named exitSpy created with
jest.spyOn(process, 'exit')) and add its mockRestore() alongside
consoleErrorSpy.mockRestore() inside afterAll.
---
Duplicate comments:
In `@apps/cli/src/channels/core/channelBridgeWorker.ts`:
- Around line 712-736: The code sets processedSuccessfully = true
unconditionally after the sendUserMessageToSession() try/catch, causing failed
forwards to be acked; move the processedSuccessfully = true assignment inside
the try block immediately after the await
withTimeout(params.deps.sendUserMessageToSession(...)) so it is only set when
sendUserMessageToSession succeeds, and leave the catch to call
params.deps.onWarning(...) and replyToConversation(adapter, ref, ...) without
marking the event processed.
In `@apps/cli/src/channels/telegram/telegramAdapter.ts`:
- Around line 317-327: Currently pullInboundMessages() advances the global
updateOffset using parseHighestUpdateOffset() (and similar code in the 329-362
block), which makes polled Telegram updates irrecoverable on later processing
failures; to fix, remove any updateOffset = maxUpdateId + 1 assignment from
pullInboundMessages() and the duplicate block, instead store the highest polled
update_id in a pending variable (e.g., pendingMaxUpdateId) or return it with the
parsed batch, and update updateOffset only inside ackInboundMessages() when
webhookMode is false (mirror webhook flow) so the offset is advanced only after
the worker successfully confirms the batch. Ensure you reference and update the
existing symbols pullInboundMessages(), ackInboundMessages(),
parseHighestUpdateOffset(), updateOffset, and webhookMode when implementing this
change.
---
Nitpick comments:
In `@apps/cli/src/channels/channelBridgeAccountConfig.ts`:
- Around line 70-72: Add JSDoc comments to the exported function
splitScopedTelegramBridgeUpdate describing its purpose, parameters (the
params.readonly.update: ScopedTelegramBridgeUpdate) and return value (an object
with localUpdate: ScopedTelegramBridgeUpdate and sharedUpdate:
SharedTelegramBridgeUpdate); follow the project's JSDoc style (brief one-line
summary, `@param` and `@returns`) and apply the same treatment to the other exported
functions in this module so all public APIs have concise JSDoc for
discoverability.
- Around line 46-64: Replace the exported Readonly type aliases with exported
interfaces: change ScopedTelegramBridgeUpdate and SharedTelegramBridgeUpdate
from "export type ... = Readonly<{ ... }>" to "export interface
ScopedTelegramBridgeUpdate { readonly tickMs?: number; readonly botToken?:
string; readonly allowedChatIds?: string[]; readonly requireTopics?: boolean;
readonly webhookEnabled?: boolean; readonly webhookSecret?: string; readonly
webhookHost?: string; readonly webhookPort?: number }" and similarly for
SharedTelegramBridgeUpdate (omit botToken/webhookSecret), i.e., remove the
Readonly<> wrapper and mark each property as readonly and optional to preserve
immutability and shape semantics while following the guideline to prefer
interface over type for object shapes.
In `@apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts`:
- Around line 7-16: Add two targeted tests in telegramWebhookRelay.test.ts using
startTelegramWebhookRelay: (1) a case where secretPathToken is invalid (e.g.,
'bad$token') but secretHeaderToken is a valid value (alphanumeric/_-), and
assert the call rejects with the same "Webhook secret token must match
[A-Za-z0-9_-]" error; (2) a case where secretPathToken and secretHeaderToken are
distinct but both valid (different strings) and assert startTelegramWebhookRelay
resolves/succeeds (and the onUpdate handler can be invoked or the server starts
successfully), ensuring the code paths for independent path/header secrets are
covered; reference startTelegramWebhookRelay, secretPathToken, and
secretHeaderToken when adding these tests.
In `@apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts`:
- Around line 14-16: The mock of process.exit (created as exitSpy with vi.spyOn)
uses an `as any` cast to satisfy TypeScript because process.exit has overloaded
signatures that don't match the mock implementation; add a one-line
justification comment directly above the mock (e.g. "// Cast to any:
process.exit has overloads not compatible with our test mock implementation")
and keep the `as any` cast, so reviewers see the permitted exception and why it
is needed for the test. Ensure the comment mentions process.exit and that this
is a test-only boundary fixture.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 52004ee2-31fc-4809-bb9d-aa2ac3131f6d
⛔ Files ignored due to path filters (3)
docs/assets/doctor-critical-summary.pngis excluded by!**/*.pngdocs/assets/doctor-missing-host-port-summary.pngis excluded by!**/*.pngyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (34)
README.mdapps/cli/src/channels/channelBindingStore.server.test.tsapps/cli/src/channels/channelBindingStore.server.tsapps/cli/src/channels/channelBridgeAccountConfig.test.tsapps/cli/src/channels/channelBridgeAccountConfig.tsapps/cli/src/channels/channelBridgeConfig.test.tsapps/cli/src/channels/channelBridgeConfig.tsapps/cli/src/channels/channelBridgeServerConfigOverlay.test.tsapps/cli/src/channels/channelBridgeServerConfigOverlay.tsapps/cli/src/channels/channelBridgeServerKv.test.tsapps/cli/src/channels/channelBridgeServerKv.tsapps/cli/src/channels/core/channelBridgeWorker.test.tsapps/cli/src/channels/core/channelBridgeWorker.tsapps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.tsapps/cli/src/channels/startChannelBridgeWorker.test.tsapps/cli/src/channels/startChannelBridgeWorker.tsapps/cli/src/channels/telegram/telegramAdapter.test.tsapps/cli/src/channels/telegram/telegramAdapter.tsapps/cli/src/channels/telegram/telegramWebhookRelay.test.tsapps/cli/src/channels/telegram/telegramWebhookRelay.tsapps/cli/src/cli/commandRegistry.tsapps/cli/src/cli/commands/bridge.test.tsapps/cli/src/cli/commands/bridge.tsapps/cli/src/cli/dispatch.tmuxDisallowed.test.tsapps/cli/src/cli/dispatch.tsapps/cli/src/daemon/startDaemon.automation.integration.test.tsapps/cli/src/daemon/startDaemon.tsapps/cli/src/persistence.tsapps/cli/src/ui/doctor.test.tsapps/cli/src/ui/doctor.tsdocs/README.mddocs/channel-bridge-uat.mddocs/channel-bridge.mddocs/telegram-channel-bridge.md
|
Addressed the latest unresolved feedback in signed commits \ and :\n- doctor webhook diagnostics now flag non-loopback hosts and strict invalid ports\n- bridge KV keys are now scoped by server+account\n- malformed non-empty allowedChatIds payloads now fail closed\n- webhook relay now rejects non-finite ports and logs onUpdate failures\n- telegram bridge docs now clarify the single-secret limitation/security tradeoff\n\n@coderabbitai review |
Avoid object-spread wrappers that drop prototype methods; ensure binding directories are account-scoped and chmod 0700.
Use pushRoute (blur+defer on web) and align docs with fixed webhook path + feature gating.
Summary
upstream/devSupersedes
Depends on
Notes
feat/telegram-channel-adapter-v2Summary by CodeRabbit
New Features
Bug Fixes
Tests
Documentation
UI/Diagnostics