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
137
138
139
140
141
//! The detached driver's captured stderr (DESIGN §8.1, §13.3, §4.2, §5.2).
//!
//! A detached `litany 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 folded capture 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.
//!
//! **Whether to fold at all is not this file's question** (bl-b95e). This file
//! is a *transport*: it names the sink and reads its tail, and says nothing
//! about what the tail means. For two rulings it was folded into every `-2`
//! row, which made the meaning "the driver said anything at all" — and
//! litany'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(())`). bl-1296
//! answered with a phrase table over those sentences; bl-b95e deleted the table
//! and moved the decision to where §13.3 already puts it for `driver.log` — the
//! **state** the launch produced ([`super::launch::stillborn`]), asked by the
//! caller ([`crate::app::derive`]'s ops refresh) before it folds. Content is
//! diagnosis. That also dissolves what no table could reach: the sink is
//! append-only for the driver's whole life and this fold re-reads its tail every
//! sweep, so one unrecognized line held its origin's newest row red for every
//! later pass, however many turns the driver went on to run.
//!
//! **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 `litany 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 a line whose launch produced nothing
/// (§4.2, §7.2; the gate is [`super::launch::stillborn`], asked by the caller).
///
/// 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` (litany 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.