tapes_harnesses/transcript/mod.rs
1//! Transcript tailer.
2//!
3//! tapes' wire capture yields a complete call inventory but no causal/fork
4//! skeleton — that lives only in the harness's on-disk transcripts
5//! (`~/.claude/projects/<cwd-encoded>/<sid>.jsonl` plus each session's
6//! `subagents/` directory). The transcript lane (`POST /v1/ingest/transcript`)
7//! carries them. This module is the client-side half every capture client needs:
8//!
9//! * [`files`] — discovery of a session's upload set and JSONL→records
10//! conversion. Extracted verbatim from a daemon client, which mirrors the Go
11//! reference client, so every client produces byte-identical upload sets for
12//! the same on-disk state.
13//! * [`trigger`] — the push state machine: 30 s quiescence, 5 min periodic safety
14//! net, and a final push on harness exit.
15//! * [`payload`] — the ingest payload shape, a cross-language contract with the
16//! Go server.
17//! * [`sweep`](mod@sweep) — a startup scan of the transcript tree, which finds sessions that
18//! ended while the client was not running. New here rather than moved: it closes
19//! a gap in any purely registry-driven discovery.
20//! * [`codex_anchors`] — Codex's counterpart for the fork skeleton alone. Codex
21//! writes no per-session transcript tree; the spawn edge lives in its rollout
22//! files as `sub_agent_activity` records, and this module derives the anchor
23//! rows that carry it down the same lane.
24//!
25//! The seed's `Transcript { path, harness }` placeholder is gone;
26//! [`files::TranscriptFile`] is the real shape, and it carries the subagent id and
27//! fork metadata that a transcript upload actually needs.
28//!
29//! # What stays with each client
30//!
31//! **Delivery, auth, and retry.** The HTTP call, the request timeout, the response
32//! parsing, the credential — and the failure backoff schedule — differ per client,
33//! and auth differs most of all: a client fronted by its own cloud edge rides a
34//! bespoke auth header of its own so that edge admits the request, and no such
35//! header is part of the tapes contract. So does each client's notion of *which*
36//! sessions to track: a daemon client's registry is fed by its proxy's
37//! per-request attribution, and a standalone client will have a different
38//! hook.
39//!
40//! # Why the eager design is safe
41//!
42//! Every push decision here errs toward pushing again, because the ingest endpoint
43//! is idempotent by construction: the server keys rows on a content hash of the
44//! records array, so unchanged content answers `deduped` and a grown transcript
45//! appends a new version. The fingerprint driving
46//! [`trigger::TriggerInput::dirty`] is deliberately coarse (size + mtime), retries
47//! re-read the files rather than buffering anything, and [`sweep`](mod@sweep) re-offers
48//! transcripts a previous process may already have sent. The transcript files on
49//! disk are the spool; there is no client-side queue to lose.
50
51pub mod codex_anchors;
52pub mod files;
53pub mod payload;
54pub mod sweep;
55pub mod trigger;
56
57pub use codex_anchors::{
58 AnchorKind, CodexAnchorScanner, SubAgentAnchor, anchor_records, build_anchor_payload,
59 parse_subagent_anchors,
60};
61pub use files::{
62 FileFingerprint, SubagentMeta, TranscriptFile, fingerprint, jsonl_to_records, session_files,
63};
64pub use payload::{
65 INGEST_PATH, IngestEnvelope, KIND_INTERACTED, TranscriptPayload, TranscriptSession,
66 build_payload,
67};
68pub use sweep::{SweepOptions, SweptSession, sweep};
69pub use trigger::{
70 DEFAULT_PERIODIC, DEFAULT_QUIESCENCE, DEFAULT_TICK, PushReason, TriggerInput, TriggerPolicy,
71 decide,
72};