Expand description
§1/§4/§6 the unified per-clone op log — one JSON-lines sink per clone.
balls owns ONE log file per clone bundle (crate::layout::CloneDir::op_log
— clones/<enc>/log), not a per-plugin or per-op-phase tree. Every record is
one JSON object on its own line {ts, lvl, src, op, phase, msg}, with src
either core (balls’ own lifecycle records — begin/invoke/seal/abort) or a
plugin name (balls envelopes each line of that plugin’s stderr, §6). The
source is a stamped FIELD, so a reader greps one source or reads the whole
interleaved sequence; metrics (§6) are a query over it, never core state.
The log is LOCAL runtime state — gitignored, never committed (like
binding.toml), no rotation/retention (stale-but-harmless like an orphan
worktree; the Level threshold limits volume instead). One object per line
keeps concurrent appends from parallel agents atomic: every line is held at or
below [LINE_MAX] (PIPE_BUF) bytes so a single O_APPEND write never
interleaves with another agent’s. The only unbounded field is the enveloped
plugin-stderr msg (a stack trace, a long git error, a diff line); it is
truncated with a [TRUNC_MARKER] so the whole record fits (§1, bl-e6a0).
A single threshold (Log::record) gates BOTH file persistence and the
terminal echo: a record below it is emitted nowhere. The two sinks differ in
SHAPE, not in gating — the file gets the JSON line (the machine record), the
terminal renders every record as its own bare msg text (the human line),
core and plugin alike — the raw JSON envelope never reaches the terminal
(bl-b6ef). The threshold is the §4
log_level (CLI --log-level ▸ XDG ▸ landing ▸ serde-default info).
Logging is best-effort — it never aborts an op, so I/O errors are swallowed.
Structs§
- Log
- The op-scoped log sink: the per-clone file path, the §4 threshold, the op
every record is stamped with, and an injected clock (so tests are
deterministic and the library does no hidden time read —
wallis the production clock).
Enums§
- Level
- The §4 severity ladder. Ordered
Debug < Info < Errorso a record is emitted iff its level is>=the configured threshold. Severity classifies the VOICE, not the op kind (bl-cf39): core narration (begin/seal/done/invoke, reads and mutates alike) isDebug— the defaultInfokeeps routine ops quiet — a plugin speaking (enveloped stderr) isInfo; a plugin’s non-zero exit and a core abort areError(outranking every threshold, so the failure locus always lands — §6).