Back to all writing

Field notes

One Hashtag Leads Back to the Conversation

Two pull requests merged on Sunday evening turn an ordinary hashtag in Hayate Agent into a durable pointer back to the exact message, and make late worker results carry the tag of the request that started them. What it stores, where the parser says no, and what was released.

One Hashtag Leads Back to the Conversation

秋の宵 しるしひとつで 戻り道

Autumn evening. With a single mark, the way back.

Say a message to Hayate Agent, the harness I build in public (formerly the VanillaCore Agent), mentions a release on Monday, and by Wednesday that conversation sits behind a fresh session. Until Sunday evening, getting back to it meant paging through /sessions, a list of session titles that, unless someone named the session, are the first 90 characters of its opening message. A second problem shows up when the agent hands a job to a background worker: the result can arrive after the chat has moved on to something else, and nothing in the reply said which request it answered.

Two pull requests merged on Sunday evening make an ordinary hashtag do both jobs. A tag like #release in my own message becomes a durable pointer to that exact message, and replies to a tagged request carry the same tag, including the late worker result. What I like most about it is how little it does. The pull request quotes the request it implements, "Mehr ist doch gar nicht nötig" (that is all it takes), and then lists what it refuses to build: no topic command, no routing, no automatic focus switching, no generated summaries.

Cover image: the words One hashtag in the AI agent I build now leads back to the exact message. Nothing more. On purpose. in white, the last line in muted light blue, above a small stack of closed dark notebooks on a dark navy desk, with one muted light-blue ribbon bookmark hanging from the middle of the stack.

A pointer, not a copy

Core pull request #206, merged at 18:22, adds a small index to the state the harness already keeps on disk. When a fresh message from a chat reaches the model's real transcript and contains a tag, the harness stores four things: the tag, the session ID, the ID of that transcript entry and the time it indexed it. It does not copy the message text and it does not touch memory. The index belongs to one conversation, so a different room or DM sees none of it.

Two commands read the index. /hashtags lists every tag the conversation has collected, and /hashtags #release looks up each referenced message in the retained session history and prints a date, an excerpt of up to 180 characters, the session ID and the /resume command that reopens that session. The lookup itself opens nothing. The person decides whether to resume, and the active session stays as it was.

The AI agent that drafts these changelog posts ran this path locally at the window-end commit 2a1269e, with the real session storage, saved harness state and command handling, and a scripted stand-in only for the model call (the same technique the pull request's own test uses). It sent one tagged message, started a new harness from the saved state and asked for the tag:

> Check the #release notes before Friday
#release · Noted, I will check the release notes.

(harness restarted from saved state)

> /hashtags
#release

> /hashtags #release
2026-11-01T19:34:13.185Z · 01a1989a-ef9c-7019-b7d8-d2f4f3206481
  Check the #release notes before Friday
  /resume 01a1989a-ef9c-7019-b7d8-d2f4f3206481

The timestamp is UTC. After the lookup, no session was active in that conversation. The #release · in front of the first reply comes from the second pull request.

The reply remembers what it was answering

Core pull request #207, merged at 19:05, stores the tags of a request with the request itself, in the saved inbox, in the task and in the record of a job handed to a worker. When the answer is delivered, the harness puts a sorted prefix in front of it, for example #hashtags #rollout · , and it does this at delivery time, so a retry after a restart adds the prefix once and not twice. The model only receives the current request's tags as information, together with the instruction not to write a prefix itself.

The worker case is the one I care about. A worker's question, its status updates and its late result are labeled with the tags of the request that started that worker, looked up from the stored job record, never from whatever the conversation is about when the result arrives. The regression tests check exactly that: a job started under #old delivers its question, status and result as #old · ... after a newer #new request has run, and a following untagged message gets an unlabeled reply. If the returned text already starts with a different tag prefix, the harness replaces it on a tagged request instead of stacking a second one.

Labels are not permissions. Only fresh chat messages count as a source of tags. Requests that arrive through the HTTP run API stay untagged even when they claim a tag, worker output and host results never create tags, generated labels never enter the index, and the transport between the harness and its workers keeps the raw text.

Where the parser says no

Tags are deliberately boring: a # at the start of a line or after whitespace, an ASCII letter, then letters or digits with optional single hyphens between the parts, followed by whitespace, the end of the line or ordinary punctuation. #release and #content-fix work, C#lang does not. The parser skips code blocks, quoted lines, inline code, text in quotation marks, Markdown links and XML-style source blocks. The documentation is upfront about the limit: pasted plain prose looks exactly like typed prose, so it can still plant a tag.

That caution has a visible cost. When a quotation mark has no partner on the same line, the parser ignores the rest of that line, and an apostrophe counts as a quotation mark. The local run shows it: "Check the #release notes" yields the tag, "Let's check the #release notes" yields nothing, because the apostrophe in "Let's" swallows everything after it. The documentation's advice is to put intentional tags on a plain line. It also says that the parser cannot tell pasted plain prose from text a person wrote, that earlier history and mid-turn steering are not indexed after the fact, that a session provider has to keep the referenced history for a lookup to work, and that in a shared room the index follows the room's existing access boundary, not a private history per sender. A missing session or message produces an explicit "unavailable" line, not a guessed summary. The pull request also lists one known review finding it left open: a longer Markdown code fence can be closed by a shorter one of the same character.

What was tested, and what was released

The two pull requests add two test files with six tests between them. At v0.1.135 all six pass in the local run above. Against the release before #206, the new tests cannot even load, because the module they test did not exist yet, so that comparison only shows that the feature is new, not how it behaves. The full-suite counts in the pull requests, 1,185 of 1,185 for #206 and 1,188 of 1,188 for #207, are the author's report, as is the independent review both describe.

#206 is in release v0.1.134, published at 18:29, and #207 in v0.1.135, published at 19:11; each tag sits on that pull request's merge commit. A release is not a running agent. #207 itself says that activation depends on a separate rollout of v0.1.134 and claims no live activation, so this post makes no claim about which installation runs the feature.

The rest of the day

The rename landed in the morning. Core #199 changes the product name in the user interface, help texts, documentation and templates to Hayate Agent and, according to the pull request, keeps the technical names of the command, package, configuration, protocol, image and repository compatible. #200 adds the project image to the README and documents the canonical GitHub repository URLs.

In the afternoon, core #201 and plugins #33 brought native end-to-end encrypted Matrix messaging into the existing Matrix adapter, and core now pins the plugin commit that carries it. Both pull requests state that no live encrypted exchange has been accepted yet and that the author's full macOS suite still had three failures, two of which also fail on the previous release. Plugins #34 adds an optional allowlist of Matrix rooms the agent may join and serve, where an explicitly empty list denies all rooms and an unset one keeps the old behavior; core #202 adds six tests for it and moves the plugin pin to that revision. Neither pull request activates anything.

Three stacked merges at 17:00 to 17:06 deal with configuration files: #203 adds trusted persona and settings overlay files, #204 reloads plugin overlay files when the harness is idle, and #205 refreshes model defaults from files at prompt and review boundaries. Early in the window, core #197 with plugins #32 lets an operator choose either the harness or the Pi agent runtime as the single owner of automatic context compaction and keeps bounded evidence for completion checks, and #198 lets authorized recovery work continue when the completion check is uncertain or keeps failing, instead of blocking the task.

What merged and what shipped

All merge and release times are Europe/Berlin. The window runs from 08:00 to 20:00 on Sunday, 1 November 2026, and covers the three public repositories the harness organization listed when checked after the window: core, deployment-base and plugins. Core merged eleven pull requests between 08:41 and 19:05 (#197 to #207), plugins merged #32, #33 and #34, and deployment-base merged nothing. A twelfth core pull request, #208, had the same title as #207 and was closed without merging. Core published ten release tags in the window, v0.1.126 to v0.1.135, and each one resolves to the merge commit of one of those pull requests. Plugins and deployment-base published no releases.

Small on purpose

The tempting version of this feature was a topic system: the agent detects what a conversation is about, switches context, files answers into folders and summarizes the thread when asked. Every one of those steps is a place where the model decides something about my work that I did not ask for. Sunday's version leaves the deciding to the person who typed the tag, the harness only stores a pointer and repeats a label, and the parser would rather miss a tag than invent one. I would take that trade again, and the apostrophe in "Let's" is a fair price for it, as long as the documentation keeps saying so.

Sources used