Skip to content

fix(coding-agent): stamp agent messages with the sender's compose time - #1189

Open
Hotragn wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Hotragn:fix/823-agent-message-compose-time
Open

fix(coding-agent): stamp agent messages with the sender's compose time#1189
Hotragn wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Hotragn:fix/823-agent-message-compose-time

Conversation

@Hotragn

@Hotragn Hotragn commented Aug 11, 2026

Copy link
Copy Markdown

Fixes #823 (part B in full; part A's stated minimum is discussed below and deliberately left as a maintainer decision).

The gap

Agent mail always uses steering delivery, so a message sent to a busy target is queued and waits until that target's turn reaches a boundary. The wait is bounded only by the target's own progress — the issue reports measured waits of 39–96 minutes for child handback messages sitting behind an orchestrator's long sleep cells.

createAgentSessionMessagePrompt rendered sender, relationship, route, and message id, and no time of any kind:

[from parent]
Agent-to-agent message received.
Source: agent_message
From: lead, active active-parent, session parent-id
To: worker, active active-child, session child-id
Message id: agentmsg_demo

Please inspect the latest result.

A time was already known to the system — the sender's receipt carries queuedAt/deliveredAt, and createAgentSessionMessage stamps a transcript-level timestamp — but neither value is ever rendered into content, so it never reaches the receiving model. The recipient could not distinguish a message composed seconds ago from one that had waited an hour, and so could not judge whether "the latest result" still referred to what the sender meant, or whether a later message had superseded it.

Approach, and the prior art it follows

This is the RFC 5322 §3.6.1 Date: origination header applied to agent mail. That field is mandatory in Internet messages precisely because store-and-forward delivery decouples send time from read time: the transport records when the message was written, and the reader decides how old is too old. Nothing here is invented — the queued-agent-mail path has exactly the store-and-forward shape the header exists for.

It also matches the in-repo precedent for the other injected-prompt surface: scheduled heartbeat prompts carry their delivery context in the prompt body rather than in details the model never sees.

composedAt is stamped once, where the payload is minted in sendAgentSessionMessage, and rendered as a Composed: line. It is also surfaced on AgentSessionMessageDetails so transcript consumers and UI have it without re-parsing text.

Placement is load-bearing

parseAgentSessionMessagePromptId walks the header by fixed offsets — optional [from …], then Agent-to-agent message received., Source:, optional From:, To:, Message id: — and stops at the id line. The new line is appended strictly after the line the parser stops on, so:

  • stamped and unstamped prompts both still parse to the correct id, including the no-From:/no-relationship shape;
  • a payload without composedAt renders text byte-identical to before, so nothing changes for callers that do not stamp.

Both properties are asserted, including an exact full-string equality check on the unstamped form.

What I did not do, and why

Part A of the issue — the queue draining one message per assistant turn — is not in this PR. The issue's own "Expected" allows either coalescing or "at minimum … told that more are waiting", and I could not implement either half to the standard the rest of this change meets:

  • Coalescing is governed by a single steeringMode setting that deliberately covers both human keyboard steering (where one-at-a-time is the right ergonomic default) and machine-generated agent mail. Splitting them means adding a per-source override to settings.json. That is a product decision about the settings surface, not a bug fix, and it belongs to whoever owns that surface.
  • Reporting backlog depth needs an accurate count of agent-message actions queued ahead of this one. unfinishedActionCount counts all unfinished actions, not agent mail, so using it would print a number that is wrong in the common case. Doing it properly needs a narrow queued-agent-message accessor on AgentSession, and I could not exercise the daemon queue end-to-end on this machine (Unix-socket tests do not run on Windows), so I would be shipping an unverified count. A misleading number is worse than none.

Happy to follow up on either with direction on the settings shape.

Tests

packages/coding-agent/test/suite/regressions/823-agent-message-compose-time.test.ts (5 cases): the stamped prompt renders the compose time and still ends with the message body; the fixed-offset parser resolves the id for both stamped and unstamped prompts and the Composed: line is positioned after Message id:; the id parses for the bare no-sender/no-relationship shape; an unstamped payload renders byte-identical text to before; and the compose time reaches transcript details while an unstamped message has no composedAt key.

Verified: npm run check clean. Every test file in the repo that references createAgentSessionMessagePrompt, Agent-to-agent message received, or Message id: was run — 436 passed. The 5 failures are all in daemon-mode.test.ts (unix-socket bind and symlink cases) and reproduce identically on a clean main worktree on this Windows machine.

Note

Stamp agent-to-agent messages with the sender's compose time

  • Adds an optional composedAt field (ISO 8601) to AgentSessionMessagePayload and AgentSessionMessageDetails in agent-messages.ts.
  • When composedAt is present, createAgentSessionMessagePrompt appends a Composed: header line after the Message id: line so recipients can judge queue wait time.
  • The daemon sets composedAt: new Date().toISOString() on every outgoing message in daemon-mode.ts.
  • A regression test suite in 823-agent-message-compose-time.test.ts verifies ordering, backward compatibility, and correct omission when no compose time is provided.

Macroscope summarized e26d5ff.

Agent mail always uses steering delivery, so a message sent to a busy target is
queued and waits for that target's turn to reach a boundary. That wait is
bounded only by the target's own progress: one measured orchestrator session
had child handback messages arrive 39-96 minutes after they were written.

createAgentSessionMessagePrompt rendered sender, relationship, route, and
message id, and no time of any kind. A time was already known to the system -
the sender's receipt carries queuedAt/deliveredAt and the transcript message
carries a timestamp - but neither reached the prompt text, so the recipient
could not tell a message composed seconds ago from one that had waited an hour,
and could not judge whether a phrase like "the latest result" still meant what
the sender meant.

Stamp composedAt on the payload where it is minted and render it as a
Composed: line. This is the RFC 5322 3.6.1 Date: origination header applied to
agent mail: the transport records when the message was written and the reader
decides how old is too old. It also matches the <heartbeat> block that carries
delivery context on scheduled prompts.

parseAgentSessionMessagePromptId walks the header by fixed offsets and stops at
the message id line, so the new line is appended strictly after it and both the
stamped and unstamped forms still parse. A payload without composedAt renders
byte-identical text to before.

fixes PrimeIntellect-ai#823
@Hotragn
Hotragn force-pushed the fix/823-agent-message-compose-time branch from 1a7e512 to e26d5ff Compare August 11, 2026 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant