Show a compaction happening, and what it recovered

The Compacting status had been declared, rendered in four places, and
never once emitted: no driver produced it, and the app had no control to
ask for a compaction in the first place. Pressing nothing for two
minutes and then quietly having less context was the whole experience.

The CLI turns out to announce all of it, which was worth measuring
rather than guessing at. Driven through /compact against 2.1.237 it
emits a `status: "compacting"` line at the start, a `status: null`
carrying `compact_result` at the end -- `"failed"` with a sentence
saying why, when it does -- and then a `compact_boundary` with the token
counts. The same records appear in the CLI's own transcript file with
camelCase keys, which is the obvious place to read the shape off and
gets every field name wrong.

So none of it is inferred here. The driver writes the line and says
nothing; the translator reports what the CLI reports. A failed
compaction surfaces the CLI's own sentence, which is specific enough to
act on.

The counts are the part worth keeping afterwards, so they land in the
transcript rather than only in a status that vanishes: a session that
went from 128,402 tokens to 9,617 has just been given its context back.
They are optional throughout, because a compaction whose size nobody
reported has to be able to say so -- a zero would read as "recovered
nothing".

Also here, all found on the way:

- `rename_all` renames variants; fields need `rename_all_fields`. Every
  field in Event was a single word until `pre_tokens`, which went out as
  snake_case, was not found by the app, and rendered as the "no counts
  reported" case -- a state it is allowed to be in, so nothing looked
  wrong. There is now a test on the wire names.
- The unparseable-line warning sliced bytes, not chars, on output that
  is full of em dashes. A panic there kills the task reading the
  session's stdout, and the session goes deaf with nothing on screen.
  The other three truncations in the tree already did this correctly.
- Echo compacts too, with invented numbers and a real shape, so this
  screen can be looked at without spending two minutes of somebody's
  account to reach the state.
This commit is contained in:
iris committed 2026-08-29 14:42:08 -04:00
1 parent 42131c75d6
commit 5396da76c7
8 files changed
+408 -38

No files matched your search

@@ -110,6 +110,19 @@ sealed class TranscriptItem {
/** Placeholder row for events this build can't render (newer kinds). */
data class Note(val text: String) : TranscriptItem()
/**
* A compaction that happened, and what it recovered.
*
* In the transcript rather than only in the status line, because the status is gone the moment
* it finishes and this is the part worth keeping: it is the explanation for a gap in the
* conversation, and for a minute or two in which the session was busy with nothing to show.
*/
data class CompactedNote(
val preTokens: Long?,
val postTokens: Long?,
val trigger: String?,
) : TranscriptItem()
}
fun foldEvent(items: List<TranscriptItem>, event: SessionEvent): List<TranscriptItem> =
@@ -181,6 +194,8 @@ fun foldEvent(items: List<TranscriptItem>, event: SessionEvent): List<Transcript
} else {
items + TranscriptItem.ImageItem(event.ref)
}
is SessionEvent.Compacted ->
items + TranscriptItem.CompactedNote(event.preTokens, event.postTokens, event.trigger)
is SessionEvent.Unknown -> items + TranscriptItem.Note("[${event.type}]")
// Screen-level state, not transcript rows -- see SessionScreen.
is SessionEvent.UsageDelta -> items
@@ -526,6 +541,15 @@ fun SessionScreen(
// the paid service's own numbers, so a session on a provider with no such service
// gets an honest "unavailable" rather than a hidden button -- a control that comes
// and goes makes its absence the signal, and absence cannot say why.
// Beside the token count it acts on, which is the line to its left: compacting is
// what that number is for. Disabled rather than hidden while one is already running,
// so the button still says the session can do this and why it cannot right now.
TextButton(
onClick = { act { compactSession(settings, summary.id) } },
enabled = status != "compacting" && status != "exited",
) {
Text("Compact")
}
TextButton(onClick = onUsage) { Text("Usage") }
}
@@ -677,6 +701,7 @@ fun SessionScreen(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
is TranscriptItem.CompactedNote -> CompactedRow(item)
}
}
}