AGENTS.md@packages/chat-gateway/src/adapters · git:20260918.2f0b7e4 · 2026-09-18 · sha256 7b059b25ec723b75

AGENTS.md@packages/chat-gateway/src/adapters git:20260918.2f0b7e4A

Immutable. This exact content is served forever at /api/v1/blob/7b059b25ec723b75.

# DOX — packages/chat-gateway/src/adapters

Files in this directory. One row per source file. See change: add-chat-gateway.

| File | Purpose |
|------|---------|
| `base.ts` | VENDORED from `@gamalan/pi-gateway@1.10.1` (MIT — see `../../NOTICE`) with ONE additive change: `InteractiveResponse` gained an optional `userId`. The platform-adapter contract: `PlatformAdapter` (initialize/start/stop/sendMessage/editMessage/deleteMessage/setTyping/getStatus/sendInteractive/cleanupInteractive), `BaseAdapter` with its generic text fallback, and the `InteractivePrompt`/`InteractiveResponse`/`PlatformMessage`/`AdapterCallbacks`/`PlatformConfig` shapes. `userId` is REQUIRED for L1/L4 authorization of a click (a group-channel member who is not allowlisted can see the buttons, so rendering is not a grant); missing ⇒ treated as unknown ⇒ refused. Pure — no imports, no side effects. Re-vendor the rest; only this field is intentionally added. |
| `discord-payload.ts` | PURE prompt↔Discord mapping, deliberately free of any `discord.js` import so it is unit-testable with no client and no network. `toDiscordControl` (select→string-select, confirm→Yes/No buttons, input/editor→modal, notify→message, unknown→message, >25 options degrades to message rather than silently truncating), the length-safe round-trippable `customIdFor`/`parseCustomId` codec (inline `p1\|…` under Discord's 100-char budget, else a bounded `p2\|token` memo), `DISCORD_MESSAGE_LIMIT`, and `chunkForDiscord` (splits on the last newline/space; hard-splits an over-long token; never truncates; `join("") === input`). |
| `discord.ts` | The `discord.js` transport implementing the vendored contract — REPLACES upstream's `discord.ts` (which used the ambient global `WebSocket`, derived the bot id from the token prefix, and re-armed a reconnect timer from `onclose` after `stop()`). Transport-only by design (task 2.2): no `child_process`/`spawn`, no `--mode rpc`, no `createServer`/`node:http`/`node:net` — it never learns about sessions or servers. `stop()` sets a stopped flag BEFORE disconnecting so nothing reconnects. `sendInteractive`/`handleInteraction` defers before work that may exceed Discord's 3s ack window (C5/F5) so the client never sees "interaction failed"; every emitted `InteractiveResponse` carries `interaction.user.id` for edge authorization. `initialize()` creates the client + logs in (the server entry MUST call it before `start()`); `allowedChannels` drops non-opted-in guild channels at the adapter; thread messages carry `metadata.parentChannelId`. |
| `__tests__/recording-adapter.ts` | The in-memory L3 fake: a full `PlatformAdapter` that records ordered `sent`/`edited`/`deleted`/`typing`/`interactive`/`cleaned` calls, with `emitMessage`/`emitInteractiveResponse` drivers and `reset()`. Keeps the F/X rows deterministic in CI with no live Discord. |