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
//! The detached driver's captured stderr (DESIGN §8.1, §13.3, §4.2, §5.2).
//!
//! A detached `lernie prompt` has no waitable status, so its ops line carries the
//! [`DETACHED_EXIT`] sentinel and an empty `stderr` (§8.1). 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: a
/// **spawn** failure already rode the line at append time (§8.1) and is the
/// authority for that row, so it is never clobbered by a sink. Everything else
/// — piped verbs, synthetic failures — rides back untouched.
/// The tail of a sink 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.
/// 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.