Transcript Incremental Projection

Nucleic-Session: 0FFD007B-0696-4517-9429-129C7B0FD5AC
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-06 18:26:35 -07:00
co-authored by Nucleic
parent 11e0fe2d66
commit 6e22cde1e4
@@ -102,26 +102,71 @@ enum TranscriptProjection {
/// owns them, project the main agent's own events at the top level, and recursively project /// owns them, project the main agent's own events at the top level, and recursively project
/// each subagent's events into the `children` of its spawn — so a subagent's inner work nests /// each subagent's events into the `children` of its spawn — so a subagent's inner work nests
/// under its card instead of leaking (and interleaving) into the main transcript. /// under its card instead of leaking (and interleaving) into the main transcript.
///
/// One-shot form: folds the whole stream in one pass. The live transcript uses
/// `IncrementalTranscriptProjection`, which folds the stream as seam-delimited segments via
/// `buildSegment` — this wrapper is the `priorEdits: []` whole-stream case of that.
static func build(_ events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> [TranscriptItem] { static func build(_ events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> [TranscriptItem] {
buildSegment(events[...], worktreeRoot: worktreeRoot(in: events[...]), priorEdits: [],
showRaw: showRaw, showLockEvents: showLockEvents).items
}
/// An edit-class tool call folded in an *earlier* segment, carried forward so a later
/// segment's lock notes can still fold onto it across the seam (a lock's `released` note
/// lands when the file *lands* in the parent — potentially many turns after the edit).
struct PriorEdit: Equatable {
let toolCallID: String
/// Repo-relative, normalized paths this call writes (`editedPaths` ∘ `normalizeForLock`).
let paths: [String]
}
/// The folded output of one contiguous slice of the stream.
struct SegmentFold {
var items: [TranscriptItem]
/// Lock lines whose backward path-match crossed the seam onto a `priorEdits` entry, in
/// note order. The caller owns those already-folded items and must attach these to them —
/// that's what keeps `fold(prefix) ++ fold(tail)` byte-identical to `fold(whole)` even
/// for a lock note released turns after its edit.
var priorLockPatches: [(toolCallID: String, lock: NoteLock)]
/// This segment's own edit-class calls (in item order), for the caller's registry.
var edits: [PriorEdit]
}
/// Fold one contiguous slice of the stream with an injected worktree root (a mid-stream
/// slice lacks the seq-0 `sessionStarted` that `build` rescans for). The slice must be
/// seam-delimited — no coalescing key, subagent scope, or open tool run straddling either
/// end — which is exactly what `IncrementalTranscriptProjection` guarantees before calling.
static func buildSegment(
_ events: ArraySlice<AgentEvent>, worktreeRoot root: String?, priorEdits: [PriorEdit],
showRaw: Bool, showLockEvents: Bool
) -> SegmentFold {
let (topLevel, byParent) = partition(events) let (topLevel, byParent) = partition(events)
// Fold lock-lifecycle notes onto the edit cards they bracket, exactly as the Mac's // Fold lock-lifecycle notes onto the edit cards they bracket, exactly as the Mac's
// `items(_:worktreeRoot:)` does — matched against the session's working directory so an // `items(_:worktreeRoot:)` does — matched against the session's working directory so an
// edit's absolute `file_path` compares against the note's repo-relative paths. Folding // edit's absolute `file_path` compares against the note's repo-relative paths. Folding
// happens only at the top level (a subagent's inner edits are literal, unlocked); the // happens only at the top level (a subagent's inner edits are literal, unlocked); the
// subagent recursion below stays plain, matching the desktop projection. // subagent recursion below stays plain, matching the desktop projection.
let root = worktreeRoot(in: topLevel) var patches: [(toolCallID: String, lock: NoteLock)] = []
let flat = foldLockNotes(flatItems(topLevel, showRaw: showRaw, showLockEvents: showLockEvents), let flat = foldLockNotes(flatItems(topLevel, showRaw: showRaw, showLockEvents: showLockEvents),
worktreeRoot: root) worktreeRoot: root, priorEdits: priorEdits, priorLockPatches: &patches)
return coalesceToolRuns(flat).map { let items = coalesceToolRuns(flat).map {
attachSubagentChildren($0, byParent: byParent, depth: 0, attachSubagentChildren($0, byParent: byParent, depth: 0,
showRaw: showRaw, showLockEvents: showLockEvents) showRaw: showRaw, showLockEvents: showLockEvents)
} }
let edits = flat.compactMap { item -> PriorEdit? in
guard case .tool(let group) = item.kind else { return nil }
let paths = editedPaths(toolName: group.name, input: group.input)
.map { normalizeForLock($0, worktreeRoot: root) }
.filter { !$0.isEmpty }
return paths.isEmpty ? nil : PriorEdit(toolCallID: group.toolCallID, paths: paths)
}
return SegmentFold(items: items, priorLockPatches: patches, edits: edits)
} }
/// The session's working directory, read from its `sessionStarted` event, so an edit's /// The session's working directory, read from its `sessionStarted` event, so an edit's
/// absolute `file_path` can be made repo-relative to compare against a lock note's /// absolute `file_path` can be made repo-relative to compare against a lock note's
/// repo-relative paths. `nil` before the start event is seen (nothing to fold against yet). /// repo-relative paths. `nil` before the start event is seen (nothing to fold against yet).
private static func worktreeRoot(in events: [AgentEvent]) -> String? { static func worktreeRoot(in events: ArraySlice<AgentEvent>) -> String? {
for event in events { for event in events {
if case .sessionStarted(let started) = event.kind, !started.cwd.isEmpty { if case .sessionStarted(let started) = event.kind, !started.cwd.isEmpty {
return started.cwd return started.cwd