Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 261 additions & 0 deletions docs/ai/design/feature-multi-telegram-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
---
phase: design
title: "Multi Telegram Channels: System Design"
description: Architecture for named Telegram channel instances and concurrent agent bridges
---

# System Design: Multi Telegram Channels

## Architecture Overview

The current channel architecture remains valid: channel-connector is a pure message pipe, and CLI owns agent orchestration. This feature changes the CLI and config identity model from a single implicit `telegram` channel to named channel instances.

```mermaid
graph TD
subgraph Config["~/.ai-devkit/channels.json"]
C1["personal: telegram token A"]
C2["work: telegram token B"]
end

subgraph CLI["ai-devkit channel start <name> --agent <agent>"]
S1["Bridge process: personal -> agent A"]
S2["Bridge process: work -> agent B"]
PM["ChannelService"]
end

subgraph Connector["@ai-devkit/channel-connector"]
TA1["TelegramAdapter(token A)"]
TA2["TelegramAdapter(token B)"]
end

subgraph Agents["@ai-devkit/agent-manager"]
A1["Agent A process"]
A2["Agent B process"]
end

C1 --> S1
C2 --> S2
S1 --> TA1
S2 --> TA2
S1 --> A1
S2 --> A2
PM --> S1
PM --> S2
```

### Key Principles

- A **channel instance** is identified by a user-facing name such as `personal` or `work`.
- Each channel instance has one channel type, one Telegram bot token, and one authorization scope.
- Each running bridge process maps one channel instance to one agent process.
- Multiple bridge processes are allowed when their channel names differ.
- `@ai-devkit/channel-connector` remains unaware of agents and channel names beyond adapter construction.

## Data Models

### Channel Configuration

Keep the existing map shape and make channel names first-class:

```typescript
interface ChannelConfig {
channels: Record<string, ChannelEntry>;
}

interface ChannelEntry {
type: 'telegram' | 'slack' | 'whatsapp';
enabled: boolean;
createdAt: string;
updatedAt?: string;
config: TelegramConfig;
}

interface TelegramConfig {
botToken: string;
botUsername: string;
authorizedChatId?: number;
}
```

Compatibility rule: if an existing config has only `channels.telegram`, it is treated as a named channel instance called `telegram`.

### Bridge Process Metadata

Store a small runtime file for channel bridge processes. Do not overload agent session utilities, because they describe historical agent sessions rather than currently running channel bridge processes.

```typescript
interface ChannelBridgeProcess {
channelName: string;
channelType: 'telegram';
agentName: string;
agentPid: number;
bridgePid: number;
startedAt: string;
}
```

Store this metadata in a small registry file such as `~/.ai-devkit/channel-bridges.json`. Do not persist bot tokens or transcript content in process metadata.

Status commands should treat registry entries as advisory and verify liveness with a PID check before reporting a bridge as running. Stale entries are removed when detected.

## API Design

### CLI Commands

```bash
ai-devkit channel connect telegram --name <name>
ai-devkit channel list
ai-devkit channel disconnect <name>
ai-devkit channel start <name> --agent <agent-name>
ai-devkit channel status [<name>]
```

Backwards compatibility:

```bash
ai-devkit channel connect telegram
ai-devkit channel disconnect telegram
ai-devkit channel start --agent <agent-name>
```

When `connect telegram` is called without `--name`:

- Create or update the default `telegram` channel entry.

When `start` is called without a name, use `telegram` if it exists and there is only one configured Telegram channel. If multiple Telegram channels exist, require an explicit name.

Commander shape:

```typescript
channelCommand
.command('connect <type>')
.option('--name <name>', 'Channel instance name')

channelCommand
.command('start [name]')
.requiredOption('--agent <name>', 'Name of the agent to bridge')

channelCommand
.command('status [name]')
```

### ConfigStore

The current `ConfigStore` API already supports named entries:

```typescript
saveChannel(name: string, entry: ChannelEntry): Promise<void>;
removeChannel(name: string): Promise<void>;
getChannel(name: string): Promise<ChannelEntry | undefined>;
```

Implementation should validate names before saving and avoid special-casing channel type as the config key.

CLI should reject duplicate Telegram bot tokens across different channel names before saving a channel. The check compares the target token against every other configured Telegram channel, excluding the channel being updated.

### Channel Service

Create a focused CLI service boundary for channel rules and foreground bridge process metadata. `ChannelService` owns the runtime bridge file directly; no separate bridge registry abstraction is needed until daemon support creates real pressure for one.

```typescript
class ChannelService {
resolveConnectChannelName(name?: string): string;
assertUniqueTelegramToken(config: ChannelConfig, targetName: string, botToken: string): void;
resolveStartChannelName(config: ChannelConfig, name?: string): string;
getLiveBridges(): Promise<ChannelBridgeProcess[]>;
getLiveBridgeByChannel(channelName: string): Promise<ChannelBridgeProcess | undefined>;
registerBridge(process: ChannelBridgeProcess): Promise<void>;
unregisterBridge(channelName: string): Promise<void>;
}
```

The service checks whether `bridgePid` is still alive and removes stale entries. This lets `channel status` distinguish configured channels from actively running bridge processes without introducing daemon lifecycle management. The bridge file is intentionally platform-neutral: `channelType` can be `telegram`, `slack`, `discord`, or another adapter type in the future, while platform-specific secrets stay in `channels.json`.

### Runtime Routing

For each started channel instance, CLI creates an isolated bridge context:

```typescript
interface BridgeContext {
channelName: string;
agent: AgentInfo;
adapter: TelegramAdapter;
activeChatId: string | null;
lastMessageCount: number;
}
```

The message handler and output polling loop close over this context, preventing cross-channel delivery.

On bridge start, `activeChatId` is initialized from the selected channel entry's `authorizedChatId` when present. On the first accepted incoming message for a channel without an authorized chat ID, CLI saves that chat ID back to the same channel entry. This keeps authorization scoped per channel and stable across restarts.

## Component Breakdown

### `packages/channel-connector`
- No broad architecture change.
- Ensure `ConfigStore` correctly preserves arbitrary channel names.
- Ensure Telegram authorization state is stored per channel instance.
- Add tests for multiple named entries with different tokens and chat IDs.

### `packages/cli/src/commands/channel.ts`
- Parse channel instance names for `start`, `disconnect`, and `status`.
- Add `--name` to `connect telegram`.
- Use `telegram` as the default channel name when connecting without `--name`.
- Reject duplicate Telegram bot tokens across different channel names.
- Resolve ambiguous default behavior.
- Start bridge contexts using the selected channel config.
- Register or report running bridge process status per channel name.

### `packages/cli/src/services/channel/`
- Add `channel.service.ts` for channel naming, duplicate-token validation, bridge lookup, and command-facing channel rules.
- Store foreground bridge metadata in `~/.ai-devkit/channel-bridges.json` from `ChannelService`.
- Track active foreground bridge processes by channel name.
- Prune stale bridge PIDs before reporting status or starting a bridge.
- Keep existing agent session utilities unchanged.

## Design Decisions

1. **Named channel instances instead of type-only config keys**
- Reason: users need multiple Telegram configs, all with the same type but different tokens.

2. **One bridge process per channel-agent mapping**
- Reason: matches the existing foreground process model and isolates long polling, chat authorization, and output polling.

3. **Require explicit channel name when multiple configs exist**
- Reason: avoids accidentally sending a bot token or agent messages through the wrong channel.

4. **Reject duplicate Telegram tokens**
- Reason: concurrent long polling for the same Telegram bot token can conflict and route messages unpredictably.

5. **Use a channel service boundary**
- Reason: channel behavior now includes naming rules, duplicate-token policy, runtime bridge metadata, and future daemon integration. Keeping this behind `ChannelService` avoids putting business rules in Commander actions or generic utilities.

6. **Persist authorized chat ID per channel**
- Reason: each Telegram bot should keep its own authorization scope across restarts, and one channel's first user must not affect another channel.

7. **No channel-connector dependency on agent-manager**
- Reason: preserves the design boundary from `feature-channel-connector`.

## Non-Functional Requirements

### Security
- Never print bot tokens.
- Store config file with mode `0600`.
- Keep authorized chat ID scoped per channel name.
- Store bridge metadata without bot tokens.
- Do not persist agent transcript content in channel process metadata.

### Reliability
- Failure in one bridge process must not stop other bridge processes.
- Starting an already-running channel name should fail clearly.
- Stale bridge metadata entries should be pruned before status/start decisions.
- SIGINT/SIGTERM cleanup should unregister only the current channel bridge process.
- Managed `channel stop <name>` behavior is out of scope until daemon support exists.

### Performance
- Per-bridge polling behavior should remain equivalent to current channel start behavior.
- Running multiple bridges should scale linearly with the number of channels and active agents.

### Compatibility
- Existing `channels.telegram` config remains usable as the `telegram` channel instance.
- Existing commands without names continue to work when unambiguous.
97 changes: 97 additions & 0 deletions docs/ai/implementation/feature-multi-telegram-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
phase: implementation
title: "Multi Telegram Channels: Implementation Guide"
description: Technical notes for implementing named Telegram channel instances
---

# Implementation Guide: Multi Telegram Channels

## Development Setup

- Work in branch/worktree `feature-multi-telegram-channels`.
- Use the root `package-lock.json` and `npm ci` for dependency bootstrap.
- Validate feature docs with `npx ai-devkit@latest lint --feature multi-telegram-channels`.

## Code Structure

Expected touch points:

- `packages/channel-connector/src/ConfigStore.ts`
- `packages/channel-connector/src/types.ts`
- `packages/channel-connector/src/__tests__/ConfigStore.test.ts`
- `packages/cli/src/commands/channel.ts`
- `packages/cli/src/__tests__/commands/channel.test.ts`
- `packages/cli/src/services/channel/channel.service.ts`
- `packages/cli/src/__tests__/services/channel/channel.service.test.ts`

## Implementation Notes

### Named Channel Instances

- Treat the `channels` record key as the channel instance name.
- Do not use channel type as the unique identifier once a name is available.
- Keep `telegram` as the default compatibility name.

### CLI Parsing

- Prefer `channel start <name> --agent <agent-name>` for explicit multi-channel use.
- Preserve `channel start --agent <agent-name>` when the target channel can be resolved unambiguously.
- Add `--name <name>` to `channel connect telegram`.
- When connecting without `--name`, create or update the default `telegram` channel entry.
- Reject duplicate Telegram bot tokens across channel entries.
- Do not implement `channel stop <name>` in this feature.

### Runtime Isolation

- Create a new Telegram adapter per bridge process.
- Keep `activeChatId`, output polling cursor, and agent mapping inside the bridge context.
- Register running process metadata under channel name.
- Initialize `activeChatId` from the selected channel entry's `authorizedChatId` when present.
- Persist the first accepted chat ID back to the same channel entry when no authorization exists.
- Use a dedicated channel bridge metadata file to report running status and prune stale bridge PIDs.

## Implemented Behavior

- `channel connect telegram --name <name>` stores a named Telegram channel.
- `channel connect telegram` creates or updates the default `telegram` channel.
- Duplicate Telegram bot tokens are rejected across different channel names.
- `channel list` includes authorization and bridge running state.
- `channel disconnect <name>` removes a named channel.
- `channel start [name] --agent <agent>` starts the selected channel; omitting `name` is allowed only when exactly one Telegram channel is configured.
- `channel status [name]` reports configured channel details plus live bridge metadata.
- `channel stop <name>` remains out of scope.

## Integration Points

- `ConfigStore` persists named channel entries in `~/.ai-devkit/channels.json`.
- `ChannelService` owns channel naming, duplicate-token validation, live bridge lookup, bridge registration/removal, and active foreground bridge metadata persistence in `~/.ai-devkit/channel-bridges.json`.
- CLI resolves agents through `@ai-devkit/agent-manager`.
- Telegram adapter continues to own Bot API long polling and message sending.

## Error Handling

- Unknown channel name: show a clear error and available names.
- Ambiguous default start: require explicit channel name.
- Already-running channel name: fail clearly and show the existing bridge PID when available.
- Invalid token: reject connect/start without printing the token.
- Duplicate token: reject connect/update without printing the token.

## Performance Considerations

- Each channel bridge has its own Telegram long-polling loop and agent output polling loop.
- Avoid shared timers or global mutable chat state that can cross channel boundaries.

## Security Notes

- Never log bot tokens.
- Keep config permissions at `0600`.
- Scope authorized chat IDs to the channel instance.
- Do not write transcript content into bridge process metadata.

## Verification Evidence

- `packages/cli`: `npm test -- --runTestsByPath src/__tests__/commands/channel.test.ts src/__tests__/services/channel/channel.service.test.ts` passed with 25 tests.
- `packages/cli`: `npm test -- --coverage --runTestsByPath src/__tests__/services/channel/channel.service.test.ts --collectCoverageFrom=src/services/channel/channel.service.ts --coverageThreshold='{}'` reported `channel.service.ts` at 92.85% statements, 83.33% branches, 93.33% functions, 92.3% lines.
- `packages/cli`: `npm run build` passed.
- `packages/cli`: `npm run lint` passed with 0 errors and 4 pre-existing warnings outside touched files.
- `packages/channel-connector`: `npm test -- --runTestsByPath src/__tests__/ConfigStore.test.ts` passed with 13 tests.
Loading
Loading