Skip to content

feat(watch): include the message id in the streamed output line #596

Description

@massy-na

Context

watch.sh prints each delivered message as a single line (watch.sh:479, inside its EPIPE guard):

if ! printf '%s | %s | %s → %s | %s\n' "$ts" "$team" "$from" "$to" "$body"; then

The body is printed in full, so the script itself is not at fault. But the consumer on the other side of the pipe may not show all of it. Claude Code's Monitor tool truncates each event line at 500 characters. The cut happens in the display layer, it is character-based, and it is not configurable: there is no Monitor equivalent of BASH_MAX_OUTPUT_LENGTH. Any harness that treats one stdout line as one notification can impose a similar cap.

We measured this on Claude Code with three messages of 1098, 1190, and 928 bytes (2026-07-30). Every one was cut at exactly 500 characters, which confirms the cap is character-based and independent of byte length. The tail of a long message, which tends to carry the actual question or condition, is replaced by ...(truncated).

Problem

When that happens, the recipient has to fetch the full body from the DB. But the output line carries no unique key. The only recovery handles left are created_at and from_agent, which are not unique and force an imprecise query. Meanwhile watch.sh:430 already reads the id column:

while IFS=$'\x1f' read -r id ts team from to body; do

and then drops it on output. That leaves the line one field short of a one-line recovery: api.sh get teams <team> messages plus id=N.

Proposal

  1. Include id in the streamed line. A trailing position disturbs existing from → to parsers less than a leading tag would, e.g.:

    <ts> | <team> | <from> → <to> | <body> [id=N]
    

    (or a leading [id=N] if you prefer greppability; if the format is considered part of the contract, it could be gated behind a delivery.monitor.line_format config.)

  2. Optional and complementary: explicit truncation or chunking on the agmsg side. A delivery.monitor.body_max config (same read path as delivery.monitor.poll_interval) would let watch.sh cut the body itself and append [full: id=N], so what is today a silent downstream truncation becomes an explicit pointer. Alternatively, a body_chunk=N config could split long bodies into multiple lines of at most N characters. Consumers that batch lines arriving within a short window into one notification (Claude Code batches within 200 ms) then display the full body intact.

Workaround we run today

A stdout filter between watch.sh and the Monitor splits any line longer than 440 characters into [i/N]-prefixed chunks. It has been in production since 2026-07-30, and a 1,400+ character message now arrives as one notification, complete. It works, but it is a consumer-side patch for information the producer already had in hand at watch.sh:430. Hence this proposal.

Happy to PR either part if the direction sounds right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions