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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
//! Per-file scan: fetch_records (show's engine), search_one_file, the candidate prefilter.
use super::*;
/// `csift show`'s fetch engine: the ADDRESSED records of exactly ONE transcript, rendered
/// FULL through the same per-record pipeline `search` uses (classify, plan pointers, tool
/// pairing, elicitation-sidecar merge) with the pure matcher, so every addressed record
/// emits regardless of any pattern. Returns the addressed exchanges + the malformed count.
pub(crate) fn fetch_records(
path: &Path,
lines: BTreeSet<usize>,
uuids: BTreeSet<String>,
turn_range: Option<crate::text::RangeSpec>,
) -> Result<(Vec<Exchange>, usize, usize)> {
let args = SearchArgs::default();
let matcher = Matcher::pure();
// A line/uuid ADDRESS restricts to named records; a `--turn` range (address empty) selects
// every record of the named turns - the SAME per-file grouping `search` numbers turns by, so
// `show --turn N` is byte-identical to the turn `search` cites as `<tok>·tN`.
let address = AddressSet { lines, uuids };
let use_address = !(address.lines.is_empty() && address.uuids.is_empty());
let time_window = TimeWindow::from_args(None, None)?;
let mut spawn_map: HashMap<PathBuf, Option<Arc<DiscoveredSpawns>>> = HashMap::new();
spawn_map
.entry(discovery_root_for(path))
.or_insert_with_key(|root| build_spawn_lookup(root).map(Arc::new));
let fr = search_one_file(
path,
&args,
&matcher,
turn_range,
&time_window,
use_address.then_some(&address),
false,
&spawn_map,
// ONE transcript - nothing else fills the pool, so the inner fan-out is free.
true,
)?;
Ok((fr.exchanges, fr.skipped_lines, fr.turn_count))
}
/// Scan a single session file: prefilter → parse → delimit turns → match → stitch.
#[allow(clippy::too_many_arguments)]
pub(crate) fn search_one_file(
path: &Path,
args: &SearchArgs,
matcher: &Matcher,
turn_range: Option<crate::text::RangeSpec>,
time_window: &TimeWindow,
address: Option<&AddressSet>,
want_siblings: bool,
spawn_map: &HashMap<PathBuf, Option<Arc<DiscoveredSpawns>>>,
inner_parallel: bool,
) -> Result<FileResult> {
let Some(mmap) = mmap_bytes(path)? else {
return Ok(FileResult {
exchanges: Vec::new(),
skipped_lines: 0,
turn_count: 0,
superseded_drafts: 0,
});
};
let bytes: &[u8] = &mmap;
// A GIANT transcript's per-turn match phase fans out even under a broad scan: such a
// file is the straggler the rest of the pool ends up waiting on (only a handful of
// files this size exist, so the nested fan-out adds no measurable steal churn - unlike
// enabling it for every mid-size file, which did).
const HUGE_FILE_BYTES: usize = 64 * 1024 * 1024;
let inner_parallel = inner_parallel || bytes.len() >= HUGE_FILE_BYTES;
// The D7 `compact_boundary` prefilter-widening is GATED on the active `-t` selector -
// computed up front because BOTH the whole-file gate below and the candidate scan key on it.
let needs_compact_boundary = args
.label_filter()
.selected(Class::CompactionBoundary.path());
// The `--additional-context` widening mirrors the D7 gate: only when the flag is set AND
// the selector can reach `harness.meta.hook` - OR when an ADDRESS names records directly
// (`show --line`/`--uuid` must render an addressed attachment record flag-free).
let needs_hook_context = (args.additional_context
&& args.label_filter().selected(Class::MetaHook.path()))
|| address.is_some();
// The `--attachments` widening (a SUPERSET of `--additional-context`): keep EVERY
// `type:"attachment"` line when the flag (or the `--count-by attachment` axis, which
// implies it) is set AND the selector can reach either `harness.meta` leaf - or when
// an ADDRESS names records directly (`show --line`/`--uuid` renders any addressed
// attachment record flag-free).
let needs_attachments = (args.scan_attachments()
&& (args.label_filter().selected(Class::MetaAttachment.path())
|| args.label_filter().selected(Class::MetaHook.path())))
|| address.is_some();
// v0.10.0 promoted non-record lines: admitted ONLY under an EXPLICIT selector that
// reaches the leaf (`reaches_gated` - a bare no-`-t` scan never parses them) or an
// address (`show --line`/`--uuid` renders an addressed line flag-free).
let reach = |c: Class| args.reaches_gated(c) || address.is_some();
let gates = CandidateGates {
compact_boundary: needs_compact_boundary,
hook_context: needs_hook_context,
attachments: needs_attachments,
queued: reach(Class::UserQueued),
turn_duration: reach(Class::MetaTurnDuration),
away_summary: reach(Class::MetaAwaySummary),
stop_hooks: reach(Class::MetaStopHooks),
snapshot: reach(Class::MetaSnapshot),
};
// ── §7f whole-file gate ──
// When the pattern anchors a raw-byte prefilter (a plain literal, either case mode) and
// this is NOT an addressing fetch (`--line`/`--uuid` emit records regardless of the
// pattern), a cheap PARALLEL pre-scan can prove that no candidate line matches: no
// per-line literal occurrence AND no synthesized-text marker (see [`Matcher::synth`]).
// Every emitted exchange requires >=1 regex hit (`hits.is_empty() -> continue`), so such
// a file provably yields nothing - skip building records for it entirely. Mechanics:
// - the pre-scan runs on the SAME newline-aligned rayon chunking as the full scan (never
// a serial whole-mmap pass - that would bottleneck the single-giant-file case);
// - a relaxed AtomicBool short-circuits it the moment ANY line may match: the remaining
// lines skim (one load + return), the partial malformed count is discarded, and the
// full scan below recounts exactly - a file WITH matches pays only the skim;
// - the malformed-line count is a TESTED contract (no silent skip): a gated file's
// candidate lines were each syntax-validated (`validate_line_syntax` - no Record
// build, no allocation) before the verdict, so real corruption (torn writes) counts
// exactly as the full scan would;
// - the elicitation-sidecar merges live OUTSIDE these bytes (a separate tiny file,
// top-level sessions only): when any are pending they could still match, so fall
// through to the normal scan (rare); their malformed count is reported either way.
if address.is_none() && matcher.has_prefilter() {
use std::sync::atomic::{AtomicBool, Ordering};
let force_full = AtomicBool::new(false);
// Pre-scan verdict per candidate line: a literal or CONSERVATIVE-marker hit
// forces the full scan (flag + short-circuit); a VERIFIABLE-marker line is
// COLLECTED for stage-2 (parsed there, so its malformed accounting happens
// there too); anything else is syntax-validated for the malformed count.
let (marker_lines, mut gate_skipped): (Vec<Vec<u8>>, usize) =
crate::parse::scan_lines_parallel(bytes, |line, _| {
if force_full.load(Ordering::Relaxed) {
return crate::parse::LineVerdict::Ignore; // verdict already "full scan"
}
if !line_is_transcript_candidate(line, &gates) {
// R10: obviously-corrupt non-candidates are COUNTED (the malformed law).
return crate::parse::non_candidate_verdict(line);
}
if matcher.line_prefilter_hits(line) || matcher.synth_conservative_hits(line) {
force_full.store(true, Ordering::Relaxed);
return crate::parse::LineVerdict::Ignore;
}
if matcher.synth_verifiable_hits(line) {
return crate::parse::LineVerdict::Keep(line.to_vec());
}
match crate::parse::validate_line_syntax(line) {
Ok(()) => crate::parse::LineVerdict::Ignore,
Err(_) => crate::parse::LineVerdict::Skip,
}
});
// Stage-2: re-render each collected marker line's SYNTHESIZED texts through the
// shared engines and regex-check them. A malformed marker line is counted here
// (it was deliberately NOT validated in the pre-scan - no double count).
let mut synth_matched = force_full.load(Ordering::Relaxed);
if !synth_matched {
for raw in &marker_lines {
match crate::parse::parse_line(raw) {
Ok(Some(rec)) => {
if matcher.synth_texts_match(&rec) {
synth_matched = true;
break;
}
}
Ok(None) => {}
Err(_) => gate_skipped += 1,
}
}
}
if !synth_matched {
// No candidate line can hit => no exchange can emit; `gate_skipped` is the
// exact malformed count (every candidate line was validated or parsed once).
if crate::subagent::is_subagent_path(path) {
return Ok(FileResult {
exchanges: Vec::new(),
skipped_lines: gate_skipped,
turn_count: 0,
superseded_drafts: 0,
});
}
let (pending, pending_skipped) = crate::elicitation::unresolved_pending(path)?;
if pending.is_empty() {
return Ok(FileResult {
exchanges: Vec::new(),
skipped_lines: gate_skipped + pending_skipped,
turn_count: 0,
superseded_drafts: 0,
});
}
}
}
// Retain every TRANSCRIPT record in file order (genuine users delimit turns;
// the rest are turn members). Two-stage prefilter (§7d):
// 1. CATEGORY prefilter - drop pure-noise lines (attachment/system/metadata)
// pre-JSON. This is the dominant cost win (attachment alone is 54% of
// records). Broad-by-design (a role substring) so no genuine turn is lost.
// 2. KEYWORD prefilter - a per-line `memmem` of the regex's required literal.
// It does NOT gate parsing (a non-matching record may still be a sibling in
// a matched turn's round-trip); instead it records `can_hit`, letting the
// match phase skip regex work on records that provably can't match.
// Parse all transcript-candidate lines IN PARALLEL (newline-aligned chunks on the rayon pool)
// so a single giant transcript is not scanned on one core. The stage-2 keyword prefilter
// (`can_hit`) is computed per line inside the parallel scan, where the raw bytes are in hand.
// The D7 `compact_boundary` prefilter-widening is GATED on the active `-t` selector: only look
// for the rare `type:"system"` boundary line when a selector can actually reach
// `harness.compaction.boundary` (or no `-t` = match-all). A `-t user` / `-t agent.*` search can
// never match a boundary, so it pays ZERO for the extra check - the hard `-t` filter PRUNES the
// byte-scan instead of taxing it (computed once above the whole-file gate, captured here).
let (mut records, mut skipped) = crate::parse::scan_lines_parallel(bytes, |line, line_no| {
if !line_is_transcript_candidate(line, &gates) {
// R10: obviously-corrupt non-candidates are COUNTED (the malformed law).
return crate::parse::non_candidate_verdict(line);
}
let can_hit = matcher.line_may_match(line);
match crate::parse::parse_line(line) {
Ok(Some(rec)) => crate::parse::LineVerdict::Keep(Kept {
rec,
can_hit,
line_no,
from_sidecar: false,
}),
Ok(None) => crate::parse::LineVerdict::Ignore,
Err(_) => crate::parse::LineVerdict::Skip,
}
});
// ── Transparent elicitation-sidecar merge (§3.10) ──
// A TOP-LEVEL session may have a hook-written `elicitations.jsonl` carrying the
// unresolved-pending AskUserQuestion/ExitPlanMode/MCP records that are MISSING from the
// native transcript (whole-turn buffered / in-memory). Merge them in as native-shaped
// records so they classify + match normally; they have no physical line (line_no 0,
// from_sidecar). Subagent transcripts have no sidecar (it is keyed by the top-level
// session). The merge is near-free when nothing is pending (typically 0 records).
if !crate::subagent::is_subagent_path(path) {
let (pending, pending_skipped) = crate::elicitation::unresolved_pending(path)?;
skipped += pending_skipped;
for rec in pending {
records.push(Kept {
rec,
can_hit: true, // no physical line to prefilter - let the matcher decide.
line_no: 0,
from_sidecar: true,
});
}
}
let (mut exchanges, turn_count, superseded_drafts) = reconstruct_and_match(
path,
&records,
args,
matcher,
turn_range,
time_window,
address,
want_siblings,
spawn_map,
inner_parallel,
);
// `--raw`: backfill each hit's VERBATIM source line from this file's mmap - one pass
// over the wanted line numbers only; the render layer then emits bytes, never a
// re-render (a re-serialization would not be verbatim).
if args.raw {
let wanted: std::collections::BTreeSet<usize> = exchanges
.iter()
.flat_map(|e| e.hits.iter())
.filter(|h| !h.from_sidecar && h.line > 0)
.map(|h| h.line)
.collect();
if !wanted.is_empty() {
let mut raw_by_line: HashMap<usize, String> = HashMap::new();
let mut ln = 0usize;
let _ = crate::parse::scan_lines_bytes(bytes, |line| {
ln += 1;
if wanted.contains(&ln) {
raw_by_line.insert(ln, String::from_utf8_lossy(line).into_owned());
}
});
for ex in &mut exchanges {
for h in &mut ex.hits {
if let Some(r) = raw_by_line.get(&h.line) {
h.raw = Some(r.clone());
}
}
}
}
}
Ok(FileResult {
exchanges,
skipped_lines: skipped,
turn_count,
superseded_drafts,
})
}
/// The `&&`-gated candidate keeps beyond the role marker (each one a SIMD memmem that
/// runs only when its gate is on, so a default scan pays ZERO for all of them).
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct CandidateGates {
/// D7: the `compact_boundary` metrics record (selected label).
pub(crate) compact_boundary: bool,
/// `--additional-context` (or an address): hook-injected context attachments.
pub(crate) hook_context: bool,
/// `--attachments` / the attachment axis (or an address): every attachment line.
pub(crate) attachments: bool,
/// v0.10.0 (explicit selector or address): `queue-operation` lines.
pub(crate) queued: bool,
/// v0.10.0: `system`/`turn_duration` lines.
pub(crate) turn_duration: bool,
/// v0.10.0: `system`/`away_summary` lines.
pub(crate) away_summary: bool,
/// v0.10.0: `system`/`stop_hook_summary` lines.
pub(crate) stop_hooks: bool,
/// v0.10.0: `file-history-snapshot` + `file-history-delta` lines.
pub(crate) snapshot: bool,
}
/// §7d stage-1 category prefilter on raw bytes: keep a line only if it could be a
/// transcript message (user/assistant role marker) - drops `attachment`,
/// `file-history-*`, `queue-operation`, and metadata noise pre-JSON unless the
/// matching [`CandidateGates`] flag admits them. Kept deliberately permissive
/// (substring, not structural) so no genuine turn is lost.
pub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {
let needs_compact_boundary = gates.compact_boundary;
let needs_hook_context = gates.hook_context;
let needs_attachments = gates.attachments;
// Every user/assistant record carries a `"role":"user"`/`"role":"assistant"`
// marker (genuine-user string content, tool carriers, assistant blocks all do).
// R13: matched serialization-tolerantly - `"role": "user"` (reserialized JSON,
// whitespace around the colon) is the same record and must not vanish silently.
static COMPACT_BOUNDARY_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"compact_boundary"));
// R13 needle law: a bare VALUE substring (the attachment payload's `type` value), never a
// compact `"key":"value"` byte pair - a reserialized line keeps the value intact.
static HOOK_CONTEXT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"hook_additional_context"));
// Quoted needle: `"attachment"` appears verbatim as the record's `"type"` VALUE (and as
// its payload KEY); an in-content quote is escaped to `\"` in raw bytes, so prose that
// merely mentions the word never false-keeps. Serialization-tolerant (the quoted value
// survives a reserialize; R13).
static ATTACHMENT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"\"attachment\""));
// v0.10.0 promoted lines. Quoted-value / bare-value needles per the R13 law: the
// `type` values `"queue-operation"` and `"file-history-` (a prefix covering both
// `-snapshot` and `-delta`), and the bare `subtype` values for the three system
// records (a value substring survives a reserialize; prose quoting the word lands
// on a role line and is harmless - it already parses).
static QUEUED_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"\"queue-operation\""));
static TURN_DURATION_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"turn_duration"));
static AWAY_SUMMARY_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"away_summary"));
static STOP_HOOKS_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"stop_hook_summary"));
static SNAPSHOT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memmem::Finder::new(b"\"file-history-"));
crate::parse::line_has_role_marker(line)
// D7: ALSO keep the rare `compact_boundary` metrics record (a `type:"system"` record with no
// role marker) so `search -t harness.compaction.boundary` can enumerate compaction points +
// inspect their `compactMetadata` - but ONLY when an active `-t` selector can reach that label
// (`needs_compact_boundary`, derived once via `label_selected`). For every other query the
// `&&` short-circuits BEFORE the memmem, so a non-boundary search pays ZERO. When it IS run,
// the `||` chain still reaches this memmem only on lines that already failed both role checks,
// and boundary records are rare - so the §7 perf contract holds either way.
|| (needs_compact_boundary && COMPACT_BOUNDARY_FINDER.find(line).is_some())
// Opt-in hook-injected additionalContext (`search --additional-context`, or an explicit
// `show --line`/`--uuid` address - the refetch a search hit prints must resolve without
// the flag). Same `&&`-gating law as the boundary: a default scan pays ZERO.
|| (needs_hook_context && HOOK_CONTEXT_FINDER.find(line).is_some())
// Opt-in FULL attachment keep (`search --attachments` / `--count-by attachment`, or an
// explicit address). Same `&&`-gating law: a default scan pays ZERO.
|| (needs_attachments && ATTACHMENT_FINDER.find(line).is_some())
// v0.10.0 promoted lines, each behind its own explicit-selector gate.
|| (gates.queued && QUEUED_FINDER.find(line).is_some())
|| (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())
|| (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())
|| (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())
|| (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())
}