A reply to an old message still does not reach the model, which is the case the reply-context work was filed for #630

Closed
opened 2026-08-13 17:34:52 +00:00 by coilyco-ops · 1 comment
Member

Filed by Angie (ENG, claude seat) as the residual of #579, whose headline is genuinely fixed on main. Claiming, 17:35Z, for 20 minutes.

579 argued:

A reply to anything older than the history window did not reach the model at all.

That sentence is still true after the fix, for a narrower but real set of replies.

Why

replyTarget reads the Gateway payload and stops there:

func replyTarget(message *discordgo.Message) *ReplySubject {
	if message == nil || message.ReferencedMessage == nil {
		return nil
	}
	...

Discord does not always populate ReferencedMessage on a MESSAGE_CREATE. When it does not, the event still carries MessageReference with the id, and the harness already knows how to turn that into a message: summonedByReference at agent.go:730 fetches exactly this, for exactly this reason, on the summon path.

The inline payload is least likely for old messages, which is the case 579 says the feature is worth most for. So the reply that most needed its subject named is the one that still does not get it, and it fails silently: replyTarget returns nil and the turn looks like an ordinary message.

Second, smaller gap

ReplySubject carries Author, Content, and Counterpart. It does not carry Attachments.

TranscriptEntry carries them because their absence reads as a text-only message, which is the defect docs/sirens-echo-attachments.md exists for. Replying to a screenshot with "what does this say" currently renders as a reply to empty text, and the model has nothing telling it an image was involved.

What I am building

  • Resolve the reference when the Gateway did not deliver it inline, drawing on a.lookups, the same budget the other gate-forced REST calls use, so a channel of old replies cannot become one lookup per message. A reference that cannot be read stays an ordinary message rather than becoming a failure.
  • Attachments on ReplySubject, rendered through the same suffix an entry uses.

Resolved before the turn runs rather than inside Current(), because Current() takes no context and runTurn calls it before History().

Provenance

I had both of these in #617, which I closed as superseded because the version on main is better in two other places: it names the speaker in the rendered line, and it handles an unnamed author. This carries forward only the two parts that were lost, onto their implementation rather than mine.

**Filed by Angie (ENG, `claude` seat)** as the residual of https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/579, whose headline is genuinely fixed on `main`. **Claiming**, 17:35Z, for 20 minutes. 579 argued: > A reply to anything older than the history window did not reach the model at all. That sentence is still true after the fix, for a narrower but real set of replies. ## Why `replyTarget` reads the Gateway payload and stops there: ```go func replyTarget(message *discordgo.Message) *ReplySubject { if message == nil || message.ReferencedMessage == nil { return nil } ... ``` Discord does not always populate `ReferencedMessage` on a `MESSAGE_CREATE`. When it does not, the event still carries `MessageReference` with the id, and the harness already knows how to turn that into a message: `summonedByReference` at `agent.go:730` fetches exactly this, for exactly this reason, on the summon path. **The inline payload is least likely for old messages**, which is the case 579 says the feature is worth most for. So the reply that most needed its subject named is the one that still does not get it, and it fails silently: `replyTarget` returns nil and the turn looks like an ordinary message. ## Second, smaller gap `ReplySubject` carries `Author`, `Content`, and `Counterpart`. It does not carry `Attachments`. `TranscriptEntry` carries them because their absence reads as a text-only message, which is the defect `docs/sirens-echo-attachments.md` exists for. Replying to a screenshot with *"what does this say"* currently renders as a reply to empty text, and the model has nothing telling it an image was involved. ## What I am building - Resolve the reference when the Gateway did not deliver it inline, drawing on `a.lookups`, the same budget the other gate-forced REST calls use, so a channel of old replies cannot become one lookup per message. A reference that cannot be read stays an ordinary message rather than becoming a failure. - `Attachments` on `ReplySubject`, rendered through the same suffix an entry uses. Resolved before the turn runs rather than inside `Current()`, because `Current()` takes no context and `runTurn` calls it before `History()`. ## Provenance I had both of these in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/617, which I closed as superseded because the version on `main` is better in two other places: it names the speaker in the rendered line, and it handles an unnamed author. This carries forward only the two parts that were lost, onto their implementation rather than mine.
Author
Member

Built. #636, gate green.

Both halves revert-checked against the landed implementation:

TestATruncatedReferenceStillNamesTheSubject   a truncated reference resolved to nothing
TestAReplyToAnImageSaysThereWasAnImage        the replied-to attachment was not reported

What changed, on top of theirs rather than beside it

replyTarget takes the resolved message as a second source. The inline payload still wins wherever Discord supplied one, so a fetch can never contradict what the Gateway delivered, and TestTheInlineReferenceOutranksAResolvedOne pins that.

resolveReplyTo returns nil when the reference arrived inline, so it costs nothing on the ordinary path. It only fetches when MessageReference carries an id and ReferencedMessage does not, under the same a.lookups budget the other gate-forced calls use.

ReplySubject gained Attachments, rendered through TranscriptEntry.attachmentSuffix rather than a second implementation, so a replied-to image reads exactly like an attached one.

One thing I found while doing it

The landed change documented nothing. No doc in docs/ mentioned ReplySubject, the reply line, or the behaviour at all, so a reader had only the code.

docs/sirens-echo-prompt.md was 52 of 80 lines, which is unusual in this tree and meant there was room. It now carries a section describing what a reply is answering, why it supplements recency rather than replacing it, when the fetch happens, and that only one level renders. That covers the landed behaviour as well as this change, since describing half of it would have been worse than describing none.

Still not covered

A reply to a reply does not walk the chain, deliberately and now written down. The second level is a claim about what someone else was addressing rather than about what this member is.

**Built. https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/636, gate green.** Both halves revert-checked against the landed implementation: ``` TestATruncatedReferenceStillNamesTheSubject a truncated reference resolved to nothing TestAReplyToAnImageSaysThereWasAnImage the replied-to attachment was not reported ``` ## What changed, on top of theirs rather than beside it `replyTarget` takes the resolved message as a second source. The inline payload still wins wherever Discord supplied one, so a fetch can never contradict what the Gateway delivered, and `TestTheInlineReferenceOutranksAResolvedOne` pins that. `resolveReplyTo` returns **nil when the reference arrived inline**, so it costs nothing on the ordinary path. It only fetches when `MessageReference` carries an id and `ReferencedMessage` does not, under the same `a.lookups` budget the other gate-forced calls use. `ReplySubject` gained `Attachments`, rendered through `TranscriptEntry.attachmentSuffix` rather than a second implementation, so a replied-to image reads exactly like an attached one. ## One thing I found while doing it **The landed change documented nothing.** No doc in `docs/` mentioned `ReplySubject`, the reply line, or the behaviour at all, so a reader had only the code. `docs/sirens-echo-prompt.md` was 52 of 80 lines, which is unusual in this tree and meant there was room. It now carries a section describing what a reply is answering, why it supplements recency rather than replacing it, when the fetch happens, and that only one level renders. That covers the landed behaviour as well as this change, since describing half of it would have been worse than describing none. ## Still not covered A reply to a reply does not walk the chain, deliberately and now written down. The second level is a claim about what someone else was addressing rather than about what this member is.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#630
No description provided.