round 1: project the web transcript from append-origin events
Replace the surface-ordered fold with a log-ordered human transcript: append-origin surface events at their own log positions plus one marker per landed compaction checkpoint. Command folding, the tool-call index, and the rev-keyed memo carry over unchanged. Removes foldDegraded, the padding sentinels, baseSeq, and degradedSeqs() -- they existed only to satisfy the core fold's seq === index assertion. That also closes the pagination hole A1 exposed: a page can carry a checkpoint whose shadowed range fell outside the window, and nothing resolves surfaceOp.start anymore.
This commit is contained in:
@@ -110,8 +110,8 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
lastAssistantTurn = node.turn
|
||||
continue
|
||||
}
|
||||
if (node.kind === 'context') {
|
||||
// No trajectory cell, but the surface still advances the duration cursor.
|
||||
if (node.kind === 'context' || node.kind === 'compaction') {
|
||||
// No trajectory cell, but transcript metadata still advances the duration cursor.
|
||||
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface SpanStats {
|
||||
* Fold snapshot nodes into per-turn spans. Only assistant nodes carry a turn
|
||||
* number; user/steering/context/tool nodes attach to the turn last seen in
|
||||
* sequence order (turn 0 collects the pre-assistant prologue).
|
||||
* @param nodes - snapshot nodes in surface order.
|
||||
* @param nodes - snapshot nodes in human transcript order.
|
||||
* @returns spans ordered by first appearance.
|
||||
*/
|
||||
export function deriveSpans(nodes: ConversationSnapshot['nodes']): readonly TurnSpan[] {
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('advances the duration cursor over context nodes', () => {
|
||||
it('advances the duration cursor over context and compaction nodes', () => {
|
||||
const nodes = [
|
||||
{ kind: 'user', seq: 1, time: 1_000, content: [{ type: 'text', text: 'hi' }], source: null },
|
||||
{
|
||||
@@ -192,17 +192,21 @@ describe('deriveTrajectoryLayout', () => {
|
||||
kind: 'context', seq: 4, time: 9_000,
|
||||
content: [{ type: 'text', text: 'extra' }], source: null,
|
||||
},
|
||||
// A landed compaction renders no cell either, but is still a real log
|
||||
// position, so it moves the cursor the same way a context row does.
|
||||
{ kind: 'compaction', seq: 5, time: 9_500, summary: 'checkpoint facts' },
|
||||
{
|
||||
kind: 'assistant', seq: 5, time: 10_000, turn: 1, step: 0,
|
||||
kind: 'assistant', seq: 6, time: 10_000, turn: 1, step: 0,
|
||||
blocks: [{ kind: 'text', text: 'done' }],
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const message = turns[0]?.groups
|
||||
.flatMap(g => g.cells)
|
||||
.find(c => c.kind === 'message' && c.text === 'done')
|
||||
// From context at 9s, not from the earlier user/tool surfaces.
|
||||
expect(message?.timeSeconds).toBe(1)
|
||||
const cells = turns[0]?.groups.flatMap(g => g.cells) ?? []
|
||||
const message = cells.find(c => c.kind === 'message' && c.text === 'done')
|
||||
// From the compaction marker at 9.5s, not from context at 9s or the earlier surfaces.
|
||||
expect(message?.timeSeconds).toBe(0.5)
|
||||
// Neither the context row nor the marker contributed a cell.
|
||||
expect(cells).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user