Skip to content

feat(chat): persist message and tool-call timestamps#149

Open
MesoX wants to merge 1 commit into
willdady:mainfrom
MesoX:feature/chat-timestamps-upstream
Open

feat(chat): persist message and tool-call timestamps#149
MesoX wants to merge 1 commit into
willdady:mainfrom
MesoX:feature/chat-timestamps-upstream

Conversation

@MesoX

@MesoX MesoX commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add timestamps to chat UI: per-tool-call (next to the BotIcon step separator) and per-message (next to action bar Copy/Delete/Edit/Retry buttons).
  • Persist via the existing JSONB chat.messages column — no schema migration. Tool start time stored in toolMetadata.startedAt; message creation time stored in metadata.createdAt.
  • User-message timestamp is stamped client-side at send (and on edit/resend) so the optimistic UI shows it immediately; backend keeps a server-side fallback on sink.onStart.
  • Assistant-message timestamp is captured once when streaming starts and reused via the AI SDK's messageMetadata callback (the callback fires for both start and finish chunks, so calling new Date() inside would store finish-time and mislabel it as createdAt).

Technical notes

  • AI SDK ai@6.0.191 quirk: the tool-output-available chunk handler ignores chunk.toolMetadata and reuses the invocation's existing toolMetadata from tool-input-available. The withToolTimestamps TransformStream therefore injects on tool-input-available.
  • finalize moved from toUIMessageStream.onFinish to the snapshot-loop finally block so lastMessages already contains toolMetadata when the sink writes to the DB.
  • Step separators render on AI SDK step-start parts (the SDK's explicit round boundary marker) rather than inferring from empty text parts.
  • stampLastUserMessageCreatedAt returns a new RunInput (non-mutating) so callers don't observe side effects on their request payload.
  • Historical messages without createdAt render no timestamp (intentional — no fake client-side fallback).

Tests

  • New unit tests for withToolTimestamps (3 cases: injects startedAt, preserves existing toolMetadata, passes other chunks through unchanged).
  • Helper accepts a now argument for deterministic tests.

Test plan

  • Run a chat with at least one tool call. Verify HH:mm:ss appears next to the small BotIcon above each tool-call card.
  • Verify timestamp shows in user-message action bar (right side) and assistant-message action bar (left side, before Copy/Delete).
  • Navigate away, return to chat. Timestamps persist for new messages and tool calls.
  • Old chats (pre-deploy) show no timestamps — correct, no DB backfill.
  • Edit a user message + resend. Resent message gets a fresh client-side timestamp.

🤖 Generated with Claude Code

@MesoX MesoX force-pushed the feature/chat-timestamps-upstream branch 2 times, most recently from b0e0252 to 528ebf5 Compare May 29, 2026 22:19
@MesoX

MesoX commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Before I proceed (hence why I left it marked as a draft) - @willdady wanted to check one thing with you. My ultimate goal at first was that some tool calls / subagents are running even for 10+ minutes and I wanted to see that in timestamps. But then I accidentally deleted that "robot" icon from tool calls and I must admit that it looks far better and saves so much vertical space in chats.

obrazek

What's your take on this? My two proposals are either:

  1. Remove that robot icon altogether from a tool call Which saves space, but I won't get precise timestamp visible in there (but can be part of the tool call itself - e.g. next to a status).
  2. Keep it there and add timestamp next to it.

But ultimately I like option number 1 much more as it deliberately saves alot of space.

Add timestamps to chat UI: per-tool-call (next to BotIcon step
separator) and per-message (next to action bar buttons).

Persisted in existing JSONB chat.messages column — no schema migration.
Tool start time stored in toolMetadata.startedAt; message creation time
stored in metadata.createdAt.

User-message timestamp is stamped client-side at send (and on
edit/resend) so the optimistic UI shows it immediately; backend keeps
a server-side fallback on sink.onStart. Assistant-message timestamp
comes from the AI SDK's messageMetadata callback.

AI SDK ai@6.0.191 quirk: the tool-output-available chunk handler
ignores chunk.toolMetadata and reuses the invocation's existing
toolMetadata from tool-input-available. The withToolTimestamps
TransformStream therefore injects on tool-input-available.

finalize moved from toUIMessageStream.onFinish to the snapshot-loop
finally block so lastMessages already contains toolMetadata when the
sink writes to the DB.

Historical messages without createdAt render no timestamp (intentional
— no fake client-side fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MesoX MesoX force-pushed the feature/chat-timestamps-upstream branch from 528ebf5 to a8e0e00 Compare May 30, 2026 05:49
@MesoX MesoX marked this pull request as ready for review May 31, 2026 21:05

@willdady willdady left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work here, @MesoX! Having looked this over, I'm not really sold on adding message timestamps in general — I'm not seeing enough value to justify the extra UI, and they're ambiguous as they stand: they show the time only, with no date, so once a conversation is more than a day old you can't actually tell when a message was sent.

The one piece I would like to see is on the tool header. Rather than showing a wall-clock timestamp there, it'd be much more useful to show the tool's run duration (e.g. "1.2s"). The current clock time next to a tool call doesn't tell you much, whereas how long the tool took to run is genuinely informative. That'd mean also persisting the tool's completion time (right now only startedAt is captured) so the duration can be computed.

So my suggestion: drop the message-level timestamps, and repurpose the tool header timestamp into a run duration instead.

@MesoX

MesoX commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@willdady thanks for the info, that's not a bad idea. I will reiterate through it a bit.

One more thing this solves for me is the latency until first token produced (e,g, you write a message and then you wait ~10 seconds before something even starts happening). With timestamps I can measure it to some degree, but the truth is I cann rather see it as maybe separate icon under the response from LLM where some standard info can be stored under some (i) icon - e.g. on mouse over. It could show not only the latency, but also input tokens, output tokens, number of tool calls and other information. But I will let it go through my head a bit and make a separate PR for it. But at the same time, this might only be relevant for local LLMs.

MesoX pushed a commit to MesoX/platypus that referenced this pull request Jun 2, 2026
Reworks PR willdady#149 per review: drop the message-level timestamps (low
value, ambiguous time-only display) and repurpose the tool header into a
run duration ("950ms" / "1.2s" / "1m 3s").

- withToolTimestamps still injects startedAt on tool-input-available,
  and now records completedAt per toolCallId. The AI SDK ignores
  toolMetadata on tool-output chunks, so the end time cannot ride the
  chunk; applyToolCompletions stamps it onto the built message before
  the sink persists it.
- Frontend computes the duration from the startedAt/completedAt pair via
  a shared formatToolDuration helper; in-progress and pre-existing tool
  calls render nothing.
- Removed client-side message createdAt stamping (chat.tsx,
  use-message-editing.ts) and the assistant messageMetadata createdAt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants