Stop losing tool calls at the seam between transcript pages
A tool's start and end are two events folded into one row, and the fold only ever *updated* an existing row -- so an end whose start was not in the same fold changed nothing and vanished. Not a broken row: no row at all, which is indistinguishable from a tool that never ran, and is what Iris saw as gaps where she remembered work happening. Paging made it routine. Each page was folded on its own and prepended, so every seam split whatever spanned it: 30 tool ends in the first page of this conversation, one of them already orphaned before a single scroll. Two changes, because the two halves fail differently. Pages are re-folded from the events they came from rather than folded apart and stitched together. That needs the events kept beside the rows, since folding is one-way. One pass over everything loaded, paid only when somebody scrolls back, which is the moment they asked for it. And an end with no start now creates a row instead of disappearing. Its name is unknown from an end alone, so it says "tool" until the earlier page arrives and replaces it -- a row that admits what it does not know beats silence, because silence is a claim that nothing happened. Verified across a real seam: scrolling back through the 80-event boundary of an 864-event import is continuous, with no gaps where tool calls were.
This commit is contained in:
1 parent
2fe34176c0
commit
e03b757dec
1 file changed
+21
-1
@@ -110,7 +110,17 @@ fun foldEvent(items: List<TranscriptItem>, event: SessionEvent): List<Transcript
|
|||||||
items + TranscriptItem.ToolRun(event.id, event.tool, event.input, "", done = false)
|
items + TranscriptItem.ToolRun(event.id, event.tool, event.input, "", done = false)
|
||||||
is SessionEvent.ToolUpdate -> updateTool(items, event.id) { it.copy(output = event.output) }
|
is SessionEvent.ToolUpdate -> updateTool(items, event.id) { it.copy(output = event.output) }
|
||||||
is SessionEvent.ToolEnd ->
|
is SessionEvent.ToolEnd ->
|
||||||
|
// Created when its start is not here, rather than dropped. A
|
||||||
|
// fold that only ever *updates* loses the whole call when the
|
||||||
|
// start fell outside the loaded window, and a tool call that
|
||||||
|
// renders as nothing is indistinguishable from one that never
|
||||||
|
// happened. The name is unknown from an end alone; loading the
|
||||||
|
// page before this one replaces the row with the real thing.
|
||||||
|
if (items.any { it is TranscriptItem.ToolRun && it.id == event.id }) {
|
||||||
updateTool(items, event.id) { it.copy(output = event.output, done = true) }
|
updateTool(items, event.id) { it.copy(output = event.output, done = true) }
|
||||||
|
} else {
|
||||||
|
items + TranscriptItem.ToolRun(event.id, "tool", "", event.output, done = true)
|
||||||
|
}
|
||||||
is SessionEvent.Question ->
|
is SessionEvent.Question ->
|
||||||
items +
|
items +
|
||||||
TranscriptItem.QuestionCard(event.id, event.prompt, event.options, answer = null)
|
TranscriptItem.QuestionCard(event.id, event.prompt, event.options, answer = null)
|
||||||
@@ -169,6 +179,9 @@ fun SessionScreen(
|
|||||||
// screen starts with the end of the conversation and fetches earlier
|
// screen starts with the end of the conversation and fetches earlier
|
||||||
// pages only when somebody scrolls to them.
|
// pages only when somebody scrolls to them.
|
||||||
var oldestSeq by remember { mutableLongStateOf(0L) }
|
var oldestSeq by remember { mutableLongStateOf(0L) }
|
||||||
|
// Every transcript event loaded, in order, beside the rows they folded
|
||||||
|
// into. See `apply`.
|
||||||
|
var loaded by remember { mutableStateOf(listOf<SessionEvent>()) }
|
||||||
var moreHistory by remember { mutableStateOf(true) }
|
var moreHistory by remember { mutableStateOf(true) }
|
||||||
var loadingHistory by remember { mutableStateOf(false) }
|
var loadingHistory by remember { mutableStateOf(false) }
|
||||||
var ready by remember { mutableStateOf(false) }
|
var ready by remember { mutableStateOf(false) }
|
||||||
@@ -179,7 +192,14 @@ fun SessionScreen(
|
|||||||
when (val event = entry.event) {
|
when (val event = entry.event) {
|
||||||
is SessionEvent.Status -> status = event.state
|
is SessionEvent.Status -> status = event.state
|
||||||
is SessionEvent.UsageDelta -> totalTokens += event.tokens
|
is SessionEvent.UsageDelta -> totalTokens += event.tokens
|
||||||
else -> items = foldEvent(items, event)
|
else -> {
|
||||||
|
// Kept as well as folded. Folding is one-way -- a tool's
|
||||||
|
// start and end become one row -- so a page arriving in
|
||||||
|
// front of what is already here cannot be stitched on
|
||||||
|
// without the events themselves.
|
||||||
|
loaded = loaded + event
|
||||||
|
items = foldEvent(items, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user