diff --git a/README.md b/README.md index 4338180b..d5129c9a 100644 --- a/README.md +++ b/README.md @@ -538,8 +538,8 @@ successful durable inbox delivery. Direct messages start or enter a recipient turn, so they consume transcript context. Batch related details and reserve direct delivery for action, a blocker, or a needed reply. Use an unmentioned room broadcast for FYI/progress. -Automatic body previews are bounded (2,000 characters per message and an -8,000-character hook-batch budget); the complete durable message remains +Automatic body previews are bounded (2,000 characters per live push; a hook +shows up to 6,000 of one message within an 8,000-character batch budget); the complete durable message remains available through `muster://messages`. Codex wakes coalesce for 30 seconds so a burst can join the active turn. diff --git a/crates/daemon/src/hooks.rs b/crates/daemon/src/hooks.rs index d031f735..38493b01 100644 --- a/crates/daemon/src/hooks.rs +++ b/crates/daemon/src/hooks.rs @@ -36,13 +36,18 @@ use myko::{ use serde_json::Value; use muster_entities::{ - AckMessages, CONTEXT_BODY_MAX_CHARS, GetAllSessions, GetAllWakeWatchers, HostInfo, MessageId, - MessageView, ReadMessages, Session, SessionId, WakeWatcher, WakeWatcherId, context_preview, - nickname_for, + AckMessages, GetAllSessions, GetAllWakeWatchers, HostInfo, MessageId, MessageView, + ReadMessages, Session, SessionId, WakeWatcher, WakeWatcherId, context_preview, nickname_for, }; const AUTO_INBOX_BODY_BUDGET_CHARS: usize = 8_000; const AUTO_INBOX_MIN_BODY_CHARS: usize = 256; +/// The most of one body a hook shows. Higher than the live push's +/// `CONTEXT_BODY_MAX_CHARS`, because a hook's block has room: a lone long DM +/// (a team summary, a review) arrives whole instead of cut at 2,000 and +/// needing a resource read (rally log §1.3). A batch still splits the +/// 8,000-char budget, and the block stays under Claude's 10,000-char limit. +const HOOK_BODY_MAX_CHARS: usize = 6_000; /// The most an inbox block may be. Claude Code takes at most 10,000 characters /// from a hook, and SessionStart puts the identity line in front of the block. const INBOX_BLOCK_MAX_CHARS: usize = 9_000; @@ -806,9 +811,9 @@ fn surface_unread(cmd_ctx: &CommandContext, sid: &str) -> (String, Vec> = cmd_ctx diff --git a/crates/daemon/tests/hook_inbox_e2e.rs b/crates/daemon/tests/hook_inbox_e2e.rs index f8ac60c9..fd18788f 100644 --- a/crates/daemon/tests/hook_inbox_e2e.rs +++ b/crates/daemon/tests/hook_inbox_e2e.rs @@ -243,7 +243,7 @@ fn stored_message_surfaces_via_prompt_submit_hook() { // Automatic delivery carries a bounded, UTF-8-safe preview. The complete // durable Message remains queryable even after the preview is acknowledged. - let long_body = format!("LONG-{}-TAIL", "é".repeat(3_000)); + let long_body = format!("LONG-{}-TAIL", "é".repeat(7_000)); SendMessage { response_required: false, to_session_id: SessionId(Arc::from("recipient")), @@ -270,7 +270,7 @@ fn stored_message_surfaces_via_prompt_submit_hook() { "hook did not identify the bounded preview: {bounded:?}" ); assert!( - bounded.chars().count() < 3_000, + bounded.chars().count() < 7_000, "automatic inbox context exceeded its per-message bound" ); let after_bounded = http_post(addr, "/hook/prompt-submit", r#"{"session_id":"recipient"}"#); @@ -279,6 +279,25 @@ fn stored_message_surfaces_via_prompt_submit_hook() { "bounded preview re-surfaced after successful hook write" ); + // A lone DM well past the live push's 2,000-char preview arrives whole in a + // hook (rally log §1.3): a team summary shouldn't need a resource read. + let summary = format!("SUMMARY-{}-END", "x".repeat(4_000)); + SendMessage { + response_required: false, + to_session_id: SessionId(Arc::from("recipient")), + body: summary.clone(), + as_session: None, + idempotency_key: None, + } + .execute(cmd_ctx(&ctx, Some("c-sender"))) + .expect("send persists summary"); + let whole = http_post(addr, "/hook/prompt-submit", r#"{"session_id":"recipient"}"#); + assert!( + whole.contains(&summary), + "a lone 4,000-char DM was cut: {whole:?}" + ); + assert!(!whole.contains("[truncated"), "{whole:?}"); + // a wrong /hook path must 404 (the misroute that caused the outage). let mut s = TcpStream::connect(addr).unwrap(); s.write_all( diff --git a/docs/hook-delivery.md b/docs/hook-delivery.md index e916c06c..9d907aac 100644 --- a/docs/hook-delivery.md +++ b/docs/hook-delivery.md @@ -16,7 +16,7 @@ Marshal's channel push. Every claim was checked against the code at | Idle session wakes | yes, the notification starts a turn | yes, through the `wait` hook (below) | | Busy session sees it | yes, as a channel event | at the next tool call (`PostToolUse`) or prompt (`UserPromptSubmit`) | | Marked read when | the push is handed to the shim's WebSocket client (`send_message.rs`, the dedupe after `push_to_client`) | the hook output has been written back (prompt hooks), or the waiter has printed it and acked (`/hook/wait`) | -| Message body in context | a bounded preview in `meta.body` (2,000 chars) | a bounded preview in the `` block: at most 20 messages, an 8,000-char budget split between them (min 256, max 2,000 each) | +| Message body in context | a bounded preview in `meta.body` (2,000 chars) | a bounded preview in the `` block: at most 20 messages, an 8,000-char budget split between them (min 256, max 6,000 each, so a lone long message arrives whole) | | Room broadcasts | not pushed; ambient | not surfaced; ambient. Only direct mail is injected | Both paths share the daemon, the durable `Message` store and the read diff --git a/docs/roadmap.md b/docs/roadmap.md index df956599..162ba670 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -663,6 +663,12 @@ Merged and needing a live run: MUS-36 and MUS-37. idle recipient is woken only when `wake` is `expected`. It is said once per shim, since #62 dropped the per-send note to keep tool output small. The statusline's `no live channel` warning is unchanged: it still shows only when no wait hook is wired, the one case where it's a fault. +- [x] **MUS-102: A lone long DM arrives whole in a hook.** From the DiscoSim rally (2026-11-06, + issues log §1.3): team summaries arrived cut at 2,000 characters, each needing a resource read. + Done: a hook shows up to 6,000 characters of one message (`HOOK_BODY_MAX_CHARS`), still within + the 8,000-character batch budget and Claude's 10,000-character hook limit; the live push keeps + its 2,000. Not done: a `read_message(id)` tool, and §10.1 (mail marked read that the model + never saw), which needs its own look at when a hook's ack fires. - [ ] **MUS-111: `setup.sh --from-source` builds a pinned release, not the default branch.** Filed by robust-warbler: `--from-source` ran `cargo install --locked --git` on the unpinned default branch. Done when: a build takes the latest release's tag by default, looked up once so it's the same