Repository navigation
Conversation
* test(pi): add failing repro for truncated live pushes The daemon clips a live push's body to CONTEXT_BODY_MAX_CHARS (2000) and sets body_truncated, but marshal-pi injects the preview as if it were the whole message. Move the push text from onInboundMessage into src/inbound.ts unchanged so it can be tested without a pi session or a daemon, and add cases for a truncated push. The two truncated cases fail. * fix(pi): say when a live push body was truncated Declare message_id and body_truncated on NotifyChannelMeta, which the daemon already sends with every push. When body_truncated is set, append the same notice marshal-prime-agent uses, naming the message id, so the agent knows to read the rest with marshal_messages. The issue suggested filling in the full body from the inbox drain, but send_message marks a live-delivered message read, so the unread drain never returns it. Pulling the full body in would also undo the 2000-char cap the daemon applies on purpose.
…136) The per-turn pull raced `drainInbox` against a 2 s timeout, and `drainInbox` acked what it read before returning. When the timeout won after the ack went out, those messages were marked read and their text was dropped. `pullInbox` now reads without acking and returns the text with `commit()` and `release()`. The turn hook commits a pull once its text is in the turn; a pull that lands after the deadline is released, so its messages stay unread for the next turn. Messages an outstanding pull holds are not handed out by another one. prime-agent's no-body live-push fallback injects first and commits after. marshal-pi keeps `drainInbox` (pull and commit in one step) for its live-push path. prime-agent's inbox read and ack now go through a bounded send, as marshal-pi's already do: a pull returns at once while Marshal is disconnected, and a read that never answers times out instead of holding up every later pull for the session.
…runcated previews (#137) After prompting a live push, the push handler called `drainInbox` and dropped what it returned. The prompt's own `chat.message` hook has already shown whatever was unread when the turn started, so the drain could only find messages that turned unread while the turn ran, such as a direct message whose push failed when the client dropped mid-turn, and it acked them without showing them. It is the same pattern as #133 in marshal-pi. The drain is gone; the next turn's `chat.message` pull delivers those messages. The push body is a bounded preview. When the daemon cut it (`body_truncated`), the prompt now says so and names the message to read with `marshal_messages`, as prime-agent already does.
* test(pi): add failing repro for before_agent_start on custom-message runs
* fix(pi): don't assume a prompt in before_agent_start
pi 1.2.0 fires before_agent_start for runs started by
pi.sendMessage(..., { triggerTurn: true }) too. Those events have
messageType "custom" and no prompt, so the status handler threw
"Cannot read properties of undefined (reading 'length')" on every live
push. Skip the status update for a run without a prompt; it keeps the
status the last prompt set, as it did before pi 1.2.0.
…rshal-tui (#135) * feat(daemon): answer --help and --version, refuse unknown arguments The daemon never read its arguments, so `marshal-daemon --help` or a mistyped flag started a real daemon. It now prints help or its version and exits, and refuses any other argument, a non-UTF-8 one included, with exit 2 before anything starts. `--foreground`, which docs/install.md told people to pass, stays accepted on its own as a no-op. * feat(shim): answer --help and --version `--help` lists the subcommands and how the shim finds the daemon, and `--version` prints the version. Both answer before the runtime starts, like `--check`. An unknown argument now points at `--help`. * feat(tui): answer --version clap already generates --help; declaring the version adds --version and -V.
* test(pi): add failing repro for truncated live pushes The daemon clips a live push's body to CONTEXT_BODY_MAX_CHARS (2000) and sets body_truncated, but marshal-pi injects the preview as if it were the whole message. Move the push text from onInboundMessage into src/inbound.ts unchanged so it can be tested without a pi session or a daemon, and add cases for a truncated push. The two truncated cases fail. * fix(pi): say when a live push body was truncated Declare message_id and body_truncated on NotifyChannelMeta, which the daemon already sends with every push. When body_truncated is set, append the same notice marshal-prime-agent uses, naming the message id, so the agent knows to read the rest with marshal_messages. The issue suggested filling in the full body from the inbox drain, but send_message marks a live-delivered message read, so the unread drain never returns it. Pulling the full body in would also undo the 2000-char cap the daemon applies on purpose. * test(pi): add failing repro for the drain after a live push After injecting a live push, onInboundMessage calls drainInbox and throws the result away. drainInbox acks every unread direct message it reads, and the daemon has already marked the pushed message read, so the drain only ever finds other messages, and those get acked without being shown. Move the delivery out of onInboundMessage into src/live-push.ts unchanged so it can be tested with a fake inbox. The case with another unread message waiting fails. * fix(pi): don't drain the inbox after a live push The daemon marks a live-delivered message read, so the drain that followed the push could only find other unread messages. It acked them and dropped the rendered text, and the next prompt's inbox no longer had them. Inject the pushed body and stop there, as marshal-prime-agent does. A push without a body (older daemons) still drains the inbox and injects it. * test(pi): a truncated live push keeps its notice without draining * fix(pi): ack a live-pushed message once it is injected The drain after a push used to ack the pushed message as a side effect. A direct push is already read when the daemon delivers it, but a room @mention is not, so without the drain it would show up again in the next inbox. Ack the pushed message alone after injecting it. * test(pi): preserve reply requirements in live pushes * fix(pi): retain reply requirement in live push text --------- Co-authored-by: max <max@lucid.rocks>
* fix(entities): make read-state sticky across a session reload A pi session's id is stable across exit / /reload (sha1 of the session file), so a session DEL is a temporary disconnect, not a permanent one. MessageRead.session_id was a belongs_to(Session) cascade, so every reload DEL'd the Session and cascade-DEL'd the acks, while the direct Messages survived (their to_session_id cascade is silently unwired). On re-registration the whole backlog was unread again and the pull-inbox re-injected it (the 20-message dump of already-seen DMs). Decouple the ack's lifetime from the session: it now follows the message (belongs_to Message) only, so an already-read DM stays read across a reload. Room messages are unaffected (hidden by the fresh joined_at on re-join). Regression test: session_del_inbox_e2e. * fix(daemon): sweep read marks whose message is gone The TTL prune deletes messages with del_by_id, which skips relationship cascades, so a pruned message's MessageRead rows stayed behind. Until now a session DEL cleaned them up; with read state no longer tied to the session, nothing did. The message sweep now also drops every read mark whose message no longer exists. * fix(entities): don't cascade a direct message with its recipient session Myko 6.8.0 makes `#[belongs_to(Session)]` a real cascade, so a session DEL now deletes every direct message addressed to it. A session row is DEL'd and re-SET under the same id on a pi /reload, a resume, or a reaped pull session's next hook, so its unread direct messages were being deleted instead of waiting for it. Make `to_session_id` a plain id; the TTL sweep still bounds the store. `to_room_id` keeps its `belongs_to(Room)` cascade: rooms aren't re-created under the same id. The session-DEL test now also asserts the direct message survives the DEL, which the read check alone would not catch. On main it fails with messages=0 after the DEL. * test(inbox): update session resume command fixture * test(inbox): preserve HTTP hook coverage across session resume --------- Co-authored-by: max <max@lucid.rocks>
* docs(readme): say which dependencies are AGPL Marshal's own source is MIT OR Apache-2.0, but it depends on myko, myko-macros, autosocket and myko-server, which declare AGPL-3.0-or-later. The licence section now says so, so anyone shipping binaries knows to assess the combined work. Ported from Castor-o7/muster #48 (3f8553e), only the disclosure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit b123c22) * docs(readme): list myko-leptos among the AGPL dependencies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) * feat(entities): let a session ack its own live pushes `SendMessage` marks a direct message read as soon as its NotifyChannel push is queued on the recipient's connection. If that connection stops being read and then dies (a laptop going to sleep, a hung process), the push is lost and the message is already read, so no inbox pull shows it again. A session can now set `acks_pushes`. For such a session the daemon leaves a pushed message unread, and the client acks it once it has shown it. A push lost on the way then comes back in the next pull. Sessions that don't set it keep the current behavior. * fix(shim): ack a live push only once it has been written to Claude The Claude Code shim now registers with `acks_pushes`, so the daemon leaves a direct push unread, and acks it itself after the MCP writer has written and flushed the channel notification. A push the shim never hands to Claude (stdout gone, connection that stopped being read) stays unread for the next inbox pull instead of being marked read when the daemon queued it. `Notifier::channel_written` returns a receiver the writer task fulfils after the flush; a failed write drops it, so nothing is acked. @mention pings are acked the same way: they are direct messages too, so an unacked one would come back in the per-turn inbox after the channel had already shown it. * test: specify reply requirements in push acknowledgment fixtures * test: preserve session resume coverage with push acknowledgments --------- Co-authored-by: max <max@lucid.rocks>
…141) * feat(entities): let a session ack its own live pushes `SendMessage` marks a direct message read as soon as its NotifyChannel push is queued on the recipient's connection. If that connection stops being read and then dies (a laptop going to sleep, a hung process), the push is lost and the message is already read, so no inbox pull shows it again. A session can now set `acks_pushes`. For such a session the daemon leaves a pushed message unread, and the client acks it once it has shown it. A push lost on the way then comes back in the next pull. Sessions that don't set it keep the current behavior. * fix(shim): ack a live push only once it has been written to Claude The Claude Code shim now registers with `acks_pushes`, so the daemon leaves a direct push unread, and acks it itself after the MCP writer has written and flushed the channel notification. A push the shim never hands to Claude (stdout gone, connection that stopped being read) stays unread for the next inbox pull instead of being marked read when the daemon queued it. `Notifier::channel_written` returns a receiver the writer task fulfils after the flush; a failed write drops it, so nothing is acked. @mention pings are acked the same way: they are direct messages too, so an unacked one would come back in the per-turn inbox after the channel had already shown it. * feat(shim): pull the inbox into Claude Code turns with plugin hooks The daemon's per-turn inbox (`/hook/session-start`, `/hook/prompt-submit`) only reached Codex: the Claude plugin shipped no hooks and the docs never mentioned them, so a message the live channel didn't show -- every message in a session started without the channels flag, for one -- sat unread until the agent happened to read `marshal://messages`. `marshal-shim claude-hook <session-start|prompt-submit>` is the Codex hook bridge for Claude Code, whose hooks take the same JSON in and out; it sends `harness=claude` instead of `harness=codex`, so the session block doesn't tell the agent to pass `asSession`. The plugin ships it as `hooks/hooks.json`, and the READMEs give the same block for hand-wired setups. * fix(entities): say when a live push's preview was cut A push carries a bounded preview in `meta.body`; when the daemon cut it, the line the model reads (`content`) said nothing, so an agent could act on half a message. Both direct and @mention pushes now append the notice the hook inbox already used, naming the message to read in full. The notice lives in one place, `truncated_notice`, shared with the hook inbox. * fix(daemon): escape, cap and dedupe the per-turn hook inbox The inbox the session hooks inject is now what Claude sessions see before every prompt, so three gaps matter more than they did: - Message bodies were copied in raw. A body could close `</marshal_inbox>` or start a line reading `- from <someone else>:` and pass for the daemon's own framing. Peer text (bodies, the operator address) is now escaped (`&`, `<`, `>`). Every character a reader may take as a line end (`\n`, VT, FF, NEL, U+2028, U+2029) starts an indented continuation line, and other controls, bidi overrides, zero-width and tag characters are dropped. Handles (the sender's nickname, the operator address, the session's own nickname) are cut at 128 units, and the session's nickname is escaped as an attribute value: the stores accept any SET. - The block had no overall size limit. Claude Code injects hook context whole only up to 10,000 characters and swaps a longer one for a file path and a short preview, yet every message in it was acked. Rendering now stops at 9,000 UTF-16 units, measured after escaping, which can grow a body fivefold. No single message may outgrow that budget, so the first one, which always goes in, still fits. Only what was rendered is acked, and the rest stays unread, counted in `remaining`, for the next turn. - An @mention ping is a direct message, but a delivered live push of one wasn't marked read the way a direct send is, so the hook showed it a second time. It is now. Codex sessions get the same escaping and size limit. * fix(shim): bound a hook request at 3 s and find the daemon like the MCP path A hook holds the agent's turn (Claude's UserPromptSubmit holds the prompt). The request had a 5 s connect timeout plus a 5 s timeout per read, so a daemon that was unreachable or stalled held every prompt for 5 s or more. It now has one 3 s deadline from name lookup to last byte (the lookup runs on a helper thread, since getaddrinfo has no timeout), a 1 s connect timeout per resolved address (all are tried), and a 256 KiB response cap. Without an explicit base URL, the hook derived its address from `MARSHAL_DAEMON_ADDRESS` before the address file, the reverse of the MCP shim, so the two could reach different daemons and register the same session on both. Both now use `daemon_address()`: file, then `MARSHAL_DAEMON_ADDRESS`, then `MYKO_ADDRESS`, then localhost. * fix(daemon): escape the message id in the hook inbox's truncation notice Bodies and handles in <marshal_inbox> are escaped, but the id in the truncation notice went in raw. SendMessage always mints a UUID, so only a client SETting a Message directly could choose one, but such an id could close the inbox block. Escape it like a handle. * test: specify reply requirements in push acknowledgment fixtures * test(inbox): retain mentions until the client acknowledges them * fix(inbox): defer mention receipts for clients that acknowledge pushes * test: preserve session resume coverage with push acknowledgments --------- Co-authored-by: max <max@lucid.rocks>
…#139) (#164) * feat(entities): let a session ack its own live pushes `SendMessage` marks a direct message read as soon as its NotifyChannel push is queued on the recipient's connection. If that connection stops being read and then dies (a laptop going to sleep, a hung process), the push is lost and the message is already read, so no inbox pull shows it again. A session can now set `acks_pushes`. For such a session the daemon leaves a pushed message unread, and the client acks it once it has shown it. A push lost on the way then comes back in the next pull. Sessions that don't set it keep the current behavior. * fix(shim): ack a live push only once it has been written to Claude The Claude Code shim now registers with `acks_pushes`, so the daemon leaves a direct push unread, and acks it itself after the MCP writer has written and flushed the channel notification. A push the shim never hands to Claude (stdout gone, connection that stopped being read) stays unread for the next inbox pull instead of being marked read when the daemon queued it. `Notifier::channel_written` returns a receiver the writer task fulfils after the flush; a failed write drops it, so nothing is acked. @mention pings are acked the same way: they are direct messages too, so an unacked one would come back in the per-turn inbox after the channel had already shown it. * feat(shim): pull the inbox into Claude Code turns with plugin hooks The daemon's per-turn inbox (`/hook/session-start`, `/hook/prompt-submit`) only reached Codex: the Claude plugin shipped no hooks and the docs never mentioned them, so a message the live channel didn't show -- every message in a session started without the channels flag, for one -- sat unread until the agent happened to read `marshal://messages`. `marshal-shim claude-hook <session-start|prompt-submit>` is the Codex hook bridge for Claude Code, whose hooks take the same JSON in and out; it sends `harness=claude` instead of `harness=codex`, so the session block doesn't tell the agent to pass `asSession`. The plugin ships it as `hooks/hooks.json`, and the READMEs give the same block for hand-wired setups. * fix(entities): say when a live push's preview was cut A push carries a bounded preview in `meta.body`; when the daemon cut it, the line the model reads (`content`) said nothing, so an agent could act on half a message. Both direct and @mention pushes now append the notice the hook inbox already used, naming the message to read in full. The notice lives in one place, `truncated_notice`, shared with the hook inbox. * fix(daemon): escape, cap and dedupe the per-turn hook inbox The inbox the session hooks inject is now what Claude sessions see before every prompt, so three gaps matter more than they did: - Message bodies were copied in raw. A body could close `</marshal_inbox>` or start a line reading `- from <someone else>:` and pass for the daemon's own framing. Peer text (bodies, the operator address) is now escaped (`&`, `<`, `>`). Every character a reader may take as a line end (`\n`, VT, FF, NEL, U+2028, U+2029) starts an indented continuation line, and other controls, bidi overrides, zero-width and tag characters are dropped. Handles (the sender's nickname, the operator address, the session's own nickname) are cut at 128 units, and the session's nickname is escaped as an attribute value: the stores accept any SET. - The block had no overall size limit. Claude Code injects hook context whole only up to 10,000 characters and swaps a longer one for a file path and a short preview, yet every message in it was acked. Rendering now stops at 9,000 UTF-16 units, measured after escaping, which can grow a body fivefold. No single message may outgrow that budget, so the first one, which always goes in, still fits. Only what was rendered is acked, and the rest stays unread, counted in `remaining`, for the next turn. - An @mention ping is a direct message, but a delivered live push of one wasn't marked read the way a direct send is, so the hook showed it a second time. It is now. Codex sessions get the same escaping and size limit. * fix(shim): bound a hook request at 3 s and find the daemon like the MCP path A hook holds the agent's turn (Claude's UserPromptSubmit holds the prompt). The request had a 5 s connect timeout plus a 5 s timeout per read, so a daemon that was unreachable or stalled held every prompt for 5 s or more. It now has one 3 s deadline from name lookup to last byte (the lookup runs on a helper thread, since getaddrinfo has no timeout), a 1 s connect timeout per resolved address (all are tried), and a 256 KiB response cap. Without an explicit base URL, the hook derived its address from `MARSHAL_DAEMON_ADDRESS` before the address file, the reverse of the MCP shim, so the two could reach different daemons and register the same session on both. Both now use `daemon_address()`: file, then `MARSHAL_DAEMON_ADDRESS`, then `MYKO_ADDRESS`, then localhost. * fix(daemon): escape the message id in the hook inbox's truncation notice Bodies and handles in <marshal_inbox> are escaped, but the id in the truncation notice went in raw. SendMessage always mints a UUID, so only a client SETting a Message directly could choose one, but such an id could close the inbox block. Escape it like a handle. * feat: deliver to and wake Claude Code without the channels flag Claude Code launched without --dangerously-load-development-channels drops `notifications/claude/channel`. On macOS the shim also couldn't read its parent's command line, so it reported channels_enabled = unknown, and the daemon pushed, got "delivered", and marked the mail read. Nothing pulled the inbox either (#139). This adds a hook path that needs no flag: - `marshal-shim claude-hook session-start|prompt-submit|post-tool-use`, wired by plugins/marshal-shim/hooks/hooks.json, pulls unread direct mail into the turn as a <marshal_inbox> block (2 s timeout, silent on failure). - `marshal-shim claude-hook wait`, an asyncRewake hook on SessionStart and Stop, holds `/hook/wait` and exits 2 with the mail on stderr, which starts a turn in an idle session. A per-session lock keeps one waiter. It acks with `/hook/ack` only after printing (`ack=client`, `X-Marshal-Ack`), so a lost ack means a duplicate, not a loss. `marshal-shim watch [--once]` is the same loop for Claude's Monitor tool. - The daemon leases surfaced mail for 60 s (MARSHAL_ACK_LEASE_SECS), so two readers don't both take it. It records a `WakeWatcher` while a wait is held (in memory only, swept with its session), reports `wake: expected` for such a session, and shows `wake` on the roster. On SIGTERM it ends held waits with `X-Marshal-Restarting`. - On macOS the shim reads its parent's argv with `ps`, so a flagless session reports channels_enabled = false and is no longer pushed to. Without it the detection is always unknown on a Mac, the daemon pushes into a session that drops the notification, and marks the mail read. - Smaller fixes: the shim tries every resolved address (localhost may resolve to ::1 first); long Monitor lines are kept whole; the truncation notice names a URI the resource serves; the shim re-registers when its roster row lost its client binding. The hooks are on for every plugin user: the plugin loads hooks/hooks.json. The plugin README says how to run without them (the MCP server wired directly loads no hooks). Unchanged from v0.54.1: if the hook listener can't bind or stops, the daemon logs `[hook] listener exited: …` and keeps running. The hooks then find no listener and give up quietly within their timeout. Built on upstream main (94ecc24): the inbox line is upstream's, plus the new truncation notice. Security: none claimed. The hook listener stays unauthenticated, exactly as in v0.54.1, and `/hook/ack` and `/hook/wait` trust the session_id they're given, like the existing hook routes. Keep MARSHAL_HOOK_BIND on loopback or a trusted network. Ported from Castor-o7/muster as the end state of these changes, renamed muster->marshal (scripts/rename-to-marshal.sh), with every gate, durability and other change stripped. Source commits, by Muster PR: 37731f2 (pre-fork) claude-hook + hooks.json + macOS cmdline #1 0ec6d21 /hook/wait + watch; 090b2a9 connect_any #4 b0e4643 long Monitor lines #5 8214bb4 watch --once #6 f1ea3c5 asyncRewake waiter; 7d4fc53 its test #21 5f550ee why a watcher can't reach the daemon (its exit on a stopped hook listener isn't ported) #22 328b3b2 ack only after hand-off (leases, /hook/ack, ack=client) #23 f69e8a6 refusal notice on the watcher's path #37 1c9fedf re-register a row with no client binding #39 2faeaa8 + f0b0ca9 WakeWatcher, roster `wake`, wake: expected #42 64bcaf5 stop reporting a wait that handed out mail #43 4436966 WakeWatcher not persisted #45 d803287 + 21d80fb truncation notice #52 fa5cda5 connect_any tests (socket2 dev-dependency) #56 9069f6f + a939bf2 end held waits on SIGTERM Dropped from those files (not Hooks): Gate: link.rs pairing/TLS/tunnel/invite and PostFailure::Certificate; the pairing branch in channels.rs daemon_address() and in every hook POST; the local-token header; http_listener Access, token and machine checks, Origin/upgrade refusal, ConnLimiter (#74); wakes_paused in /hook/wait and the roster (#76); hooks.rs attested() and the machine override (#16), and with_known_platform (#24 6a20210, a no-op without attestation); gate_filter entries (#39); pair/pairing/unpair subcommands; log redaction (#69); the 6157 default address. Durability: idempotency_key (#32), the clean-session-end mail test (#57). Other: terminal send (#19), roster command (#40), rustfmt-only churn, doc-address and README changes outside the hook section. Tests: cargo test --workspace passes (claude_hook 10, hook_wait_e2e 7, hook_inbox_e2e 1); clippy -D warnings clean; fmt clean; MARSHAL_TEST_SHIM=<this shim> python3 -m unittest discover -s scripts/sandbox passes (19). Not exercised by Zoo: every Claude-only path above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(hooks): a prompt hook acks its mail only after printing it The listener marked prompt-hook mail read once it had flushed the reply, so a reply lost after that (a claude-hook killed at its timeout, a dropped proxy hop) lost the mail. /hook/prompt-submit and the other inbox hooks now take `ack=client`, as /hook/wait does: the ids come back in `X-Marshal-Ack`, nothing is marked read, and the lease keeps the mail from being offered twice until it lapses. The shim's hook command (Claude and Codex) asks for it, prints the context and flushes, then POSTs /hook/ack; if the print fails it doesn't ack, so the mail comes again. A caller that doesn't ask keeps the daemon's ack after the flush. Ported from Castor-o7/muster #99 (71483ca), renamed muster->marshal. Dropped (Gate): the pinned-TLS branch of the hook POST and the removal of link::https_post, which this tree doesn't have. Dropped (Muster docs): docs/hook-delivery.md. Tests: hook_client_ack (new; fails without the listener change), plus the workspace tests and the sandbox unittest with this shim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: report wake expected only when a waiter can wake the session SendMessage told a sender `wake: expected` for any recipient with a live client, while the roster's wake_state counts a client only with channels on. So a Claude session whose MCP shim was connected without the channels flag, and with no waiter, read `wake: none` on the roster and `wake: expected` to the sender, though nothing could wake it. Now a send says expected only when a waiter holds /hook/wait for the recipient, matching the roster's `waiting`. The client_id clause dates from when Codex's MCP shim registered a session; today it registers none, and the TS plugins register with channels on and get the live push. Ported from Castor-o7/muster #97 (14c2b25, MUS-71), renamed muster->marshal. Dropped (Gate): the pause clause and its two tests, as this tree has no pause. Dropped (Muster docs): hook-delivery.md, the roadmap and the conformance suite's known-failure entry. Tests: send_message_command gains a connected channels-off recipient with no waiter (unobserved; fails on the old rule) and a recipient with a waiter, connected or not (expected). send_message_e2e's channels-off case now asserts unobserved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(hooks): log client acks and lease re-offers When mail went unread after a hook, the daemon's log couldn't say whether a reader had acked it or the lease had lapsed and it was offered again. Now: - `/hook/ack` logs `[hook] <session>: client acked N message(s)`. - An offer of mail whose lease lapsed unacked logs `[hook] <session>: offering N message(s) again: the last reader didn't ack within the N s lease`. A lapsed lease is remembered for an hour (FORGET_LAPSED) so the re-offer can be told from a first offer; mail that was acked and released isn't a re-offer. Ported from Castor-o7/muster #127 (f008007, MUS-86), renamed muster->marshal. Dropped (Gate): its gate e2e lines. Dropped (Muster docs): the roadmap. Tests: the lease tests now check the re-offer count, plus a lapsed lease remembered and then forgotten, and an acked message that isn't a re-offer. cargo test --workspace passes (193); clippy -D warnings and fmt clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * eval: Zoo campaign for this PR (gpt-6-luna, 3 repeats, 18 trials), baseline 94ecc24 vs 62552e6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(daemon): drop a stale "opt-in" from the hook listener comment The hooks are on by default; the comment said opt-in from an earlier draft. Comment only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(entities): any client can write a WakeWatcher without the gate myko-server applies a client's SET with no per-entity check, so the hook listener isn't the only writer. Say so, and that the row is a record, not proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(entities): word the WakeWatcher note for upstream readers Upstream has no gate, so drop "without the gate". The note also argued WakeWatcher is separate because a Session row is only a claim, which a client-writable WakeWatcher doesn't escape. Say plainly what it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(daemon): a held /hook/wait doesn't count as activity Each wait re-registered the session and set last_activity_at, and a waiter asks again each time one ends, so an idle session looked active. Operator mail went to whichever session polled last, and the stale-session sweep never reaped it. A wait now leaves last_activity_at alone, and a session a wait registers starts with none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: specify reply requirements in push acknowledgment fixtures * fix(daemon): a peer's body can't forge the inbox framing Bodies went into the <marshal_inbox> block raw, so one holding </marshal_inbox> or a newline and "- from X [response_required=...]:" could close the block or pose as another entry. Escape & and <, drop control characters, and indent continuation lines; sender and operator names too. Cap the block at 9,000 characters, under Claude Code's 10,000-character hook limit with room for the SessionStart identity line: twenty escaped previews could pass it. Mail that doesn't fit is released, not acked, and comes in the next hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(daemon): the hook listener's wait loop doesn't spin under run() run() dropped its watch sender at once, so every held wait's stopping.changed() returned immediately and the loop busy-polled the inbox. Keep the sender for the listener's lifetime. Drop a stale issue cite, and say in the plugin README what an older shim without claude-hook prints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(inbox): retain mentions until the client acknowledges them * fix(inbox): defer mention receipts for clients that acknowledge pushes * Validate framed hook replies and acknowledge only successful bounded delivery * Cover partial replies failed sinks and safe concurrent inbox acknowledgments * Preserve canonical preview notice while linking to complete message history * Align preview regression with shared canonical truncation text * Keep resumed Claude waiters alive through orphaned poll takeover * test: preserve session resume coverage with push acknowledgments * Allow coarse socket timeout scheduling during waiter takeover * test(shim): allow fallback connection attempts their full budget * test(shim): isolate Windows waiter configuration --------- Co-authored-by: lobo235 <7@netlobo.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: max <max@lucid.rocks>
* ci: preserve workflow validation and runner qualification Add local workflow validation and best-effort resource reports from the #113 follow-up. Keep #91's Windows acceptance recipe manual and #125's compiler-cache qualification separate from production builds. * feat(identity): propagate embedding-agent correlation metadata Carry optional MARSHAL_AGENT_ID through authoritative hooks, shim registration, native plugins, and roster/whoami output. Serialize hook/shim registration on the server so an absent shim environment cannot erase hook metadata. Keep session IDs, operator attribution, routing, acknowledgment, and leases separate. Preserves the optional Session/roster capability proposed in closed #104, original feature commit 86ee0aa. Tests cover hook/bridge/wait transport, concurrent writers, old payloads, MCP output, exact legacy fallback, and all three native plugins reconnecting to a restarted daemon. Co-authored-by: Pulse Agent [session:01a05607-1623-7db0-8ca1-79abef182283] <agent+session-01a05607-1623-7db0-8ca1-79abef182283@agents.rship.io> Co-authored-by: lucid-max <max@lucid.rocks> * test(ci): run native embedding registration checks --------- Co-authored-by: Pulse Agent [session:01a05607-1623-7db0-8ca1-79abef182283] <agent+session-01a05607-1623-7db0-8ca1-79abef182283@agents.rship.io>
Takes upstream ignition-is-go/marshal main at 9b00c6f (v0.55 to v0.58: #131-#141, #147, #164, #165, #166) into Muster main. Conflicts were resolved file by file; the per-file resolutions are in the PR body. In short: Muster's names, crate versions, gate (MUS-59), links (MUS-66), identity (MUS-3), wake watcher (MUS-77/94), acks and leases (MUS-75/81) and MUS-90 plugin reconnect wake are kept. Upstream's --help/--version, agent_id, sticky read state (#131), #164's hook inbox budget, escaping and strict ack framing (X-Muster-Ack-V1 with a Content-Length), the native Windows CI job, and the plugins' show-before-ack fixes are taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
Workspace tests at 18d5195 (Murph)
The failure is upstream's Cause: this is the trade-off already listed under Decisions to confirm. Muster's shim keeps raw Session SET/DEL, because the gate refuses upstream's Options for the read:
This needs a decision before merging. Raw log: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: parked (draft). Conflicts are all resolved and the workspace builds with
clippy -D warningsclean, but full verification isn't finished. Per frank-crocodile, the read and the merge freeze are the next rally's first job. Before merging: bring in current Muster main (#173 and anything since), then finish the checks below.Merges upstream ignition-is-go/marshal
9b00c6f(v0.58.0; #131–#141, #147, #164, #165, #166) into Muster main. Nothing goes to ignition-is-go or Castor-o7/marshal. Conflicts were resolved with #84's (MUS-62) rules: Muster's names everywhere, upstream's additions taken, and one coherent design where both sides fixed the same thing.Conflicts resolved
Build, CI, docs, TUI, UI
Cargo.toml,crates/tui/Cargo.toml,crates/ui/Cargo.toml[patch.crates-io](MUS-59 vendored myko-server). Upstream's only dependency changes are in the shim (socket2dev-dep, theclaude_hook.rspackage exclude).Cargo.lockcrates/tui/src/main.rs#[command(name = "muster-tui", version)]: Muster's name with upstream's--version(#135).crates/ui/src/console.rsharness/identityfields on the console Session, plus upstream'sagent_id: None..github/workflows/ci.ymlwindows-native-wakejob on GitHub-hostedwindows-2025, renamed tomuster-shim. Upstream's self-hosted-only steps (resource reports, actionlint, workflow validator) aren't taken.scripts/ci/windows-isolation.ps1,scripts/ci/test_windows_isolation.ps1scripts/ci/*helpers come in unused, like the sandbox in #84..github/workflows/release.yml.github/workflows/public-ci-acceptance.yml,public-ci-red-team.ymlplugins/marshal-shim/*hooks/hooks.jsonis identical to Muster'splugins/muster-shim/hooks/hooks.jsonapart from the name, and Muster's README already documents the hooks.README.mdasyncRewakewait, statusline). Licence: Muster's wording with upstream's precise list of AGPL dependencies (myko, myko-macros, autosocket, myko-server, myko-leptos), in Muster's crate names. Inbox-block paragraph: upstream's sentence on the 10,000-character hook-block limit,remainingand escaping, since the merged hooks.rs takes #164's budget and escaping.docs/install.mdcrates/daemon, crates/entities
crates/{daemon,entities}/Cargo.tomlversion.workspace,rust-version, metadata. Upstream changed only the version (0.54.1 → 0.58.0), so there are no dependency bumps to take.entities/src/lib.rssuccessionandwake_pausemodules.entities/src/message.rsto_session_iddoc (#131 reload/resume wording). Muster'soperator@example.comexample.truncated_notice(new from upstream) points atmuster://messages.entities/src/message_read.rssweep_orphan_reads), withmuster-pi. The behaviour is the same on both sides: Muster's MUS-46 had already dropped the session cascade.entities/src/session.rsacks_pushesdoc (MUS-81, plugin wording) plusharness/identity. Took upstream'sagent_idfield and tests (#166), and itsRegisterSessioncommand plusupdate_registered_session. That command is harmless; see Open questions.entities/src/broadcast_message.rsbody_too_large, andMessageReadfor #140'sacks_pushesskip). Muster'sroom_label(&room.id.0)with upstream's truncated-id argument onmention_push_content. Took upstream's truncated-preview test, with Muster's email literal andmuster://.entities/src/send_message.rsWakeStatus::Expectedviacontrols::wakes_paused(), MUS-81). Took upstream'sdirect_push_content(…, truncated_id)tests, withoperator@example.comandmuster://.entities/src/wake_watcher.rs(AA)is_observed/explainand their test). Addedagent_id: Noneto its Session literal.daemon/src/main.rsbuild_serverpersister overrides (WakePause, RemoteSession),serve_until_stoppedwith the MUS-58 ready gate. Dropped the duplicateSTOP_DRAIN/stop_signalthat upstream's side reintroduced. Layered upstream #135 on top.-V/--versionprintsmuster-daemon X.--foregroundis accepted ahead of a bare or--tlsrun. Any other argument after it, or any non-UTF-8 argument (viaargs_os), is refused with exit 2,muster-daemon: unknown argument: …andTry 'muster-daemon --help'.(this used to be an anyhow bail with exit 1).--helpnow printsusage(): name and version,Usage: muster-daemon …, Muster's command list (USAGE), options, and environment (MUSTER_BIND,MUSTER_HOOK_BIND,MUSTER_TLS,MYKO_POSTGRES_URL,RUST_LOG).USAGEstill beginsusage:for subcommand errors.daemon/tests/cli_flags.rs(upstream A)muster-daemon/MUSTER_*. It also setsMUSTER_STATE_DIRto a fresh dir underCARGO_TARGET_TMPDIRand removesMUSTER_TLS, so the--foregroundrun never reads the operator's real pairings, pause or tokens.daemon/src/http_listener.rsAccess(local token +machine_may_act, MUS-7),run_bound, conn limiter, MUS-10 pause on/hook/wait,X-Muster-Restarting. Took upstream'sack_header()(#164 boundary fix: a JSON id list, DEL escaped) asX-Muster-Ack-V1, which matches the merged shim'slink.rs. While every id is plain ([A-Za-z0-9._:-]), it also sends the old comma-separatedX-Muster-Ack, for hook clients from before V1 andscripts/test-gate-e2e.sh. Every hook response, refusals included, goes throughwrite_response_with, which always sendsContent-Length. I added401 Unauthorizedand503 Service Unavailablereason phrases (they used to say "OK"). The gate's own 426 refusals (gate.rs) already carryContent-Length.daemon/src/hooks.rsAUTO_INBOX_OUTPUT_BUDGET_UTF16),peer_text_within/peer_handle/peer_attr(escape& < >, every line-break class, drop invisible and bidi characters), the per-line escape-aware cut, the 192 KiB ack-header cap, and leasing only the messages shown (so no release step is needed). Layered from Muster: MUS-66 link-aware sender (nickname@linkviaMessageLink/RemoteSession); MUS-86 reoffer log; theMUSTER_ACK_LEASE_SECSenv var; session-startensure_assigned(MUS-3) andsuccession::take_notice, now with upstream'speer_attron the nickname;with_known_platformandattested(gate-attested operator and host).register_hook_sessionnow goes through upstream'supdate_registered_session(a serialised read-modify-write, withagent_idfrom the body) carrying Muster's attestation, harness and identity logic. That is a directemit_set, not theRegisterSessioncommand. Muster'squote_body/quoted_previewand its char-count budget (INBOX_BLOCK_MAX_CHARS) are replaced by upstream's equivalents. The Muster tests for them were dropped, since upstream's renamed tests cover the same cases, anda_message_body_cannot_forge_inbox_framingwas kept and moved ontopeer_text_within.truncation_noticeuses upstream's sharedtruncated_noticetext.daemon/src/cleanup.rssweep_memberships, kept-session reaping, boot tests). Took upstream'ssweep_orphan_readsinsweep_messagesand its test. Merged theMESSAGE_TTL/sweep_messagesdocs: MUS-46, plus the orphan-read sweep that replaces "read rows go at next boot".daemon/tests/send_message_command.rspause_state, the paused-waiter and paused-push tests, the pull-waiter test). Added upstream'sa_recipient_with_a_waiter_expects_a_wake, holdingpause_state(false). Muster'sidempotency_key,harnessandidentityliterals stay.daemon/tests/send_message_e2e.rs,broadcast_message_command.rs,ws_reconnect_subscription.rsagent_id.daemon/tests/hook_inbox_e2e.rs[truncated; full messagetext withmuster://.daemon/tests/hook_client_ack.rs(AA)run_until(.., None, ..),MUSTER_ACK_LEASE_SECS,idempotency_key). Took upstream's V1ack_idsparser and its four tests: injection round-trip, undisplayed backlog, oversized id, concurrent readers. Addedthe_legacy_ack_header_is_sent_only_for_plain_ids.daemon/tests/hook_wait_e2e.rs(AA)agent_id. The hostile-body assertion now expects</muster_inbox>, because upstream escapes>as well.daemon/tests/hook_inbox_framing.rs,hook_inbox_safety.rs,session_del_inbox_e2e.rs(upstream A, not conflicted)harness/identityandidempotency_key: Noneso they build.hook_inbox_framingandhook_inbox_safetycallmuster_entities::rate::set_enabled(false)in setup: they send more than the 20-message burst, and plain-mode Muster has no send limits either (MUS-64).Sessionliterals (cleanup.rs,succession.rs,auto_rooms.rs,entities/send_message.rstests, andtests/{identity_settle,identity_succession,pause_and_allow,rate_limit,read_messages_recipient,redaction,remote_send}.rs)agent_id: Noneadded so they build.cleanup.rstests: dropped Muster's duplicateset_read/read_idshelpers, which are equivalent to the ones upstream's test brought in.cargo fmtapplied to both crates.crates/shim
crates/shim/Cargo.tomlexclude = ["tests/claude_hook.rs"]andsocket2dev-dep came in cleanly.src/link.rs(add/add)redeem_invite,https_post_why, andPostFailurewithCertificate/NotTls/426 and its MUS-83 wording). Layered from upstream #164:MAX_RESPONSE_BYTES,response_length, framedread_reply(stops at Content-Length, one deadline), and the strictparse_response: the body must be exactly its Content-Length, no Transfer-Encoding, one ack header, no control characters, no acks on an empty body. A body short of Content-Length is rejected and nothing is acked from it. Ack header is nowX-Muster-Ack-V1: <JSON array>(upstream'sX-Marshal-Ack-V1, renamed); restarting headerX-Muster-Restarting.connect_anytakes a deadline (upstream), but Muster's 5 s per-attempt cap stays (a paired gate is across a network).https_post_whynow connects by a deadline and caps the TLS read atMAX_RESPONSE_BYTES + 1; its reply still goes through the strictparse_response. Tests: all of upstream's (renamed), Muster's not-a-gate TLS test, plus two new ones: 401/403/426 +Certificateare refusals, and the old comma-separatedX-Muster-Ackis ignored.src/codex_hook.rsidentity_qMUS-3,HookRouteMUS-18, MUS-22 operator line, MUS-38 nickname cache, MUS-7 local token,MUSTER_*env). Layered from upstream: one deadline per request (http_post_why(.., deadline),resolve_within, write loop,link::read_reply);TIMEOUT5 s → 3 s (fits CodexSessionEnd's 3 s);registration_bodywithagent_idfromMUSTER_AGENT_ID(trimmed, body only, never the query) on every hook, the wait andsession-register;acquire_waiter_lockwith the parent check;poll_timeout/default_lock_wait(70 s, was 35 s);deliverreturnsio::Resultandwrite_watch_eventsso a closed Monitor/hook pipe leaves mail unacked;resolve_basenow uses the shim's own order (file, then env) viacrate::unpaired_daemon_address(); Muster's previous env-first order was a mismatch with the MCP path. New glue:hook_pairingandpost_why(pairing over TLS, else plain, both by deadline). Hook ack: its ownharness.timeout()budget (Muster had used 10 s; upstream reused the request's remaining deadline). Upstream's new unit tests added.src/main.rsdaemon_address()with pairing/TLS andrefusal::Credential,with_local_token, MUS-3 harness/identity, MUS-26, MUS-38, wake_pause/remote_sessions cells). From upstream:-h/--help,-V/--version(muster-shim <ver>),unknown argument … (try 'muster-shim --help');usage(), rewritten to list every Muster subcommand, the pairing,muster/daemon-address,MUSTER_DAEMON_ADDRESS,MYKO_ADDRESS, 6255 andMUSTER_BASE_URL;first_daemon_address(pure, tested) behind a new syncunpaired_daemon_address(), which Muster'sdaemon_address()now uses in its unpaired branch; #140forward_push(acks anew_message/mentionpush withAckMessagesonce written) andacks_pushes: (!is_codex).then_some(true);agent_id: codex_hook::agent_id_from_env()on the Session; theclaude-hook waitcomment. A duplicatewake_watchers_cellfrom upstream was dropped. Not taken: upstream'sRegisterSession/DeleteSessioncommands inemit_session_set/emit_session_del, so Muster's raw Session SET/DEL events stay (see Open questions).src/tools.rsmuster_entities, wake_pause/remote_sessions cells, MUS-3declared_role/harnesson the roster), with upstream'sagent_idon whoami/roster andpub(crate) await_command, which merged cleanly. Upstream's newcodex_identity_metadata_comes_only_from_the_authoritative_rowtest now builds a ToolHost with Muster's extra cells and usesmuster://URIs.src/channels.rsmacos_reads_the_parent_cmdline, which Muster already has (duplicate).src/codex_bridge.rssession()literal (harness/identity, plus upstream'sagent_id: None, which merged cleanly). Upstream'smessage()test helper was dropped: its only callers are tests Muster replaced with the MUS-77/94 per-path watcher.tests/claude_hook.rs(add/add)MUSTER_AGENT_IDchecks,APPDATAisolation, and the boundary tests (incomplete reply, trickled reply, closed Monitor pipe, oversized reply, waiter takeover, agent_id only in the JSON body). Their raw frames useX-Muster-Ack-V1.tests/codex_bridge_e2e.rsMUSTER_BASE_URL,MUSTER_HOST,muster_shim=debug) plus upstream's agent id asMUSTER_AGENT_ID; Muster's Session literal.tests/claude_push_ack_e2e.rs,tests/cli_flags.rs(new from upstream)muster-shim,muster_entities,MUSTER_*,plugin:muster-shim@muster).cli_flagsalso checks that--helpnames claude-hook, watch, tell, all, roster, doctor, setup, pair, pairing, unpair andMYKO_ADDRESS.plugins
muster-pi/src/inbox-pull.ts,test/inbox-pull.test.ts(AA)InboxPullerplusholdPush/PushHold, per-hold leases andholdLeaseFromEnv. That makes it a superset: upstream's file has no push holds and no lease. Muster's tests are upstream's 8 plus 10 for holds and leases.muster-pi/src/live-push.ts(AA)deliverLivePush(hold, inject, commit or release; pull fallback acked once shown). Upstream's version (#134: inject, thenack(message_id), with adrainInboxfallback that acks before showing) is replaced.livePushTextnow comes from upstream'sinbound.tsand is re-exported, so Muster's tests still import it from here.muster-pi/src/inbound.ts,test/inbound.test.ts(UA)muster_messages. The doc comment now says the full body stays in the durable store, because withacksPushesthe daemon no longer marks a push read when it delivers it. Test comment adjusted the same way.muster-pi/test/live-push.test.ts(AA)drainInbox/ackfake tests are dropped, because they test the replaced mechanism.muster-pi/src/daemon.tsInboxPullerwithleaseMs: holdLeaseFromEnv(), plusholdPush. Upstream'sdrainInboxis dropped (nothing uses it now). Took upstream'sagentIdon Session (auto-merged). MUS-90'severConnectedreconnect wake is unchanged.muster-pi/src/index.tshold/pull). Took upstream #147'sbefore_agent_startprompt guard (const prompt,pushMusterStatus). Muster'smuster-inboxcustomType.agent_idin whoami and roster came through the auto-merge.muster-pi/src/entities.tsmessage_iddoc comment plus upstream'sCONTEXT_BODY_MAX_CHARSbody comment.muster-pi/test/{before-agent-start,embedding-agent}.test.ts(UA)muster-opencode/src/index.tsclaimShownguard at the top of the push handler, then upstream #137:pushText()(flags truncated previews) and no drain after the prompt (it acked messages without showing them). The reconnect wake (everConnected, a body-less notify after the re-SETs) is indaemon.ts, which upstream didn't conflict with. A body-less notify still goes throughdrainInbox, anddrainInboxInnerstill usesclaimShown. The comment explains how the two fit: chat.message's drain for that prompt acks the pushed id but doesn't show it again.muster-opencode/src/entities.tsmessage_idcomment (MUS-90).muster-opencode/README.mdmuster-opencode/src/push.ts,test/push.test.ts,test/embedding-agent.test.ts(UA)muster-prime-agent/src/inbox-pull.ts,test/inbox-pull.test.ts(UA)InboxPuller.holds(sessionId, id), so a push can see that a pull holds a message.muster-prime-agent/src/daemon.tspullInbox(ack only once shown) with MUS-90's shown-id guard built into it.drainInboxis gone.claimShown(sid,id)returns false if a push or a committed pull already showed the id, or if an outstanding pull holds it. The puller'sreadisreadUnshown: it acks ids that were already shown without showing them again, and returns the rest. The puller'sack(called on commit) records the ids as shown before it acks. A released pull claims nothing, so its mail stays unread for the next pull. Took upstream'sINBOX_COMMAND_TIMEOUT_MSandsendForInbox(bounded inbox commands). MUS-90'severConnectedreconnect wake is unchanged. Its body-less notify goes to the pull fallback inindex.ts.muster-prime-agent/src/index.tspullForTurnper turn, and the body-less (reconnect) fallback injects, then commits, or releases on failure. Muster'sclaimShowncheck on body pushes and its inline truncation notice are kept (auto-merged).muster-prime-agent/test/daemon.test.ts(AA)drainInboxtopullInbox+commit. Upstream's "returns at once while disconnected" test moved from node:test to bun'sexpect. Two new tests: a push skips a message a pull holds, and a released pull neither acks nor claims.muster-prime-agent/test/embedding-agent.test.ts(UA)plugins/marshal-shim/*Decisions to confirm in the read
X-Muster-Ack-V1(a JSON array, with a Content-Length) and also the old comma-separatedX-Muster-Ackwhen the ids are plain, for older hook clients during rollout.scripts/test-gate-e2e.shstill reads the old header. It can be removed for a clean cutover.RegisterSession/DeleteSessioncommands are kept on the daemon but unused, becausegate_filter.rsrefuses unlisted commands. Adopting them needs gate rules first.>is escaped as>in inbox bodies (upstream docs: design for keeping messages across restarts without Postgres (MUS-17) #164).pullInbox(ack once shown) with MUS-90'sclaimShownbuilt in, sodrainInboxis gone. Its remaining gap is MUS-81 (noacksPushesyet).Verification so far (Murph, at 18d5195)
cargo build --workspaceandcargo clippy --workspace --all-targets -- -D warnings: clean.cargo fmt --check: clean.cargo test -p muster-entities -p muster-daemon: 260/260 (resolution agent, before integration).cargo test: see the follow-up comment (a first run was invalid because HOME was overridden).cargo deny(not installed on Murph), Zootrunk build, the conformance grid, and a merge of current main.Not done here
🤖 Generated with Claude Code