Read a message straight from its Discord link - #90
Draft
lucasrcezimbra wants to merge 1 commit into
Draft
Conversation
messages_fetch now takes either the stored `messageId` a reader handed out or a `messageLink` copied out of Discord, exactly one of the two. A pasted link resolves to the ingested message it names and then follows the same fetch it always did, so telemetry, statuses and copy are unchanged. The three gates a link passes — the host and shape check, the snowflake check, and the configured-server check — and their exact wording now live in one place, messageLinkTarget in messages.common.ts, which bookmarks_add consumes too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lucasrcezimbra
force-pushed
the
issue-88-fetch-by-message-link
branch
from
August 18, 2026 00:26
e2cedf7 to
cc9b3ac
Compare
lucasrcezimbra
marked this pull request as ready for review
August 18, 2026 00:45
This was referenced Aug 18, 2026
lucasrcezimbra
marked this pull request as draft
August 18, 2026 00:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #88
The problem
Somebody pastes a Discord message link into a chat and asks "what does this say?". Until now the assistant could not answer in one call.
messages_fetchonly took the internalmessageIdthatmessages_catch_up,mentions_listorbookmarks_listhand out, so from a bare link the assistant had to list the channels to turn the link's channel snowflake into a storedchannelId, derive a timestamp from the message snowflake, catch up from that timestamp, scan the results for the matchingdiscordMessageId— 200 messages at a time, in a busy channel — and only then fetch. Indirect, token-heavy, and easy to get wrong.What changed
messages_fetchnow accepts either locator, exactly one of the two:{ "messageId": "01a0…-…" } { "messageLink": "https://discord.com/channels/<server>/<channel>/<message>" }A link resolves to the ingested message it names —
messages.discord_message_idis unique across the store, so the message snowflake alone is the key — and then follows the fetch that already existed, unchanged: the same request, retrieval, skip and failure rows, the same statuses, the same copy. No new Discord operation, no new table, no new reason.What the owner reads when a link cannot be used, each naming the fix:
messageIdfrom messages_catch_up, mentions_list or bookmarks_list, ormessageLinkcopied from Discord with Copy Message Link — one of the two, never both"The first three are exactly what
bookmarks_addhas always answered, because they now come from the same source.bookmarks_additself is unchanged, including its own not-ingested wording.One source for the link's gates, and why that diverges from the usual advice
The link's three gates — host and shape, the three snowflakes, the configured server — were inlined in
addBookmarkByLink. They now live inmessageLinkTargetinapp/business/messages.common.ts, and bothbookmarks_addandmessages_fetchcall it.The
business-folderskill prefers a private copy per file for a utility with two consumers, and extraction only at three or more. That preference is about helpers, and what is shared here is not a helper: it is three sentences the owner reads. A forked copy of a refusal is a product defect no type checker can see — two wordings for one situation, drifting the first time either is improved, and the owner has no way to tell which tool taught them the wrong phrasing. The same reasoning moved themessageLinkfield's Zod message intomessages.common.ts, whichbookmarks.common.tsnow imports.It lives in
.common.ts, not in a.server.ts, on purpose: the parity test reads every exported function inapp/business/*.server.tsas an owner-facing capability, so exporting a parser from there would demand aparity-exemptions.tsentry that is false by that test's own standard.messages.common.tsis also the right domain — identifying a message by its link is a fact about messages, which is whybookmarks.server.tsalready imported from it.fetchMessageSchemastays a plainz.objectwith both fields optional, and the exactly-one rule is enforced infetchMessage, where the locators are resolved and the error can speak in fetch's own vocabulary.createThreadSchemaputs its sibling rule (channelIdxormessageId) in a.refine()instead; this shape is the more robust of the two, becauseapp/mcp/field-messages.test.tswalks a tool's fields only when its schema is aZodObject— true through a refinement in Zod 4 today, but true by construction here.How it was verified
fetchMessage: a link answers byte for byte what the stored id answers (both fetches deep-compared), a canary-host link works, each of the three shared gates refuses, a link nobody ingested refuses, and both-locators and neither-locator refuse without touching Discord or writing a telemetry row. One more at the MCP layer provesmessages_fetchis callable with a link throughcallTool.bookmarks_add's existing suite stayed green against the shared parser.messages_fetchwith nothing but a link built by the seed, and reads the message back; it then passes both locators and asserts the refusal and that Discord was never asked a second time.pnpm run db:migrateandpnpm run db:seed:dev: a link assembled from the seeded snowflakes resolved to the right stored message and came backfailedwith "Discord refused to hand this message over, so nothing was read." — the demo token is not a real bot token, which is exactly the mapped copy for that — while the malformed, wrong-server, never-ingested, both-locator and neither-locator calls each came back with the sentence quoted above.pnpm run lint,pnpm run tsc,pnpm run test:unit(597 passing),pnpm run test:e2e(59 specs, 20 of 20 tools reached),pnpm run test:seed-coverage(17 demonstrated, 3 unseedable) all green.messages_fetchstays declared unseedable: it still reads live from Discord, whichever locator names the message.Docs updated in the same change: the README tool table and its escape-hatch note, and the architecture document's tool list plus a new paragraph in "The live escape hatch" recording why an un-ingested link is refused rather than fetched blind.