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
-
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.)
-
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.
Context
watch.shprints each delivered message as a single line (watch.sh:479, inside its EPIPE guard):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_atandfrom_agent, which are not unique and force an imprecise query. Meanwhilewatch.sh:430already reads theidcolumn:and then drops it on output. That leaves the line one field short of a one-line recovery:
api.sh get teams <team> messagesplusid=N.Proposal
Include
idin the streamed line. A trailing position disturbs existingfrom → toparsers less than a leading tag would, e.g.:(or a leading
[id=N]if you prefer greppability; if the format is considered part of the contract, it could be gated behind adelivery.monitor.line_formatconfig.)Optional and complementary: explicit truncation or chunking on the agmsg side. A
delivery.monitor.body_maxconfig (same read path asdelivery.monitor.poll_interval) would letwatch.shcut the body itself and append[full: id=N], so what is today a silent downstream truncation becomes an explicit pointer. Alternatively, abody_chunk=Nconfig 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.shand 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 atwatch.sh:430. Hence this proposal.Happy to PR either part if the direction sounds right.