1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! The detached driver's captured stderr (DESIGN §8.1, §13.3, §4.2, §5.2).
//!
//! A detached `lernie prompt`'s status is never observed — the row is written at
//! launch and never rewritten — so its ops line carries the
//! [`DETACHED_EXIT`] sentinel and an empty `stderr` (§8.1), and says nothing but
//! that the handoff happened (bl-afa9: a spawn that never happened writes a
//! synthetic-failure line instead, so this sentinel means one thing). That made a driver
//! which **dies right after launch** — a version-skew refusal, a missing model
//! config — indistinguishable from a clean launch: exit `-2`, nothing rendered,
//! a prompt that visibly "does nothing". So the child's stderr is routed to a
//! per-spawn **sink file** under the yog state root, and the row's `stderr` is
//! folded in from that file **at read time** ([`fold`]) rather than copied into
//! `ops.jsonl`. The file is the authority; the row is a projection, so the fact
//! is never stored twice and a still-running driver's later output surfaces on
//! the next sweep without rewriting a durable line.
//!
//! A capture the notice classifier does not recognize makes the row a rendered
//! failure ([`OpRow::failed`](super::rows::OpRow::failed) reads `DETACHED_EXIT`
//! that way), which is what stirs the §6/§11 activity surface and the §7.3
//! ichor-red banner — no new signal, the existing machinery fed a fact it was
//! previously denied.
//!
//! **A non-empty capture was the whole test until bl-1296, and that was too
//! wide.** This file is a *transport*: it folds the tail in and says nothing
//! about what the tail means. What it meant was decided one layer up by
//! "the driver said anything at all", and lernie's contract makes this sink an
//! **operator-notice** channel as much as a dying one — declines, superseded
//! landings, accepted-crash-class launch notes, a §6 budget stop, all printed
//! on paths that return `Ok(())`. Since the sink is append-only for the
//! driver's whole life and this fold re-reads its tail every sweep, one benign
//! line held its origin's newest row red until the operator acked it.
//! [`super::notice`] is the narrow reading that tells the two apart; the fold
//! is unchanged.
//!
//! **The join key is computed, not stored.** The sink's name derives from facts
//! the ops line already carries — its `ts` and its workspace argument — so the
//! row needs no extra field to find its file, and [`sink`] is the single home of
//! the naming: the spawn side creates that path, [`fold`] reads it back.
use ;
use fs;
use ;
use ;
/// The sink directory under the yog state root, beside `ops.jsonl` (§5.2).
const DIR: &str = "detached";
/// The sink leaf's extension.
const EXT: &str = "err";
/// How many trailing bytes of a sink the projection folds in. A driver may
/// chatter for hours; the tail is where a death lands, and the bound keeps the
/// sweep's read cost flat regardless of how long the loop has run.
const TAIL: u64 = 4096;
/// Sink-name stand-in for a workspace path with no file name (`/`, `..`).
const UNNAMED: &str = "workspace";
/// The per-spawn stderr sink for a detached `lernie prompt`:
/// `<state_root>/detached/<ts>-<workspace leaf>.err`. Both sides of the fold go
/// through here — the spawn hands this path to
/// [`spawn_detached`](crate::cli_outbound::Cli::spawn_detached), [`fold`] reads
/// it back — so the naming has one home. `ts` (unix seconds) plus the workspace
/// leaf separates every spawn the operator can actually make: one fire per
/// workspace per second.
/// `entry` with its detached child's captured stderr folded in — the read-time
/// projection the ops sweep applies to every tailed line (§4.2, §7.2).
///
/// Only a [`DETACHED_EXIT`] line whose own `stderr` is empty is folded, which
/// since bl-afa9 is every `-2` line yog writes: a spawn that never launched is a
/// synthetic-failure line now, not a detached one. The empty-`stderr` guard
/// stays because `ops.jsonl` is append-only — a pre-bl-afa9 line that stored a
/// spawn error is a durable fact, and a sink must never clobber a stored one.
/// Everything else — piped verbs, synthetic failures — rides back untouched.
/// The tail of a capture file as text: at most [`TAIL`] bytes, starting at a
/// line boundary when the head was clipped (a half-line is noise, not a cause).
/// An absent or unreadable sink — the overwhelmingly common case, a clean
/// launch that never wrote — yields the empty string, leaving the row a clean
/// launch.
///
/// `pub(crate)` since bl-55d8: this is the crate's **one** bound on how much of
/// a captured stderr yog ever reads back, and the §7.3 no-response wound reads
/// a step's own `stderr.log` (lernie ARCH §2.3) through it. Nothing about the
/// bound is detached-spawn-specific — a driver that chatters for hours and an
/// adapter that retried a hundred times both die at the tail — and a second
/// spelling of "how far back do we read" is how the two would drift.
pub
/// The last [`TAIL`] bytes of `path` plus whether the head was clipped. Seeks
/// rather than reading the whole file: a long-lived driver's sink is unbounded,
/// and this runs per detached row on every sweep.