Skip to content

Add single-worker Discord bot design spec - #40

Merged
jackm43 merged 18 commits into
mainfrom
simplify
Jul 23, 2026
Merged

Add single-worker Discord bot design spec#40
jackm43 merged 18 commits into
mainfrom
simplify

Conversation

@jackm43

@jackm43 jackm43 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

jackm43 and others added 18 commits July 23, 2026 19:11
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fe subpackages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port all 10 slash commands (rag, ragboard, ragspend, ragspendboard,
raghammer, ragunban, undorag, ask, bicture, ragjam) into src/commands as
evobot-style Command objects, replacing the Task 2 dispatch stub with a
real registry. Formerly-enqueued kinds (ask, bicture, ragjam) run their
job in-process behind the type-5 deferred ack. dispatch() ports the guild
allow-list, admin gate (RAG_ADMIN_USER_IDS), AI ban + usage gates, and
catches any thrown handler into a friendly deferred edit so waitUntil
never rejects.

@discordjs/builders crashes on import under the Workers runtime (its
module-load s.nativeEnum reads discord-api-types enums that resolve to
undefined here), so command data is built with a minimal Workers-safe
SlashCommandBuilder that emits the same command JSON via toJSON().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend test/interactions.test.ts with a properly Ed25519-signed
/ragboard slash-command POSTed to /interactions: asserts the 200
{type: 5} deferred ack, then awaits the waitUntil'd dispatch and
asserts the deferred-reply PATCH to the Discord webhook edit endpoint
was made with the expected leaderboard content, against a real D1
instance seeded with a rag_totals row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the AI Gateway cost reconciliation from packages/discord/ai/spend.ts as a
batch sweep (src/lib/ai/reconcile.ts): select pending rag_ai_spend_events, look
up real cost in the AI Gateway logs via the Cloudflare API (CLOUDFLARE_API_TOKEN),
then UPDATE estimated_cost_micros and re-derive rag_ai_spend_totals exactly as the
old queue consumer did. Rows whose log has not appeared stay pending. Runs from
scheduled() each cron tick, replacing the retired spend queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the Task 1 stub with the real DiscordGateway Durable Object ported from
apps/gateway/src/gateway.ts (identify/resume, heartbeat, op 7/9, READY, watchdog
alarm, start/stop/ensureConnected). On MESSAGE_CREATE the old cross-script
INTERACTION_SESSION.runMention hop is replaced by in-DO processing under a
floating promise with full error containment, deduped by processed:<messageId>
storage keys (plus a synchronous in-memory guard) pruned after 24h by the
watchdog alarm.

Port the mention filters + resolution + channel/thread reply execution to
src/events/messageCreate.ts (from domain/mention.ts + consumer.ts). Wire the
operator control routes (POST /gateway/start|stop, GET /gateway/health) gated by
GATEWAY_CONTROL_TOKEN and the scheduled ensureGatewayConnected + spend sweep into
src/index.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ortable

Split the .md text imports out of config.ts into lib/ai/prompts.ts and load
them via a dynamic import() on the KV-fallback path only. Workers still
bundles the .md files fine; Node (tsx running scripts/register-commands.ts)
can now import src/commands/index.ts without a .md loader, since the dynamic
import only executes inside loadConfig's fallback branch, which the
registration script never calls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/register-commands.ts now imports the commands map from
src/commands/index.ts and builds the registration payload from
[...commands.values()].map(c => c.data.toJSON()) instead of a duplicated
hand-written SlashCommandBuilder list. Payload building is factored into an
exported buildCommandPayload() so it can run (and be tested) under Node
without hitting Discord; the PUT-empty-global / PUT-guild-set REST v10 flow
for guild 457689460096630794 is unchanged. Route strings are inlined rather
than imported from discord.js, since discord-api-types' runtime enums crash
under the workerd-backed vitest pool this suite runs in.

test/register-payload.test.ts locks the derived payload to the 10 registered
command names and spot-checks options (ask's prompt, rag's user, ragjam's
prompt+lyrics) against what the old hand-written builders produced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the old pnpm-workspace multi-app platform (apps/, packages/,
pnpm-workspace.yaml, deploy.sh, and the platform-only scripts/tests) now
that the single ragbot-worker at the repo root replaces it. Prune
package.json to the deps the surviving worker actually uses, restore a
plain tsconfig include (src/test/scripts), regenerate the stale
worker-configuration.d.ts, trim .env to the op refs still needed, and
rewrite README/AGENTS for the single-worker layout.

pnpm install && pnpm run check && pnpm test all green (85 tests); wrangler
deploy --dry-run builds. Deployed Cloudflare resources from the old
platform are untouched here and decommissioned separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zod had no import anywhere in src/, scripts/, test/, or config — leftover
from the old platform. Removed from package.json and refreshed the
lockfile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iew)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A leftover DiscordGateway object from a pre-'-v2' singleton name still had
gatewayEnabled=true and a live watchdog alarm chain, so it maintained a second
Discord session. The old architecture masked this because all sessions deduped
through InteractionSession.claim(); the collapsed worker dedupes per-object, so
every mention got two replies. Any instance whose id is not
idFromName(discord-gateway-v2) now wipes its storage and alarm on construction
and on alarm, and never connects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jackm43
jackm43 merged commit 252bd2f into main Jul 23, 2026
1 check passed
@jackm43
jackm43 deleted the simplify branch July 23, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant