Skip to content

feat(cli): add telegram channel adapter, scoped config, and diagnostics (replacement)#115

Open
cdenneen wants to merge 81 commits intohappier-dev:devfrom
cdenneen:feat/telegram-channel-adapter-v2
Open

feat(cli): add telegram channel adapter, scoped config, and diagnostics (replacement)#115
cdenneen wants to merge 81 commits intohappier-dev:devfrom
cdenneen:feat/telegram-channel-adapter-v2

Conversation

@cdenneen
Copy link
Copy Markdown

@cdenneen cdenneen commented Mar 6, 2026

Summary

  • rebased on latest upstream/dev
  • squashed replacement history for Telegram adapter path
  • includes scoped bridge config + diagnostics + Telegram adapter behavior

Supersedes

Depends on

Notes

  • Replacement branch: feat/telegram-channel-adapter-v2

Summary by CodeRabbit

  • New Features

    • Added a Channel Bridge for bi-directional Telegram sessions with webhook and polling modes, CLI bridge commands, daemon auto-start, and runtime config overlay.
  • Bug Fixes

    • Improved KV conflict/retry resilience and safer webhook/token validation and error recovery.
  • Tests

    • Broad new test coverage for adapters, worker, KV flows, overlays, CLI, and startup/shutdown edge cases.
  • Documentation

    • Added Channel Bridge docs, Telegram guide, and UAT checklist; README index updated.
  • UI/Diagnostics

    • Doctor enhancements: webhook validation, port parsing, secret redaction, and exit-code handling.

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@coderabbitai review

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@greptile review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds 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

Cohort / File(s) Summary
Core worker & runtime
apps/cli/src/channels/core/channelBridgeWorker.ts, apps/cli/src/channels/core/channelBridgeWorker.test.ts, apps/cli/src/channels/startChannelBridgeWorker.ts, apps/cli/src/channels/startChannelBridgeWorker.test.ts, apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts
Adds a provider-agnostic channel bridge worker, tick loop, slash-command parsing/handling, per-session runtime caching, lifecycle start/stop orchestration, public worker APIs and extensive unit/integration tests.
Telegram adapter & webhook relay
apps/cli/src/channels/telegram/telegramAdapter.ts, apps/cli/src/channels/telegram/telegramAdapter.test.ts, apps/cli/src/channels/telegram/telegramWebhookRelay.ts, apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts
Implements Telegram API client, polling/webhook adapter with inbound normalization, ack/queue semantics, outbound truncation/error handling, secure loopback Fastify webhook relay, and tests.
Server KV, binding store & concurrency
apps/cli/src/channels/channelBridgeServerKv.ts, apps/cli/src/channels/channelBridgeServerKv.test.ts, apps/cli/src/channels/channelBindingStore.server.ts, apps/cli/src/channels/channelBindingStore.server.test.ts
Adds ChannelBridge KV client/types, base64 payload handling, schema parsing/validation, optimistic read/mutate with version-mismatch errors, and a cache-backed server binding store with optimistic retries and comprehensive tests.
Scoped/account config & overlay
apps/cli/src/channels/channelBridgeAccountConfig.ts, apps/cli/src/channels/channelBridgeAccountConfig.test.ts, apps/cli/src/channels/channelBridgeServerConfigOverlay.ts, apps/cli/src/channels/channelBridgeServerConfigOverlay.test.ts
Implements scoped Telegram bridge config read/upsert/remove, split local/shared updates, pruning of empty nodes, and overlay helper to merge server KV config into settings; includes tests.
Runtime config resolution
apps/cli/src/channels/channelBridgeConfig.ts, apps/cli/src/channels/channelBridgeConfig.test.ts
Adds layered runtime config resolver merging env/global/server/account scopes with parsing, bounds checking, host/port rules, secret extraction, and tests.
CLI command surface & dispatch
apps/cli/src/cli/commands/bridge.ts, apps/cli/src/cli/commands/bridge.test.ts, apps/cli/src/cli/commandRegistry.ts, apps/cli/src/cli/dispatch.ts, apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts
Adds bridge CLI (list/set/clear) with validation, transactional server-KV vs local secret updates and rollback, registry wiring, tmux disallowed checks update, and tests.
Daemon integration & test harness
apps/cli/src/daemon/startDaemon.ts, apps/cli/src/daemon/startDaemon.automation.integration.test.ts
Wires best-effort channel bridge startup into daemon bootstrap, overlays server KV into settings, manages lifecycle and shutdown timeouts, and exposes test harness hooks/mocks.
Diagnostics, persistence types & UI
apps/cli/src/persistence.ts, apps/cli/src/ui/doctor.ts, apps/cli/src/ui/doctor.test.ts
Adds channelBridge settings type, webhook validation and strict port parsing, redaction helpers for channelBridge secrets, runtime overlay diagnostics and exit-code control, and tests.
Documentation & README
README.md, docs/README.md, docs/channel-bridge.md, docs/channel-bridge-uat.md, docs/telegram-channel-bridge.md
Adds Channel Bridge docs, UAT checklist, Telegram guide, and README index entries including a "Channel Integrations" subsection linking the Telegram guide.
Misc & broad tests
apps/cli/src/channels/.../*.test.ts, apps/cli/src/cli/.../*.test.ts (multiple new/updated test files)
Large suite of unit and integration tests added across adapters, KV, store, worker, CLI, webhook relay, and runtime covering many success and error scenarios.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is sparse and vague, lacking required template sections (Why, How to test, Checklist). While it mentions rebasing and squashing, it does not clearly explain the problem being solved or provide testing instructions. Complete the PR description by filling in Why (problem/issue it solves), How to test (step-by-step verification), and Checklist sections per the repository template to improve clarity and reviewability.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main additions: a Telegram channel adapter, scoped configuration management, and diagnostic improvements, which aligns with the substantial code additions across multiple modules.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds a full Telegram Channel Bridge to the Happier CLI: a provider-agnostic bridge worker (channelBridgeWorker.ts), a Telegram adapter with both polling and webhook modes, a scoped settings/KV config system, happier bridge CLI commands, daemon integration, and enhanced happier doctor diagnostics. It supersedes #110 and depends on #114.

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:

  1. Dead parameters and messageId type-contract mismatch in createStableBridgeLocalId (startChannelBridgeWorker.ts:52): sessionId and text are declared in the parameter object but never contribute to the hash; more critically, messageId is typed ?: string (matching the ChannelBridgeDeps interface) but throws at runtime when absent, creating a hidden contract violation that TypeScript cannot catch.

  2. Settings-level webhook token not validated for the allowed character set (channelBridgeConfig.ts:216): settingsWebhookSecret is resolved through readTrimmedString (non-empty check only), while the env-var path uses readWebhookSecretToken (full pattern validation). A malformed token in settings.json silently causes a webhook startup failure and polling-mode fallback at daemon start.

  3. No operator warning when resolveSessionRuntime returns null (startChannelBridgeWorker.ts:290): when a bound session becomes inaccessible, outbound forwarding stalls silently with no log output — unlike the analogous store.getBinding failure path which both notifies the user in-channel and emits a warning.

Confidence Score: 2/5

  • PR has solid core bridge implementation but carries three unaddressed issues: type-contract violation in localId generation, silent operational stall with no diagnostics, and inconsistent webhook token validation creating config confusion.
  • Score of 2/5 reflects that the PR is functionally sound for polling-mode operation and has been well-hardened through multiple review rounds. However, three issues from the prior review cycle remain unaddressed: (1) dead parameters and type-contract mismatch in createStableBridgeLocalId create developer confusion and hidden runtime constraints; (2) resolveSessionRuntime returning null silently stalls outbound forwarding with no operator visibility into why; (3) settings-derived webhook token validation is inconsistent with env-var validation, making it easy to misconfigure when editing settings.json directly. None are critical regressions, but they represent observable gaps in type safety, operational diagnostics, and config validation consistency that should be resolved before merge.
  • apps/cli/src/channels/startChannelBridgeWorker.ts (dead parameters, missing warning on session null), apps/cli/src/channels/channelBridgeConfig.ts (settings webhook token validation gap)

Last reviewed commit: 74ae08b

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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: Replace channelBridge: unknown with a typed settings envelope.

This persisted subtree is now a first-class part of Settings, and leaving it as unknown is already forcing downstream as any access 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 "Prefer interface over type for 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.

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<{
As per coding guidelines "Add JSDoc comments for public APIs and complex logic".
🤖 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.ts writes and falls back to asserting mock call order instead. Keep an in-memory settingsState here 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 using interface instead of type for object shapes.

Per coding guidelines, interface is preferred over type for 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

📥 Commits

Reviewing files that changed from the base of the PR and between cd992ae and dde7d03.

⛔ Files ignored due to path filters (3)
  • docs/assets/doctor-critical-summary.png is excluded by !**/*.png
  • docs/assets/doctor-missing-host-port-summary.png is excluded by !**/*.png
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (33)
  • README.md
  • apps/cli/src/channels/channelBindingStore.server.test.ts
  • apps/cli/src/channels/channelBindingStore.server.ts
  • apps/cli/src/channels/channelBridgeAccountConfig.test.ts
  • apps/cli/src/channels/channelBridgeAccountConfig.ts
  • apps/cli/src/channels/channelBridgeConfig.test.ts
  • apps/cli/src/channels/channelBridgeConfig.ts
  • apps/cli/src/channels/channelBridgeServerConfigOverlay.test.ts
  • apps/cli/src/channels/channelBridgeServerConfigOverlay.ts
  • apps/cli/src/channels/channelBridgeServerKv.test.ts
  • apps/cli/src/channels/channelBridgeServerKv.ts
  • apps/cli/src/channels/core/channelBridgeWorker.test.ts
  • apps/cli/src/channels/core/channelBridgeWorker.ts
  • apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts
  • apps/cli/src/channels/startChannelBridgeWorker.test.ts
  • apps/cli/src/channels/startChannelBridgeWorker.ts
  • apps/cli/src/channels/telegram/telegramAdapter.test.ts
  • apps/cli/src/channels/telegram/telegramAdapter.ts
  • apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts
  • apps/cli/src/channels/telegram/telegramWebhookRelay.ts
  • apps/cli/src/cli/commandRegistry.ts
  • apps/cli/src/cli/commands/bridge.test.ts
  • apps/cli/src/cli/commands/bridge.ts
  • apps/cli/src/cli/dispatch.ts
  • apps/cli/src/daemon/startDaemon.automation.integration.test.ts
  • apps/cli/src/daemon/startDaemon.ts
  • apps/cli/src/persistence.ts
  • apps/cli/src/ui/doctor.test.ts
  • apps/cli/src/ui/doctor.ts
  • docs/README.md
  • docs/channel-bridge-uat.md
  • docs/channel-bridge.md
  • docs/telegram-channel-bridge.md

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

Addressed the latest review items in commit 8514428.

Highlights:

  • Added explicit senderId in Telegram inbound parsing.
  • Hardened webhook relay with explicit header secret token requirement and loopback-only host validation.
  • Prevented tick-only scoped updates from materializing empty provider trees.
  • Switched plain-session outbound transcript reads to bypass decryption path.
  • Reordered bridge CLI persistence so shared KV writes happen before local settings updates.
  • Added strict empty --bot-token validation.
  • Hardened doctor redaction for global/server/account provider scopes and exported strict webhook port parsing.
  • Clarified UAT/doc behavior for webhook 401 vs 404 outcomes and refreshed bridge docs/index links.

Targeted regression tests passed locally:

  • 10 files, 130 tests passed.

@coderabbitai review

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@greptile review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

@cdenneen Thanks for the detailed summary of the changes in commit 8514428b! The hardening across webhook relay, doctor redaction, and scoped config persistence all sound like meaningful improvements. Triggering a fresh incremental review now to check the latest commit.


🧠 Learnings used
Learnt from: cdenneen
Repo: happier-dev/happier PR: 110
File: apps/cli/src/channels/telegram/telegramAdapter.test.ts:17-41
Timestamp: 2026-03-04T15:25:08.440Z
Learning: In apps/cli/src/channels/telegram/telegramAdapter.test.ts, the TelegramApiClient (getMe, getUpdates, sendMessage) is intentionally stubbed with vi.fn mocks. This is an accepted exception to the no-mocking guideline because TelegramApiClient is a true third-party API system boundary requiring external credentials/network. End-to-end Telegram transport validation is tracked separately as an integration suite to avoid CI flakiness.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

Added a follow-up fix in commit 1714afa to address daemon startup coupling feedback.

Change:

  • Moved channel bridge bootstrap into its own best-effort async task so machine registration/sync setup is no longer blocked by bridge startup awaits.

Validation rerun after this patch:

  • startDaemon.automation.integration.test.ts
  • bridge.test.ts
  • startChannelBridgeWorker.test.ts
  • telegramWebhookRelay.test.ts
  • channelBindingStore.server.test.ts
  • doctor.test.ts
  • dispatch.tmuxDisallowed.test.ts

All passed.

@coderabbitai review

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@greptile review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

@cdenneen Thanks for the follow-up fix! Decoupling the channel bridge bootstrap into a best-effort async task is the right approach — machine registration and sync setup are on the critical startup path (per the daemon lifecycle requirements), so removing the blocking awaits from bridge initialization prevents any bridge startup latency or failure from delaying core daemon readiness. Triggering an incremental review of commit 1714afaa now.


🧠 Learnings used
Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/**/*.ts : Daemon startup registration must send REST POST /v1/machines request with machine ID, encrypted metadata (base64), and encrypted daemonState (base64); server response includes metadataVersion and daemonStateVersion for independent versioning

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/startDaemon.ts : Daemon startup must call `authAndSetupMachineIfNeeded()` to ensure credentials exist before proceeding

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/**/*.ts : Daemon WebSocket connection must establish persistent connection to backend via `ApiMachineClient` and register RPC handlers: `spawn-happy-session`, `stop-session`, `requestShutdown`

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/**/*.ts : New Happier process spawned by daemon must call `notifyDaemonSessionStarted()` to POST to daemon's `/session-started` endpoint to report itself and receive `happySessionId`

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/**/*.ts : Daemon state updates via WebSocket must emit 'machine-update-state' with machineId, encrypted daemonState (base64), and expectedVersion; server response includes version-mismatch handling with current state

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/**/*.ts : On daemon version mismatch detection, spawn new daemon via `spawnHappyCLI(['daemon', 'start'])`, which will see old daemon.state.json version mismatch and call `stopDaemon()` to kill old daemon via HTTP `/stop` with SIGKILL fallback

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/**/*.ts : ApiMachineClient WebSocket communication must handle bidirectional messages: daemon to server (machine-alive, machine-update-metadata, machine-update-state), server to daemon (rpc-request with spawn-happy-session, stop-session, requestShutdown); all data encrypted with TweetNaCl

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/daemon/**/*.ts : Daemon-spawned sessions initiated via RPC `spawn-happy-session` must: create directory if needed, spawn detached Happier process with `--happy-starting-mode remote --started-by daemon`, add to `pidToTrackedSession` map, set up 10-second awaiter for session webhook

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/{index,daemon/startDaemon}.ts : Daemon lifecycle: Receive `daemon start` command in `src/index.ts`, spawn detached process via `spawnHappyCLI(['daemon', 'start-sync'], { detached: true })`, which calls `startDaemon()` from `src/daemon/startDaemon.ts`

Learnt from: CR
Repo: happier-dev/happier PR: 0
File: apps/cli/src/daemon/CLAUDE.md:0-0
Timestamp: 2026-02-16T05:35:08.653Z
Learning: Applies to apps/cli/src/daemon/src/index.ts : CLI startup must check if machine ID exists in settings; if not, create ID locally only (do NOT create machine on server); CLI does not manage machine details; all API and schema live in daemon subpackage

Learnt from: cdenneen
Repo: happier-dev/happier PR: 110
File: apps/cli/src/channels/telegram/telegramAdapter.test.ts:17-41
Timestamp: 2026-03-04T15:25:08.440Z
Learning: In apps/cli/src/channels/telegram/telegramAdapter.test.ts, the TelegramApiClient (getMe, getUpdates, sendMessage) is intentionally stubbed with vi.fn mocks. This is an accepted exception to the no-mocking guideline because TelegramApiClient is a true third-party API system boundary requiring external credentials/network. End-to-end Telegram transport validation is tracked separately as an integration suite to avoid CI flakiness.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

Addressed the latest Greptile findings in commit a264754 and resolved the corresponding threads.

Summary:

  • Added queue-overflow warning/counter for Telegram webhook update eviction.
  • Hardened HAPPIER_CHANNEL_BRIDGE_IO_TIMEOUT_MS parsing to strict numeric input.
  • Added regression coverage for the overflow warning behavior.

@coderabbitai review

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@greptile review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

@cdenneen Thanks for the follow-up commit! The additions sound well-targeted:

  • Queue-overflow warning and eviction counter for the Telegram webhook update queue adds important observability for back-pressure scenarios.
  • Strict numeric parsing for HAPPIER_CHANNEL_BRIDGE_IO_TIMEOUT_MS closes the gap where non-numeric env var values could silently produce NaN-based timeouts.
  • Regression coverage for the overflow warning behavior ensures that path stays tested going forward.

Triggering an incremental review of commit a2647541 now.


🧠 Learnings used
Learnt from: cdenneen
Repo: happier-dev/happier PR: 110
File: apps/cli/src/channels/telegram/telegramAdapter.test.ts:17-41
Timestamp: 2026-03-04T15:25:08.440Z
Learning: In apps/cli/src/channels/telegram/telegramAdapter.test.ts, the TelegramApiClient (getMe, getUpdates, sendMessage) is intentionally stubbed with vi.fn mocks. This is an accepted exception to the no-mocking guideline because TelegramApiClient is a true third-party API system boundary requiring external credentials/network. End-to-end Telegram transport validation is tracked separately as an integration suite to avoid CI flakiness.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (1)
apps/cli/src/cli/commands/bridge.ts (1)

266-282: ⚠️ Potential issue | 🟠 Major

Handle 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 @/persistence is stubbed, it will not catch regressions in the actual settings.json shape or local write behavior that this PR changes. Since you already create a temp HAPPIER_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 the RecordLike | null return from readScopedTelegramBridgeConfig() are all describing closely related parts of the same public config surface with anonymous shapes. A ScopedTelegramBridgeConfig interface (plus a nested webhook interface) would make downstream usage type-safe and reduce drift between the read/split paths.

As per coding guidelines, "Prefer interface over type for 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

📥 Commits

Reviewing files that changed from the base of the PR and between cd992ae and 1714afa.

⛔ Files ignored due to path filters (3)
  • docs/assets/doctor-critical-summary.png is excluded by !**/*.png
  • docs/assets/doctor-missing-host-port-summary.png is excluded by !**/*.png
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (34)
  • README.md
  • apps/cli/src/channels/channelBindingStore.server.test.ts
  • apps/cli/src/channels/channelBindingStore.server.ts
  • apps/cli/src/channels/channelBridgeAccountConfig.test.ts
  • apps/cli/src/channels/channelBridgeAccountConfig.ts
  • apps/cli/src/channels/channelBridgeConfig.test.ts
  • apps/cli/src/channels/channelBridgeConfig.ts
  • apps/cli/src/channels/channelBridgeServerConfigOverlay.test.ts
  • apps/cli/src/channels/channelBridgeServerConfigOverlay.ts
  • apps/cli/src/channels/channelBridgeServerKv.test.ts
  • apps/cli/src/channels/channelBridgeServerKv.ts
  • apps/cli/src/channels/core/channelBridgeWorker.test.ts
  • apps/cli/src/channels/core/channelBridgeWorker.ts
  • apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts
  • apps/cli/src/channels/startChannelBridgeWorker.test.ts
  • apps/cli/src/channels/startChannelBridgeWorker.ts
  • apps/cli/src/channels/telegram/telegramAdapter.test.ts
  • apps/cli/src/channels/telegram/telegramAdapter.ts
  • apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts
  • apps/cli/src/channels/telegram/telegramWebhookRelay.ts
  • apps/cli/src/cli/commandRegistry.ts
  • apps/cli/src/cli/commands/bridge.test.ts
  • apps/cli/src/cli/commands/bridge.ts
  • apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts
  • apps/cli/src/cli/dispatch.ts
  • apps/cli/src/daemon/startDaemon.automation.integration.test.ts
  • apps/cli/src/daemon/startDaemon.ts
  • apps/cli/src/persistence.ts
  • apps/cli/src/ui/doctor.test.ts
  • apps/cli/src/ui/doctor.ts
  • docs/README.md
  • docs/channel-bridge-uat.md
  • docs/channel-bridge.md
  • docs/telegram-channel-bridge.md

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

Addressed all current open threads in commit a54e4b5 and resolved each thread with per-item notes.

Highlights:

  • Hardened chat-id parsing + env-empty behavior to avoid accidental allow-all widening.
  • Added deferred adapter ack flow for inbound webhook draining.
  • Hardened persisted bindings parsing (required cursor/timestamp fields).
  • Added write-time webhook host validation + expanded tmux command guard.
  • Mitigated oversized Telegram outbound message stalling.
  • Closed daemon bridge startup/shutdown race and added integration coverage.
  • Updated UAT clean-state documentation.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

@cdenneen Thanks for the comprehensive summary! The changes in commit a54e4b54 look well-targeted across several important areas:

  • Chat-id parsing + env-empty hardening — closing the accidental allow-all widening is a critical security fix.
  • Deferred adapter ack flow — properly sequencing inbound webhook draining avoids premature acknowledgment.
  • Persisted bindings parsing — requiring cursor/timestamp fields prevents silent partial-state hydration.
  • Write-time webhook host validation + tmux guard — good defense-in-depth additions.
  • Oversized Telegram outbound mitigation — prevents a stall on large payloads from blocking the bridge.
  • Daemon bridge startup/shutdown race + integration coverage — closing that race with test coverage is the right approach.
  • UAT clean-state docs — keeping the UAT guide accurate for multi-machine scenarios is important for reproducibility.

Triggering an incremental review of commit a54e4b54 now.


🧠 Learnings used
Learnt from: cdenneen
Repo: happier-dev/happier PR: 110
File: apps/cli/src/channels/telegram/telegramAdapter.test.ts:17-41
Timestamp: 2026-03-04T15:25:08.440Z
Learning: In apps/cli/src/channels/telegram/telegramAdapter.test.ts, the TelegramApiClient (getMe, getUpdates, sendMessage) is intentionally stubbed with vi.fn mocks. This is an accepted exception to the no-mocking guideline because TelegramApiClient is a true third-party API system boundary requiring external credentials/network. End-to-end Telegram transport validation is tracked separately as an integration suite to avoid CI flakiness.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

@greptile review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Restore exitSpy in afterAll to prevent leakage.

consoleErrorSpy is restored but exitSpy is 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 | 🟠 Major

Don't ack the inbound event after sendUserMessageToSession() fails.

This catch sends an error reply, but processedSuccessfully is still set to true afterward. 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 | 🟠 Major

Don't advance the polling offset before the worker confirms the batch.

Polling mode still sets updateOffset inside pullInboundMessages(), while ackInboundMessages() immediately returns when webhookMode is false. Any later failure in /attach, binding reads, or sendUserMessageToSession() makes that Telegram update unrecoverable on the next getUpdates call. Keep the highest polled update_id pending 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 the as any cast.

Per coding guidelines, as any casts require a one-line justification. process.exit has 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 any casts 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 using interface for these object shapes.

Per coding guidelines, interface is preferred over type for defining object shapes. However, since these use Readonly<> 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 interface over type for 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

📥 Commits

Reviewing files that changed from the base of the PR and between cd992ae and a54e4b5.

⛔ Files ignored due to path filters (3)
  • docs/assets/doctor-critical-summary.png is excluded by !**/*.png
  • docs/assets/doctor-missing-host-port-summary.png is excluded by !**/*.png
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (34)
  • README.md
  • apps/cli/src/channels/channelBindingStore.server.test.ts
  • apps/cli/src/channels/channelBindingStore.server.ts
  • apps/cli/src/channels/channelBridgeAccountConfig.test.ts
  • apps/cli/src/channels/channelBridgeAccountConfig.ts
  • apps/cli/src/channels/channelBridgeConfig.test.ts
  • apps/cli/src/channels/channelBridgeConfig.ts
  • apps/cli/src/channels/channelBridgeServerConfigOverlay.test.ts
  • apps/cli/src/channels/channelBridgeServerConfigOverlay.ts
  • apps/cli/src/channels/channelBridgeServerKv.test.ts
  • apps/cli/src/channels/channelBridgeServerKv.ts
  • apps/cli/src/channels/core/channelBridgeWorker.test.ts
  • apps/cli/src/channels/core/channelBridgeWorker.ts
  • apps/cli/src/channels/startChannelBridgeWorker.startupFailure.test.ts
  • apps/cli/src/channels/startChannelBridgeWorker.test.ts
  • apps/cli/src/channels/startChannelBridgeWorker.ts
  • apps/cli/src/channels/telegram/telegramAdapter.test.ts
  • apps/cli/src/channels/telegram/telegramAdapter.ts
  • apps/cli/src/channels/telegram/telegramWebhookRelay.test.ts
  • apps/cli/src/channels/telegram/telegramWebhookRelay.ts
  • apps/cli/src/cli/commandRegistry.ts
  • apps/cli/src/cli/commands/bridge.test.ts
  • apps/cli/src/cli/commands/bridge.ts
  • apps/cli/src/cli/dispatch.tmuxDisallowed.test.ts
  • apps/cli/src/cli/dispatch.ts
  • apps/cli/src/daemon/startDaemon.automation.integration.test.ts
  • apps/cli/src/daemon/startDaemon.ts
  • apps/cli/src/persistence.ts
  • apps/cli/src/ui/doctor.test.ts
  • apps/cli/src/ui/doctor.ts
  • docs/README.md
  • docs/channel-bridge-uat.md
  • docs/channel-bridge.md
  • docs/telegram-channel-bridge.md

@cdenneen
Copy link
Copy Markdown
Author

cdenneen commented Mar 6, 2026

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants