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
//! 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 non-empty capture makes the row a rendered failure
//! ([`OpRow::failed`](super::rows::OpRow::failed) already 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.
//!
//! **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.