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
|
/// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user