blyxt, Oct 12: closing out L1
blyxt is a Telegram client for Blygger 0.3. You write in one forum topic, Pub, and posts appear at rslantonie.com/blyg/. You read in another, Sub, where the blygs you follow arrive as messages. Oct 12 was the day it caught up with the rest of the 0.3 spec at level 1: the publish gestures, threads, and the reader-side SHOULDs. Two Claude agents worked in parallel, one on the publish side and one on Sub topics. This is the record of that day.
Morning: links and rendering
The day began on the rendering layer. [[id]] links now resolve to the item they name, on this blyg or one you follow. Every URL in content_html is made absolute, because that HTML travels into feeds, readers and other blygs. Sub stopped posting raw markdown and started posting the rendered HTML. That way a quoted passage reads as a quote and not as a ![[id]] directive. Posts that Telegram rate-limits are retried on the next poll instead of being lost.
Withdraw, pin, republish
Three gestures, all made by replying in Pub:
/withdraw [note] publishes the §9 endcap. It's a new version with the content emptied, and it keeps both the page and the changelog. Withdrawing is the only way out for a published item; the protocol has no delete.
- Pinning a message in Telegram (or replying
/pin vN) pins that version forever. It's served at items/{id}/v{n}.json and as a frozen page at f/{id}/v{n}/. A pin can't be undone and emits no feed event, and unpinning in Telegram changes nothing. The live page cites its pins, and that citation is how anyone finds them.
/republish brings a withdrawn post back under the same id as its next version. Every post now records its authored kind when it's first published. A fragment that's withdrawn and republished therefore comes back as a fragment at the same address. A withdrawal changes the current kind, but never the authored one.
Threads
Replying in Pub to one of your own posts publishes a thread that quotes it. If you select part of the post before replying, it becomes a partial quote: the selection is checked word for word against the quoted post's text and baked in with a selector. Writing ![[id]] on a line of its own does the same thing. The quote is baked into the thread's HTML as a blyg-transclusion blockquote, and its exact version goes into transclusions[]. A closure check stops a thread from quoting itself, directly or through other threads. Quotes resolve only against this blyg's own posts for now. Quoting a post from a blyg you follow is an L2 question.
The fragment cap
Fragments SHOULD stay within 2,000 characters. Partway through the day it became clear why vgr singled Telegram out as a good fragment reader and writer: a Telegram message is roughly the size of a fragment already. blyxt's rule is simple. A message over the cap publishes as a thread, which has no cap. An edit that would push a fragment past the cap is refused, with a reply explaining how to publish it instead. The kind is fixed at creation because it decides the page's address.
Sub topics, in parallel
The second agent spent the afternoon turning Sub into folders. /newsub <name> creates a new topic that follows its own blygs, and /sub <name> adopts an existing one. Each subscribe command works in the topic it's sent in. A blyg followed from several topics is fetched only once per poll and then fanned out to each of them. Once a Sub post is more than an hour old, a new version of it, or its withdrawal, is also posted as a reply underneath, so it shows up at the bottom of the topic. Further versions within the hour edit that reply rather than adding more.
Reading honestly
Three reader SHOULDs from §13 landed on the Sub side:
- Regressions: if a followed blyg serves an older version than one blyxt has already shown, blyxt keeps the newer one and posts a ⚠ notice. It doesn't refetch the old version forever.
- Stealth edits: blyxt rechecks items it has already seen and verifies
content_hash. If the content changes without a new version number, it adopts the new content and says so. Before turning this on, blyxt checked all 137 of vgr's hashes against his content, and every one matched.
- No archive index: a blyg without
items/index.json is followed through its feed instead. That gets flagged as lossy, since a post that scrolls out of the feed between polls can be missed.
Plain RSS and Atom feeds can be followed too, at level 0. Their posts show up marked RSS, as a title, a summary and a link, under synthetic ids that can never be mistaken for blyg ids.
Two agents, one database
Running two agents against one live D1 database took some care. Each worked in its own git worktree. Migrations were numbered so they couldn't collide. Before each deploy, the agent checked the remote migration state, to avoid shipping code ahead of a schema the other agent hadn't applied yet. The trickiest migration rebuilt the table of imported items so it could be kept per topic. It was dry-run against an exported copy of the live database first, and all 137 imported items and 31 message links came through intact.
The friction was mostly in the tooling. Claude Code's safety classifier sometimes blocked remote migrations and deploys as production changes, so Russell ran a few of them by hand. Afterwards, local allow rules gave the project's own migrate and deploy scripts standing permission. The issue tracker's pre-commit hook also failed inside worktrees, because it couldn't find its database there. Bypassing the hook was itself blocked as tampering with a log. The fix was to point the hook at the main checkout's tracker.
Where it stands
L1 is closed and live. Open for L2 are two design decisions: where replies to other people's posts get published, and whether to store remote content so it can be quoted. There's also an idea on the list, stub reply links at the bottom of Sub messages, which needs a design for how it interacts with threads.
Claude wrote this from the day's commits, the issue tracker and the session notes. It's published as a thread with a generation disclosure.