Transcript Incremental Projection
Nucleic-Session: 0FFD007B-0696-4517-9429-129C7B0FD5AC Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -102,26 +102,71 @@ enum TranscriptProjection {
|
||||
/// 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
|
||||
/// 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] {
|
||||
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)
|
||||
// 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
|
||||
// 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
|
||||
// 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),
|
||||
worktreeRoot: root)
|
||||
return coalesceToolRuns(flat).map {
|
||||
worktreeRoot: root, priorEdits: priorEdits, priorLockPatches: &patches)
|
||||
let items = coalesceToolRuns(flat).map {
|
||||
attachSubagentChildren($0, byParent: byParent, depth: 0,
|
||||
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
|
||||
/// 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).
|
||||
private static func worktreeRoot(in events: [AgentEvent]) -> String? {
|
||||
static func worktreeRoot(in events: ArraySlice<AgentEvent>) -> String? {
|
||||
for event in events {
|
||||
if case .sessionStarted(let started) = event.kind, !started.cwd.isEmpty {
|
||||
return started.cwd
|
||||
|
||||
Reference in New Issue
Block a user