feat(logs): let the sink decide ready_output - #667
Conversation
A daemon with `ready_output` kept in-process capture, because the supervisor had to read the output to match the pattern. That put it back in the data path for exactly those daemons: killing the supervisor left the pipe readerless, so the daemon took SIGPIPE moments later and its output stopped being recorded — the failure the sink exists to prevent. Give the sink the pattern instead. It matches, stores and flushes the line, then reports it over IPC; the supervisor relays it into the same channel in-process readers feed, so readiness, hooks and port detection run unchanged. Relayed lines are marked not-to-persist, since the sink has already written them. Registration happens before the sink starts, so a match in the daemon's first line has somewhere to go, and withdrawal is ownership-checked so a retry's registration cannot be removed by its predecessor unwinding. `on_output` hooks and PTY daemons still use in-process capture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 27 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c20d6ce. Configure here.
Greptile SummaryThis PR moves
Confidence Score: 3/5The PR is not yet safe to merge because valid readiness matches spanning more than the retained 4 KiB boundary context can still be missed, causing startup timeout and daemon termination. The attempt token changes resolve stale cross-retry delivery, but the capped-line fix retains only 4 KiB while ready-output regexes have no equivalent width restriction, leaving the original readiness failure reachable. Files Needing Attention: src/cli/log_sink.rs Important Files Changed
Reviews (2): Last reviewed commit: "fix(logs): tie a sink's reports to the s..." | Re-trigger Greptile |
A sink outlives its daemon — it exits only once every descendant has closed the pipe — so the sink of a failed attempt can still be draining, and reporting, while a retry starts. Reports were keyed by daemon id alone, so that late line was delivered to the new attempt and could mark a process ready before it had printed anything. Each registration now carries a token, quoted by the sink and checked before delivery, the same way monitor registrations are identified. Also match a readiness pattern across the line-length cap. An over-long line is emitted in pieces, and a pattern straddling a split matched none of them, so a daemon that had announced itself was killed at its readiness timeout instead. A bounded tail of the previous piece is carried forward, and the text reported is what the pattern matched against, since the supervisor re-matches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both P1s were real; fixed in 58c0a41. Stale reports crossing retries (@cursor, @greptile-apps — same finding). A sink outlives its daemon by design: it exits only at EOF, once every descendant has closed the pipe. So attempt N's sink can still be draining, and reporting, while attempt N+1 is starting — and since the relay was keyed by daemon id alone, N's line was delivered to N+1's channel and could mark a process ready that had printed nothing. I had guarded the withdrawal side of this ( Registrations now carry a token, minted per attempt, passed to the sink as Line cap breaking readiness (@greptile-apps). Confirmed, and it is a divergence from the in-process path, which uses
Six unit tests on the matcher, including the case that must not match: 535 unit tests, logs.bats 29/29. The This comment was generated by Claude Code. |
|
Both fixes look correct and well-implemented. Stale reports crossing retries: The token threading is thorough — Line cap breaking readiness: One minor note: The LGTM on both fixes. The two P1s are closed. |
| /// be killed at its readiness timeout despite having announced itself. Keeping | ||
| /// the tail of the previous piece closes that for any pattern shorter than this | ||
| /// while still bounding what is held. | ||
| const MATCH_CARRY_BYTES: usize = 4 * 1024; |
There was a problem hiding this comment.
Bounded carry still loses matches
When a readiness regex needs more than 4 KiB of text from before a 64 KiB split, ReadyMatcher discards the beginning of that match before evaluating the next fragment. The sink never reports the valid match, so the supervisor reaches the readiness deadline and kills an otherwise ready daemon.
Knowledge Base Used: Supervisor Core
Accurate, and deliberate. I'm not changing it, for these reasons: Removing the bound reintroduces what the cap exists to prevent. The configuration it fails on does not exist. It requires a single line over 64 KiB and a readiness pattern whose match spans more than 4 KiB of that line. Readiness patterns are things like The alternative was worse. Before the carry, any pattern straddling a split failed. Now only patterns longer than 4 KiB do. That is a strict improvement over the state you flagged in the first comment, and over unbounded growth. Worth noting what the bound is measured against: it is 4 KiB of carried context, not a limit on the pattern. A short pattern is found wherever it lands, including across the split, because the carry always reaches back further than the pattern needs. The trade-off is documented at the constant. If a pattern that long ever turns out to be real, the fix is to raise The other two comments on this PR ( This comment was generated by Claude Code. |

The gap
#661 moved log capture out of the supervisor, but
ready_outputdaemons were excluded:The reason was structural — the supervisor had to read the output to match the pattern — but the consequence is that for those daemons nothing about 2.19.0 applies. The supervisor is still the pipe's only reader, so killing it leaves the pipe readerless, the daemon takes SIGPIPE seconds later, and capture dies with it. That is the original #631 failure, still live for anyone using a readiness pattern — which is most people running a server.
The change
The sink gets the pattern (
--ready-pattern) and does the matching, since it is the process holding the stream. On the first match it stores and flushes the line, then reports it to the supervisor over IPC (SinkReadyMatch). Ordering matters: the report travels the sink's own queue behind the line itself, so by the time the supervisor hears about it,collect_startup_logsandpitchfork logscan already see the line that caused it.Supervisor-side, the reported line is relayed into the very same channel the in-process readers feed. The monitoring task cannot tell the difference, so readiness,
on_ready, active-port detection and the deadline logic are untouched — the pattern is still matched by the supervisor, so the sink is reporting "look at this line", not asserting readiness itself.OutputLinecarries apersistflag. In-process lines are written to the store by the monitoring task as before; relayed lines are not, because the sink already wrote them. Verified there is exactly one copy.Two ordering hazards worth calling out, both handled:
run_oncebefore the sink is spawned, so a daemon whose first line matches doesn't report into a void.removeinDropwould delete the new attempt's registration and hang it until its readiness timeout. The guard comparesSender::same_channelbefore removing, the same revalidate-before-acting pattern used for monitor tokens.The relay is also dropped explicitly at process exit, before the post-exit drain: it holds a sender, so leaving it registered would keep the channel open and make every drain wait out its full 5s deadline.
Still not covered
on_outputhooks andpty = true. Both still use in-process capture and remain crash-vulnerable; the module docs say so.on_outputis the natural next step — same plumbing, plus debounce — while PTY needs the sink to be handed a terminal master rather than a pipe.Tests
ready_output is decided by the sink— asserts a sink process owns the stream, thatstartwaited for the line rather than a timer, and that the deciding line is in the store exactly oncelog capture for a ready_output daemon survives a supervisor crash— SIGKILL the supervisor, daemon lives, log lines keep accruingBoth were mutation-tested: with
is_supportedreverted to main's version, both fail. 523 unit tests; 96 bats across logs, basic, hooks, port and pty.This PR was generated by Claude Code.
Note
Medium Risk
Touches daemon startup readiness, IPC, and retry ordering; mistakes could mark daemons ready early or miss readiness, but existing supervisor matching is preserved and tokens guard stale sinks.
Overview
ready_outputdaemons can use the out-of-process log sink again, so log capture and readiness no longer force the supervisor to own the output pipe (the gap left after moving logging to sinks).The hidden
log-sinkcommand gains--ready-patternand--relay-token. The sink matches readiness in-process (ANSI stripped, with carry across over-long line splits), persists the matching line before reporting via new IPCSinkReadyMatch. The supervisor relays that line into the same monitoring channel as in-process readers, usingOutputLine { persist }so sink-stored lines are not duplicated.is_supportedno longer excludesready_output;OutputRelayregisters per start attempt with token-checked delivery and careful unregister on retry. Integration bats cover sink-owned readiness and supervisor-crash survival forready_output.Reviewed by Cursor Bugbot for commit 58c0a41. Bugbot is set up for automated code reviews on this repo. Configure here.