feat(chat): record and show the assistant response duration #337

Merged
rwachters merged 7 commits from feat/assistant-message-response-duration into master 2026-10-01 23:20:06 +00:00
rwachters commented 2026-10-01 23:19:15 +00:00 (Migrated from github.com)

Summary

Records how long the LLM provider call that produced an assistant message actually took, stores it on the message, exposes it through the shared model and shows it as a Response duration row in the message details dialog, so a slow model can be told apart from a slow turn.

Changes

  • Added the additive V34 migration with a nullable assistant_messages.response_duration_ms column, declared on AssistantMessageTable and mapped back through toAssistantMessage.
  • Added ChatMessage.AssistantMessage.responseDurationMs: Long? = null as the shared wire-model field.
  • Threaded the value through MessageDao and ConversationTurnPersistence, written together with the row or the finalizing content update and cleared by an edit that describes a different generation.
  • Measured the non-streaming provider call and the streaming provider call, including interrupted and failed attempts, with the streaming measurement frozen at the stream's terminal signal rather than when the stream object closes.
  • Copied the value into cloned sessions so a clone reproduces the generation it came from.
  • Added a Response duration row to MessageUsageDialog, formatted with the tool-call duration formatter and omitted entirely when nothing was measured, with EN/ES strings.

Implementation notes

  • The measured window is one provider call: from request dispatch to the response's terminal signal, including streamed reasoning and the tool-call requests the model emitted, and excluding every tool execution. It is deliberately not the client-side turn timer, which covers tool execution too.
  • null means "nothing was measured": a row predating the column, a hand-inserted or hand-edited message, and an unmeasured generation all read as absent, never as zero. No backfill is performed.
  • Elapsed time is taken from the wall clock and clamped at zero, since a backwards clock adjustment must not persist a negative duration.
  • Existing mock-based suites received mechanical matcher updates, because MockK matches an omitted trailing argument against the Kotlin default and the production code now passes a real measurement.

Validation

  • ./gradlew :server:testClasses :server:test — 2189 tests, 0 failures (1 pre-existing skip).
  • ./gradlew :app:desktopTest — 976 tests, 0 failures.
  • ./gradlew :app:desktopMainClasses and ./gradlew :app:compileKotlinWasmJs / :app:wasmJsMainClasses.
  • ./gradlew :common:desktopTest.

Risks and follow-up

  • Four low-priority review findings (stronger capture-based assertions for the measured duration and the hostile-input behaviour of the nullable write contract) are deferred to a follow-up, not fixed here.
  • The new dialog row is covered by a desktop Compose UI test only; other targets are covered by compilation.
## Summary Records how long the LLM provider call that produced an assistant message actually took, stores it on the message, exposes it through the shared model and shows it as a Response duration row in the message details dialog, so a slow model can be told apart from a slow turn. ## Changes - Added the additive `V34` migration with a nullable `assistant_messages.response_duration_ms` column, declared on `AssistantMessageTable` and mapped back through `toAssistantMessage`. - Added `ChatMessage.AssistantMessage.responseDurationMs: Long? = null` as the shared wire-model field. - Threaded the value through `MessageDao` and `ConversationTurnPersistence`, written together with the row or the finalizing content update and cleared by an edit that describes a different generation. - Measured the non-streaming provider call and the streaming provider call, including interrupted and failed attempts, with the streaming measurement frozen at the stream's terminal signal rather than when the stream object closes. - Copied the value into cloned sessions so a clone reproduces the generation it came from. - Added a Response duration row to `MessageUsageDialog`, formatted with the tool-call duration formatter and omitted entirely when nothing was measured, with EN/ES strings. ## Implementation notes - The measured window is one provider call: from request dispatch to the response's terminal signal, including streamed reasoning and the tool-call requests the model emitted, and excluding every tool execution. It is deliberately not the client-side turn timer, which covers tool execution too. - `null` means "nothing was measured": a row predating the column, a hand-inserted or hand-edited message, and an unmeasured generation all read as absent, never as zero. No backfill is performed. - Elapsed time is taken from the wall clock and clamped at zero, since a backwards clock adjustment must not persist a negative duration. - Existing mock-based suites received mechanical matcher updates, because MockK matches an omitted trailing argument against the Kotlin default and the production code now passes a real measurement. ## Validation - `./gradlew :server:testClasses :server:test` — 2189 tests, 0 failures (1 pre-existing skip). - `./gradlew :app:desktopTest` — 976 tests, 0 failures. - `./gradlew :app:desktopMainClasses` and `./gradlew :app:compileKotlinWasmJs` / `:app:wasmJsMainClasses`. - `./gradlew :common:desktopTest`. ## Risks and follow-up - Four low-priority review findings (stronger capture-based assertions for the measured duration and the hostile-input behaviour of the nullable write contract) are deferred to a follow-up, not fixed here. - The new dialog row is covered by a desktop Compose UI test only; other targets are covered by compilation.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Torvian.eu/chatbot!337
No description provided.