-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add message editing/deletion support with MessageDeliveryService #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Add message_mappings table to track GitHub entities to Towns message IDs - Create MessageDeliveryService combining thread tracking with message lifecycle - Support edit/delete actions for comments, reviews, and review comments - Add _(edited)_ indicator to formatted messages when content is edited - Extract toDeliveryAction helper for mapping webhook actions - Replace ThreadService with unified MessageDeliveryService 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
WalkthroughThis PR replaces the thread-based delivery model with a message-mapping delivery model. It adds a new Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Files/areas to pay special attention to:
Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (2)**/*.ts📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📚 Learning: 2025-11-25T03:24:12.463ZApplied to files:
🧬 Code graph analysis (1)src/services/message-delivery-service.ts (4)
🔇 Additional comments (8)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/services/message-delivery-service.ts (2)
261-321: Timestamp / expiry behavior is sound; consider minor refinements for consistencyThe thread/message mapping helpers look solid: they key off the composite identifiers, gate lookups on
expiresAt, and useonConflictDoUpdateto keep mappings current. That aligns well with the DB schema you defined and keeps state external to the bot process, as per the stateless-architecture guideline. Based on learnings, this is exactly the right direction.Two small, optional refinements:
Reuse a single
nowwhere practicalIn several places you do:
gt(..., new Date())ingetThreadId/getMessageIdconst now = new Date();instoreThread/storeMappingconst now = new Date();incleanupExpired()The tiny time skew between these calls is unlikely to matter, but if you ever refactor or unit test around boundaries, passing a
now: Datedown (or computing it once per call) keeps semantics perfectly consistent.Be explicit about “expired but not yet cleaned up” behavior
Because lookups use
gt(expiresAt, now)and cleanup useslt(expiresAt, now), there’s an intentional “expired but still present” window where rows:
- Are ignored by
getThreadId/getMessageId- But have not yet been deleted by
cleanupExpired()That’s a reasonable design (no edits/deletes to very old messages; periodic janitor cleans them up), but it may be worth a short comment near
expiresAtcalculation orgetMessageId/getThreadIdexplaining that this is intentional, so future maintainers don’t try to “fix” the inequality asymmetry.These are polish-level suggestions only; the current logic is functionally correct.
Also applies to: 352-420
448-513: Cleanup/interval flow looks good; consider minor ergonomics improvementsThe cleanup utilities are straightforward and correct: you delete expired threads and message mappings and log aggregate counts, then schedule a periodic job via
startPeriodicCleanup(). That fits the new persistence model nicely.A couple of optional tweaks you might consider:
Run deletes in parallel
cleanupExpired()currently does twoawait db.delete(...)calls sequentially. They’re independent, so you could shave a bit of latency by running them together:
- const threadResults = await db
.delete(eventThreads).where(lt(eventThreads.expiresAt, now)).returning({ id: eventThreads.id });- const messageResults = await db
.delete(messageMappings).where(lt(messageMappings.expiresAt, now)).returning({channelId: messageMappings.channelId,entityId: messageMappings.githubEntityId,});
- const [threadResults, messageResults] = await Promise.all([
db.delete(eventThreads).where(lt(eventThreads.expiresAt, now)).returning({ id: eventThreads.id }),db.delete(messageMappings).where(lt(messageMappings.expiresAt, now)).returning({channelId: messageMappings.channelId,entityId: messageMappings.githubEntityId,}),- ]);
Not critical, but it keeps the job fast even as tables grow. 2. **Document lifecycle of the interval handle** You already return `ReturnType<typeof setInterval>` so callers can clear it. A brief comment in this method’s JSDoc noting that the caller is responsible for calling `clearInterval()` on shutdown would make that contract explicit and avoid accidental timer leaks. These are nice-to-haves; the current implementation is already serviceable. </blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used**: CodeRabbit UI **Review profile**: CHILL **Plan**: Pro <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 1e69cd6fe6f744911466c13ee910e079ce1eddf2 and 2e8a6d11f4b299d11815c0240dc72b80b9fb7ab5. </details> <details> <summary>📒 Files selected for processing (10)</summary> * `drizzle/0007_tiresome_glorian.sql` (1 hunks) * `drizzle/meta/0007_snapshot.json` (1 hunks) * `drizzle/meta/_journal.json` (1 hunks) * `src/constants.ts` (1 hunks) * `src/db/schema.ts` (1 hunks) * `src/formatters/webhook-events.ts` (3 hunks) * `src/github-app/event-processor.ts` (15 hunks) * `src/index.ts` (3 hunks) * `src/services/message-delivery-service.ts` (1 hunks) * `src/services/thread-service.ts` (0 hunks) </details> <details> <summary>💤 Files with no reviewable changes (1)</summary> * src/services/thread-service.ts </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>📓 Path-based instructions (2)</summary> <details> <summary>**/*.ts</summary> **📄 CodeRabbit inference engine (AGENTS.md)** > `**/*.ts`: Store context externally - maintain stateless bot architecture with no message history, thread context, or conversation memory > Use `<@{userId}>` for mentions in messages AND add mentions in sendMessage options - do not use @username format > Implement event handlers for onMessage, onSlashCommand, onReaction, onTip, and onInteractionResponse to respond to Towns Protocol events > Define slash commands in src/commands.ts as a const array with name and description properties, then register handlers using bot.onSlashCommand() > Set ID in interaction requests and match ID in responses to correlate form submissions, button clicks, and transaction/signature responses > Use readContract for reading smart contract state, writeContract for SimpleAccount operations, and execute() for external contract interactions > Fund bot.appAddress (Smart Account) for on-chain operations, not bot.botId (Gas Wallet/EOA) > Use bot.* handler methods directly (outside event handlers) for unprompted messages via webhooks, timers, or tasks - requires channelId, spaceId, or other context stored externally > Always check permissions using handler.hasAdminPermission() before performing admin operations like ban, redact, or pin > User IDs are hex addresses in format 0x..., not usernames - use them consistently throughout event handling and message sending > Slash commands do not trigger onMessage - register slash command handlers using bot.onSlashCommand() instead > Use getSmartAccountFromUserId() to retrieve a user's wallet address from their userId > Include required environment variables: APP_PRIVATE_DATA (bot credentials) and JWT_SECRET (webhook security token) Files: - `src/constants.ts` - `src/formatters/webhook-events.ts` - `src/index.ts` - `src/services/message-delivery-service.ts` - `src/db/schema.ts` - `src/github-app/event-processor.ts` </details> <details> <summary>**/*.{ts,tsx,js,jsx}</summary> **📄 CodeRabbit inference engine (AGENTS.md)** > Provide alt text for image attachments and use appropriate MIME types for chunked attachments (videos, screenshots) Files: - `src/constants.ts` - `src/formatters/webhook-events.ts` - `src/index.ts` - `src/services/message-delivery-service.ts` - `src/db/schema.ts` - `src/github-app/event-processor.ts` </details> </details><details> <summary>🧠 Learnings (3)</summary> <details> <summary>📚 Learning: 2025-11-25T03:24:12.463Z</summary>Learnt from: CR
Repo: HereNotThere/bot-github PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:24:12.463Z
Learning: Applies to **/.ts : Use bot. handler methods directly (outside event handlers) for unprompted messages via webhooks, timers, or tasks - requires channelId, spaceId, or other context stored externally**Applied to files:** - `src/index.ts` - `src/github-app/event-processor.ts` </details> <details> <summary>📚 Learning: 2025-11-25T03:24:12.463Z</summary>Learnt from: CR
Repo: HereNotThere/bot-github PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:24:12.463Z
Learning: Applies to **/*.ts : Implement event handlers for onMessage, onSlashCommand, onReaction, onTip, and onInteractionResponse to respond to Towns Protocol events**Applied to files:** - `src/services/message-delivery-service.ts` </details> <details> <summary>📚 Learning: 2025-11-18T23:35:49.436Z</summary>Learnt from: shuhuiluo
Repo: HereNotThere/bot-github PR: 26
File: src/db/index.ts:61-69
Timestamp: 2025-11-18T23:35:49.436Z
Learning: Inwebhook_deliveriestable (src/db/index.ts), theinstallation_idcolumn should NOT have a FOREIGN KEY constraint because the table serves as an immutable audit log for idempotency tracking. Records must persist independently even after installations are deleted, and a foreign key would create race conditions when webhooks arrive before installation records are created. The field is intentionally nullable to support webhooks without installation context.**Applied to files:** - `drizzle/meta/0007_snapshot.json` </details> </details><details> <summary>🧬 Code graph analysis (2)</summary> <details> <summary>src/services/message-delivery-service.ts (4)</summary><blockquote> <details> <summary>src/types/bot.ts (1)</summary> * `TownsBot` (8-8) </details> <details> <summary>src/db/index.ts (1)</summary> * `db` (57-57) </details> <details> <summary>src/db/schema.ts (2)</summary> * `eventThreads` (288-323) * `messageMappings` (237-282) </details> <details> <summary>src/constants.ts (1)</summary> * `MESSAGE_MAPPING_EXPIRY_DAYS` (101-101) </details> </blockquote></details> <details> <summary>src/github-app/event-processor.ts (1)</summary><blockquote> <details> <summary>src/services/message-delivery-service.ts (4)</summary> * `MessageDeliveryService` (58-514) * `ThreadingContext` (21-25) * `EntityContext` (31-37) * `DeliveryAction` (9-9) </details> </blockquote></details> </details> </details> <details> <summary>🔇 Additional comments (15)</summary><blockquote> <details> <summary>drizzle/meta/_journal.json (1)</summary><blockquote> `53-60`: **LGTM!** The new journal entry follows the established pattern with sequential index and consistent version formatting. </blockquote></details> <details> <summary>src/constants.ts (1)</summary><blockquote> `96-101`: **LGTM!** The constant is well-documented and provides a clear, configurable expiry period for message mappings. </blockquote></details> <details> <summary>src/index.ts (2)</summary><blockquote> `52-59`: **LGTM!** The wiring correctly replaces ThreadService with MessageDeliveryService. The EventProcessor now receives the delivery service for centralized message lifecycle management. --- `242-244`: **LGTM!** Periodic cleanup is correctly wired for the new delivery service with appropriate logging. </blockquote></details> <details> <summary>src/formatters/webhook-events.ts (3)</summary><blockquote> `170-191`: **LGTM!** The edit handling is correctly implemented with the `_(edited)_` indicator for both thread replies and standalone messages, with appropriate header differentiation. --- `202-230`: **LGTM!** PR review formatting correctly handles both "submitted" and "edited" actions with consistent indicator placement. --- `241-262`: **LGTM!** Review comment formatting follows the same pattern as issue comments, maintaining consistency across the codebase. </blockquote></details> <details> <summary>drizzle/meta/0007_snapshot.json (1)</summary><blockquote> `578-692`: **LGTM!** The schema snapshot correctly captures the `message_mappings` table with the composite primary key, check constraints, and expiry index. The absence of foreign keys is appropriate as mappings should persist independently for audit purposes. </blockquote></details> <details> <summary>drizzle/0007_tiresome_glorian.sql (1)</summary><blockquote> `1-18`: **LGTM!** The migration correctly creates the `message_mappings` table with appropriate constraints and an index for efficient expiry-based cleanup. </blockquote></details> <details> <summary>src/db/schema.ts (1)</summary><blockquote> `233-282`: **LGTM!** The `messageMappings` table schema is well-structured with: - Appropriate composite primary key for unique entity identification - Validation constraints for entity and parent types - Expiry index for efficient cleanup queries - Nullable `githubUpdatedAt` for ordering webhooks and preventing stale overwrites </blockquote></details> <details> <summary>src/github-app/event-processor.ts (5)</summary><blockquote> `49-53`: **LGTM!** The constructor is simplified to only depend on `SubscriptionService` and `MessageDeliveryService`, removing the direct bot dependency. The delivery service now handles all message sending internally. --- `114-127`: **LGTM!** The delivery delegation is clean - formatters are wrapped to accept `isThreadReply` and the service handles all threading, editing, and deletion logic. Using `Promise.allSettled` ensures one channel failure doesn't block others. --- `229-257`: **LGTM!** Issue comment handling correctly: - Uses `toDeliveryAction` for action mapping - Early returns for unsupported actions - Provides complete `entityContext` with parent PR/issue information - Correctly determines parent type based on `pull_request` field presence --- `264-293`: **Consider the edge case for `review.submitted_at`.** The `submitted_at` field might be `null` for draft reviews or certain edge cases. Using `Date.now()` as a fallback is reasonable, but this could cause ordering issues if webhooks arrive out of order. Verify this behavior is acceptable for your use case. --- `397-404`: **LGTM!** The `toDeliveryAction` helper provides a clean mapping from GitHub webhook actions to delivery actions. Returning `null` for unsupported actions enables early exit in handlers. </blockquote></details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
- Use switch statement for explicit action branching, preventing delete/edit without entityContext from falling through to create - Add comments explaining gt/lt expiry asymmetry in lookups vs cleanup - Parallelize cleanup deletes with Promise.all 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.