discord-bot: fix "no response", serialize turns, attribution, mentions, post-at-bottom

client.py: send() falls back to ResultMessage.result when no TextBlock streams
(the "(no response)" bug) and reconnects+retries once on a closed SDK session.

message_handler.py: per-thread turn lock so messages arriving mid-turn or from a
second user queue in order (nothing dropped); per-session requester-attribution
env (discord_id -> users.json key), pinned to the thread opener; _USER_MAP caches
only on a successful load; final answer posts as a fresh message at the BOTTOM
(no edit-in-place); a <@id> tag goes out as a fresh send so it actually pings.

main.py: allowed_mentions permits user pings, blocks @everyone/@here/roles.

DISCORD_CLAUDE.md: no thread auto-delete; tiered close-out (Q&A -> one-line rolling
log, substantive -> /save); @mention guidance; opener-pinned attribution note.

whoami-block.sh / sync.sh: bot-context attribution (Executed by ClaudeTools Bot /
Requested by <person>; git author = mapped requester, committer = bot). Strict
no-op for interactive sessions.

users.json: discord_id for Mike/Howard; added Winter Williams (bot-only, full trust).

Reviewed by Code Review Agent + Grok + Gemini (Gemini's "malformed email" finding
verified as a false positive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 21:00:20 -07:00
parent 7fc29a7c5f
commit 2efd4a4fb3
7 changed files with 264 additions and 37 deletions

View File

@@ -98,34 +98,41 @@ For every request, work this loop:
name, ID) to determine who is asking, and address them by name.
2. **Do the work** — perform the action or answer the question. Ask clarifying questions in
the thread as needed; the session persists, so the conversation continues naturally.
3. **Anything else?** — when the task is done, ask: "Anything else for this one?" Keep
handling follow-ups in the same thread until the requester is satisfied.
4. **Offer Syncro** — once they have nothing else, ask whether to log the work in Syncro
("Want me to log this in Syncro?"). If yes, invoke `/syncro` to create or update the ticket.
5. **Save** — after the loop closes, run `/save` to write the session log and sync the repo.
6. **Kill the thread** — after `/save` completes successfully, delete the thread:
```bash
bash C:/Users/guru/ClaudeTools/projects/discord-bot/scripts/delete-thread.sh <Thread ID>
```
The Thread ID is in every `[DISCORD_CONTEXT]` block as `Thread ID: <id>`. Do not delete
if `/save` failed or errored. Do not post a closing message — the deletion is immediate.
3. **Anything else?** — when the task is done, ask "Anything else for this one?" and keep
handling follow-ups in the same thread. A directly-connected second topic stays in the
SAME thread/session; only a genuinely unrelated request warrants a fresh thread.
4. **Close the loop — match the capture to the work:**
- **Pure Q&A / read-only / nothing changed in the repo** → do NOT run `/save`. Append a
one-line entry to the rolling bot log
`session-logs/bot/<YYYY-MM>/<YYYY-MM-DD>-bot-activity.md` (create the month folder if
needed): `HH:MM PT - <requester> - <topic> - <outcome / links>`. The Discord thread holds
the full detail, and the on-disk transcript is recoverable via `/recover` if a full
narrative is ever needed. No Syncro prompt unless the work is billable.
- **Substantive work** (changed a client record, infra, a ticket, or repo files) → offer
Syncro if it is billable/ticketable, then run `/save` to write a full session log to the
correct client/project location.
5. **Sync**`/save` already syncs. For the one-line rolling-log case a `/sync` is enough, and
batching is fine (the periodic sync sweeps it up). Never push empty commits for pure Q&A.
6. **Keep the thread** — never auto-delete. The thread is the conversation record. Delete only
if the requester explicitly asks (`scripts/delete-thread.sh <Thread ID>`).
**Attribution is automatic — do not set it manually.** Each thread runs with session env
(`CLAUDETOOLS_ACTOR=discord-bot`, `CLAUDETOOLS_REQUESTER`, `CLAUDETOOLS_REQUESTER_USER`) derived
from the `[DISCORD_CONTEXT]` sender. So `/save`'s User block renders as "Executed by: ClaudeTools
Discord Bot / Requested by: <them>", and commits are authored as the mapped requester with the
bot as committer. Attribution **pins to the thread opener**: if a second person posts in someone
else's thread, the work is still credited to whoever started the thread (a thread = one person's
request).
---
## Thread Lifecycle — Auto-Delete on Completion
## Thread Lifecycle — Threads Are Kept (No Auto-Delete)
Every `[DISCORD_CONTEXT]` block now includes `Thread ID: <id>` (injected by the bot after
the thread is resolved or created). This ID is what you pass to the delete script.
Threads are the durable conversation record and are **NOT auto-deleted** on completion.
Leave every thread in place after the task and `/save` finish.
**When to delete:** Only after step 6 of the Task Loop — `/save` succeeded, session log
committed and pushed. The thread is the conversation record; don't kill it before the log lands.
**When NOT to delete:**
- `/save` failed or sync errored
- The user said "yes" to continuing (open follow-up items remain)
- The thread is an ongoing informational channel, not a single-task session
**Script:**
**Delete ONLY on explicit request** — if the requester says to delete/close the thread,
pass the `Thread ID` (in every `[DISCORD_CONTEXT]` block) to the delete script:
```bash
bash C:/Users/guru/ClaudeTools/projects/discord-bot/scripts/delete-thread.sh <Thread ID>
```
@@ -275,6 +282,21 @@ This creates an audit trail and keeps the repo in sync.
---
## Tagging a user (@mention)
To actually notify someone in Discord, emit a real mention `<@THEIR_DISCORD_ID>` — NOT
literal "@name" text (plain "@winter" pings no one). Discord IDs live in `.claude/users.json`
under each user's `discord_id`:
- Mike `<@264814939619721216>` - Howard `<@624667664501178379>` - Winter
`<@624666486362996755>` - Rob `<@261978810713505792>`
Read `users.json` for any ID you do not have. Tag only when it serves the task (e.g. "this
needs <@...>'s sign-off") — never gratuitously, and never `@everyone`/`@here` (the bot blocks
those). A reply containing a tag is posted as a fresh message so the ping actually lands.
---
## Local Machine Rules (BEAST)
- Working directory: `C:/Users/guru/ClaudeTools`