# botmaster `--attachment` — design

audience: AI coding agents first. Implementer: codex (`-m gpt-5.6-terra -c model_reasoning_effort=medium`).
Owner intent: send the owner files (screenshots, logs, receipts) over Telegram from any session.

## Context

`botmaster` is a bash shim exec-ing `modules/botmaster/notify/send.ts` (bun). Today it sends
text only, via Telegram `sendMessage` (JSON body). Channel → bot token resolution goes through
the D1 `bots` table (`rows()`/`resolveChannel` in `send.ts` + `resolve.ts`). The local SQLite
store (`store.ts`) records every outbound `Message`; `format.ts` renders the identity line.

## Contract

CLI (extend `parseArgs` in `send.ts`):

```
botmaster [--channel <name>] [--fyi | --needs-answer] [--reply <id>] \
          [--attachment <path>]... "<text>"
```

- `--attachment <path>` — repeatable, max 5 per invocation. Requires a non-empty `<text>`
  (the caption/context); `--attachment` with no text is a usage error.
- Composes with `--reply` and priority flags exactly as text does.
- Mutually exclusive with `--inbox` (which takes no message).

Args type delta: `attachments: string[]` (empty when flag absent).

## Behavior

1. **Validation — fail closed, before any network call.** Refuse with a FatalError naming the
   path when an attachment: does not exist, is not a regular file, is unreadable, is empty,
   or exceeds 45 MB (Telegram bot upload cap is 50 MB; leave headroom). Refuse any path that
   resolves (realpath) under `~/.ssh`, `~/.aws`, `~/.config/overdeck`, any path whose basename
   matches the secret shapes already encoded in `SECRET_PATH_PATTERNS` in
   `modules/systray/remote_dispatch.py` (re-state the patterns locally in TS — do not import
   Python), or any file mode `o600` under `$HOME` outside the current repo. One bad attachment
   refuses the whole send: nothing is sent, nothing is inserted into the store.
2. **Transport.** First attachment goes via Telegram `sendDocument` as `multipart/form-data`
   (the existing `telegram()` helper is JSON-only — add a multipart variant beside it, do not
   change the JSON one), with the rendered identity text as `caption` (Telegram caps captions
   at 1024 chars — if the rendered text is longer, send the text first as a normal
   `sendMessage`, then the document with no caption). Attachments 2..5 follow as bare
   `sendDocument` calls replying to the first (`reply_to_message_id`).
3. **Store.** One `Message` row per invocation (unchanged shape) + new table
   `message_attachments (message_id TEXT, seq INTEGER, path TEXT, bytes INTEGER,
   tg_message_id INTEGER, PRIMARY KEY (message_id, seq))` written after each successful
   upload. `Store` gains `recordAttachment(messageId, seq, path, bytes, tgMessageId)` and
   `listAttachments(messageId)`.
4. **Partial failure.** If upload N of M fails, the rows for 1..N-1 stay recorded, the process
   exits non-zero naming which attachment failed; already-sent files are not retracted.
5. The 30-second fyi coalescing path (editMessageText) never applies to a send that carries
   attachments — skip it.

## Out of scope

Inbound attachments (owner → session), image-specific `sendPhoto` rendering, resumable
uploads, the proxy/waker path (attachments ride the direct `send.ts` path only).

## Acceptance

- `/usr/bin/bun test modules/botmaster/notify/` — new tests: refusal per validation rule
  (missing, unreadable, oversize, secret-shaped, empty text), multipart body construction,
  caption-overflow split, store rows recorded per upload, coalescing skipped.
- Live proof: `botmaster --attachment <small png> "attachment self-test"` delivers a document
  to the overdeck channel and `listAttachments` returns one row with a tg_message_id.
