---
phase: 38-telegram-bridge
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - server/package.json
  - server/src/services/telegram.ts
  - server/src/routes/telegram.ts
  - server/src/app.ts
autonomous: true
requirements: [TGRAM-01, TGRAM-02, TGRAM-05, TGRAM-06]

must_haves:
  truths:
    - "A text message sent to the Telegram bot produces an agent reply prefixed with the agent name"
    - "The bot runs via long polling with no public HTTPS endpoint"
    - "The telegram.ts service file is under 500 lines"
  artifacts:
    - path: "server/src/services/telegram.ts"
      provides: "grammY bot lifecycle, text message relay, session map, agent prefix"
      exports: ["telegramService"]
    - path: "server/src/routes/telegram.ts"
      provides: "POST /api/telegram/token validation, GET /api/telegram/status"
      exports: ["telegramRoutes"]
  key_links:
    - from: "server/src/services/telegram.ts"
      to: "server/src/services/chat.ts"
      via: "chatService(db).createConversation, addMessage"
      pattern: "chatService.*createConversation|addMessage"
    - from: "server/src/services/telegram.ts"
      to: "server/src/services/puter-proxy.ts"
      via: "puterProxyService(db).chatStream async generator collection"
      pattern: "puterProxy.*chatStream"
    - from: "server/src/app.ts"
      to: "server/src/services/telegram.ts"
      via: "conditional start on telegramToken presence"
      pattern: "telegramService|tg\\.start"
---

<objective>
Create the Telegram bridge service and management routes. Install grammY, implement the telegramService factory function with text message relay (user -> agent -> reply with [AgentName] prefix), session mapping, long polling lifecycle, and token validation endpoint.

Purpose: This is the core Telegram relay infrastructure. Text messaging must work end-to-end before voice can be layered on (Plan 02).
Output: `server/src/services/telegram.ts`, `server/src/routes/telegram.ts`, updated `server/src/app.ts`
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/38-telegram-bridge/38-CONTEXT.md
@.planning/phases/38-telegram-bridge/38-RESEARCH.md
@.planning/REQUIREMENTS.md

<interfaces>
<!-- Key types and contracts the executor needs -->

From server/src/services/chat.ts:
```typescript
export function chatService(db: Db) {
  return {
    async createConversation(companyId: string, data: { title?: string; agentId?: string }),
    async addMessage(conversationId: string, data: { role: string; content: string; agentId?: string; ... }),
    async listMessages(conversationId: string, opts: { cursor?: string; limit?: number }),
    // ...
  }
}
```

From server/src/services/puter-proxy.ts:
```typescript
export function puterProxyService(db: Db) {
  return {
    async* chatStream(
      companyId: string,
      agentId: string | null | undefined,
      messages: unknown[],
      model: string | undefined,
      signal: AbortSignal | undefined,
    ): AsyncGenerator<string>,
    async resolveToken(companyId: string): Promise<string>,
  }
}
```

From server/src/services/nexus-settings.ts:
```typescript
export function nexusSettingsService() {
  return {
    async get(): Promise<NexusSettings>,  // includes telegramToken?: string
    async set(patch: Partial<NexusSettings>): Promise<NexusSettings>,
  }
}
```

From server/src/services/agents.ts:
```typescript
// agentService(db).list(companyId) returns agents with .name property
```

From server/src/services/companies.ts:
```typescript
// companyService(db).list() returns companies array
```

Route mounting pattern in app.ts:
```typescript
api.use(someRoutes(db));  // mounts under /api prefix
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install grammY and create telegramService + telegramRoutes</name>
  <files>server/package.json, server/src/services/telegram.ts, server/src/routes/telegram.ts</files>
  <read_first>
    - server/src/services/chat.ts (createConversation, addMessage signatures)
    - server/src/services/puter-proxy.ts (chatStream async generator pattern)
    - server/src/services/agents.ts (list method, agent.name field)
    - server/src/services/companies.ts (list method)
    - server/src/services/nexus-settings.ts (get/set, telegramToken field)
    - server/src/routes/nexus-settings.ts (route pattern reference)
    - server/src/routes/authz.ts (assertBoard helper)
  </read_first>
  <action>
1. Install grammY: `cd server && pnpm add grammy`

2. Create `server/src/services/telegram.ts` — factory function `telegramService(db: Db)`:
   - Import `Bot, InputFile` from "grammy", plus chatService, agentService, companyService, puterProxyService, nexusSettingsService, logger
   - In-memory `sessionMap: Map<string, string>` for `${chatId}:${agentId}` -> conversationId
   - `getOrCreateConversation(chatId, agentId, db)` — looks up Map, creates via chatService if missing
   - `resolveDefaultAgent(db)` — gets first company via companyService(db).list(), then first agent via agentService(db).list(companyId). Returns `{ companyId, agentId, agentName }`. If no agents, returns null.
   - `bot.on("message:text", async (ctx) => { ... })` handler:
     - Resolve agent via `resolveDefaultAgent(db)`. If null, reply "No agents configured" and return.
     - Get/create conversation via `getOrCreateConversation`
     - Persist user message via `chatSvc.addMessage(convId, { role: "user", content: ctx.message.text })`
     - Build messages array from `chatSvc.listMessages` (last 20 messages, map to `{role, content}` format for LLM)
     - Collect full response from `puterProxyService(db).chatStream(companyId, agentId, messages, undefined, undefined)` by iterating the async generator
     - Prefix reply with `[AgentName]: ` (TGRAM-02)
     - Split at 4000 chars if reply exceeds 4096 (Telegram limit) — send multiple messages
     - Persist assistant message via `chatSvc.addMessage`
     - Send reply via `ctx.reply(text, { parse_mode: "Markdown" })`
     - Wrap in try/catch — on error reply "Sorry, something went wrong."
   - `bot.catch((err) => logger.error({ err }, "Telegram bot error"))` before start
   - `start(token: string)`: create Bot, call `bot.api.deleteWebhook()` first (Pitfall 4), register handlers, call `bot.start()` (do NOT await — never-resolving promise). Save bot reference.
   - `stop()`: await `bot?.stop()`, set bot to null
   - `isRunning()`: returns `bot !== null`
   - Return `{ start, stop, isRunning }`
   - CRITICAL: Keep under 500 lines total (TGRAM-06). Target ~250 lines for text-only; voice handlers will add ~150 in Plan 02.

3. Create `server/src/routes/telegram.ts` — `telegramRoutes(db: Db)`:
   - `POST /telegram/token` — assertBoard, validate token string from body, create temp `new Bot(token)`, call `bot.api.getMe()` to validate (catch -> 400 "Invalid Telegram bot token"). On success save via `nexusSettingsService().set({ telegramToken: token })`. Return `{ ok: true, botUsername: me.username }`.
   - `GET /telegram/status` — assertBoard, return `{ running: boolean }` from the telegram service instance. To access the service instance, accept it as a second parameter: `telegramRoutes(db, telegramSvc)`.

  </action>
  <verify>
    <automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "grammy" server/package.json
    - grep -q "export function telegramService" server/src/services/telegram.ts
    - grep -q "export function telegramRoutes" server/src/routes/telegram.ts
    - grep -q "bot.on.*message:text" server/src/services/telegram.ts
    - grep -q "AgentName\|agentName" server/src/services/telegram.ts
    - grep -q "bot.start()" server/src/services/telegram.ts
    - grep -q "deleteWebhook" server/src/services/telegram.ts
    - grep -q "getMe" server/src/routes/telegram.ts
    - wc -l < server/src/services/telegram.ts | awk '{exit ($1 > 500)}'
  </acceptance_criteria>
  <done>telegramService factory function exists with text relay handler, agent prefix, long polling lifecycle. telegramRoutes has token validation and status endpoints. grammY installed. File under 500 lines.</done>
</task>

<task type="auto">
  <name>Task 2: Wire telegram service and routes into app.ts startup</name>
  <files>server/src/app.ts</files>
  <read_first>
    - server/src/app.ts (full file — understand createApp structure, route mounting, return value)
    - server/src/services/telegram.ts (the file created in Task 1)
    - server/src/routes/telegram.ts (the file created in Task 1)
  </read_first>
  <action>
1. In `server/src/app.ts`:
   - Add imports: `import { telegramService } from "./services/telegram.js"` and `import { telegramRoutes } from "./routes/telegram.js"`
   - Inside `createApp` function, after all route mounting but before the catch-all 404:
     - Create telegram service instance: `const tg = telegramService(db);`
     - Mount routes: `api.use(telegramRoutes(db, tg));`
     - Read settings and conditionally start: `const settings = await nexusSettingsService().get(); if (settings.telegramToken) { tg.start(settings.telegramToken).catch(err => logger.error({ err }, "Failed to start Telegram bot")); }`
     - Add `nexusSettingsService` import if not already present
   - Add `tg` to the return value of createApp (or store as module-level ref) so the token route can restart it after saving a new token

2. In `server/src/routes/telegram.ts`, update the POST /telegram/token handler to also (re)start the telegram service after saving the token. The route receives the service instance as parameter — call `await svc.stop()` then `await svc.start(token)`.
  </action>
  <verify>
    <automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "telegramService" server/src/app.ts
    - grep -q "telegramRoutes" server/src/app.ts
    - grep -q "telegramToken" server/src/app.ts
    - grep -q "tg.start\|tg\.stop\|svc.start\|svc\.stop" server/src/routes/telegram.ts
  </acceptance_criteria>
  <done>Telegram service starts automatically on app boot when token is configured. Token validation endpoint restarts the bot after saving new token. TypeScript compiles clean.</done>
</task>

</tasks>

<verification>
- `cd server && pnpm exec tsc --noEmit` — zero errors
- `wc -l server/src/services/telegram.ts` — under 500 lines
- `grep -c "bot.on\|bot.start\|bot.catch" server/src/services/telegram.ts` — at least 3 matches
- `grep "telegramRoutes\|telegramService" server/src/app.ts` — both present
</verification>

<success_criteria>
- grammY installed in server/package.json
- telegramService factory function with text relay, agent prefix, session map, long polling
- telegramRoutes with POST /telegram/token (validates + saves + restarts) and GET /telegram/status
- app.ts conditionally starts telegram bot on boot
- TypeScript compiles without errors
- telegram.ts under 500 lines
</success_criteria>

<output>
After completion, create `.planning/phases/38-telegram-bridge/38-01-SUMMARY.md`
</output>
