Skip to content

spec: per-message timestamps in Agent Mode (#9889)#10128

Open
lonexreb wants to merge 3 commits intowarpdotdev:masterfrom
lonexreb:spec/9889-agent-message-timestamps
Open

spec: per-message timestamps in Agent Mode (#9889)#10128
lonexreb wants to merge 3 commits intowarpdotdev:masterfrom
lonexreb:spec/9889-agent-message-timestamps

Conversation

@lonexreb
Copy link
Copy Markdown
Contributor

@lonexreb lonexreb commented May 5, 2026

Summary

Spec for issue #9889 — Agent Mode shows the conversation as a sequence of prompts and responses with no temporal information. Users cannot tell when a prompt was submitted, how long the agent took, or how recent any given exchange is — particularly relevant for long-running tasks, comparing two attempts, or reviewing what an autonomous agent did overnight.

Investigation

The data already exists at the model layer:

  • AIAgentExchange carries start_ts: Option<DateTime<Local>> and completed_ts: Option<DateTime<Local>> (app/src/ai/agent/conversation.rs:3218-3219).
  • Conversation::start_time_from_exchange_messages derives the start time from the user input's AIAgentContext::CurrentTime (conversation.rs:556).
  • The corresponding finish-time helper at lines 538-553 derives completion from the latest message timestamp on the exchange.
  • Conversation restoration replays the underlying timestamps, so restored conversations get the same derivation for free.

Only the view layer omits this information.

What's in the spec

product.md — 8 testable behavior invariants (B1–B8), 8 acceptance criteria (A1–A8), explicit non-goals (per-token timestamps, server/client time reconciliation, CPU/credit accounting), and 5 risks with concrete TECH decisions to make.

tech.md — picks the data-derivation site (pure helper at the view layer, no new model state), the UI integration site (existing bubble metadata row, not a new gutter), the timer strategy (single shared 1Hz timer gated to visible in-progress exchanges), and lays out 5+6 unit tests + 8 integration tests.

Architectural choices

  • Pure derivation helper at the view layer, reusing the existing model-level helpers. No new state on the model, no view-layer caching of derived times.
  • Inline in the existing bubble metadata row (recommended over a new gutter) — leverages existing density and avoids a new layout slot.
  • Single shared 1Hz TimestampTickService for all in-progress duration counters, gated to "panel visible AND in-progress exchange present." A long conversation with 50 visible exchanges costs one timer, not 50.
  • Format auto-promotion via 30s tick: "just now" → "Xm ago" → "HH:MM" → "Day HH:MM" → "YYYY-MM-DD HH:MM". The 30s rule is applied internally to a 1Hz timer to avoid layout churn.
  • New setting agents.show_message_timestamps (default true, SyncToCloud::Always). When false the widgets are not added to the layout — zero off-state cost (A5 invariant).
  • Missing-timestamp fallback to em-dash with a once-per-exchange log::warn! so production data gaps are visible without flooding the log.
  • Cancellation handling: "cancelled at HH:MM • Xs" instead of "HH:MM • Xs."

Test plan

  • Data-derivation unit T1–T5 (exchange_times.rs)
  • Format unit T6–T11 (timestamp_widgets.rs)
  • Integration IT1–IT8 including a snapshot test for the off-state, a clock-advance test for format auto-promotion, and a yaml-restore test for historical timestamps
  • Manual: long-running prompt; verify "running for Xs" ticks once per second and freezes on completion
  • Manual: hover any timestamp; verify ISO 8601 tooltip
  • Manual: toggle setting off; verify pixel-equivalent layout to current build

Open questions for maintainer review

  1. Default: true (opt-out, recommended) or false (opt-in)?
  2. Inline-bubble (recommended) vs. gutter rendering. Defers to design — issue is needs-mocks.
  3. TimestampTickService lifetime: per-conversation (if conversations can render in multiple places simultaneously) or singleton (if not)?
  4. Tooltip format: ISO 8601 (unambiguous) or localized "Tuesday, May 4, 2026 at 3:47:23 PM CDT" (friendlier)?

Closes (spec-only) #9889 — implementation PR will follow once spec direction is confirmed.

Adds product.md + tech.md for issue warpdotdev#9889: Agent Mode shows the
conversation as a sequence of prompts and responses with no
temporal information. Users cannot tell when a prompt was
submitted, how long the agent took, or how recent any given
exchange is.

Investigation: the data already exists at the model layer.
AIAgentExchange carries derived start_ts and completed_ts
(app/src/ai/agent/conversation.rs:3218-3219), with helpers at
lines 538-567 deriving them from message timestamps and from
AIAgentContext::CurrentTime on the user input. Only the view layer
omits this information.

Spec proposes:
- Pure derivation helper at the agent_view layer (no new state on
  the model) reusing the existing model-level helpers.
- Three new widgets (TimestampLabel, DurationLabel,
  ProgressDurationLabel) added inline in the existing bubble
  metadata row, not in a new gutter — leverages existing density.
- Single shared 1Hz timer (TimestampTickService) for all
  in-progress duration counters, gated to "panel visible AND
  in-progress exchange present" so an idle long conversation
  costs nothing per second.
- Format auto-promotion ("just now" -> "Xm ago" -> "HH:MM" ->
  "Day HH:MM" -> "YYYY-MM-DD HH:MM") via a 30s tick.
- Hover tooltip with ISO 8601 disambiguator for users who need
  exact timing.
- New setting agents.show_message_timestamps (default true,
  SyncToCloud::Always); when false the widgets are not added to
  the layout (zero off-state cost).
- Missing-timestamp fallback to em-dash with a once-per-exchange
  warn log so production data gaps are spotted.
- Cancellation handling: "cancelled at HH:MM • Xs."

Test plan covers five data-derivation unit tests, six format
unit tests, and eight integration tests including a snapshot
test of the off-state and a clock-advance test for format
auto-promotion.

Four open questions for maintainer review on the default, inline
vs gutter rendering, tick service lifetime, and tooltip format.
@cla-bot cla-bot Bot added the cla-signed label May 5, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 5, 2026

@lonexreb

I'm starting a first review of this spec-only pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 5, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This spec defines product behavior and a technical plan for per-message timestamps in Agent Mode. The product goals are clear, but the tech spec currently relies on incorrect model references and targets the wrong UI integration point, which would lead implementation away from the actual conversation rendering path.

Concerns

  • The data-source section names fields and locations that do not match the checked-out codebase, so the implementation plan needs to be corrected before coding.
  • The UI integration section points at the Agent Message Bar even though conversation prompt/response rendering lives elsewhere.
  • Product formatting promises locale-preferred time display, while the tech spec hard-codes English 12-hour chrono::format strings.

Security

  • The new opt-out telemetry should explicitly document that no timestamp values, conversation IDs, or message content are emitted and that existing telemetry privacy controls apply.

Verdict

Found: 0 critical, 3 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9889/tech.md Outdated

## What already exists

- `AIAgentExchange` carries a `start_ts: Option<DateTime<Local>>`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] AIAgentExchange is defined in app/src/ai/agent/mod.rs with start_time and finish_time; the referenced conversation.rs:3218-3219 lines are SerializedBlock fields. Update the data-model references and derivation plan so implementers do not target nonexistent start_ts / completed_ts exchange fields.

Comment thread specs/GH9889/tech.md Outdated
Per product.md §risk 1 recommendation: **inline in the existing
message-bubble metadata row**.

For the user prompt bubble (in `agent_view/agent_message_bar.rs`):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] agent_view/agent_message_bar.rs renders the bottom status/input message bar, not the conversation prompt bubble. Point the spec at the actual prompt and response renderers (for example ai/blocklist/block/view_impl/query.rs and the corresponding response block renderer) before implementation.

Comment thread specs/GH9889/tech.md Outdated
match delta {
d if d < ChronoDuration::minutes(1) => "just now".into(),
d if d < ChronoDuration::hours(1) => format!("{}m ago", d.num_minutes()),
d if d < ChronoDuration::days(1) => ts.format("%-I:%M %p").to_string(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] This hard-codes English 12-hour formatting, but product B2 requires the user's locale-preferred format and explicitly allows 24-hour output. Either make B2 fixed-format or specify a locale-aware formatter and tests for 24-hour locales.

Comment thread specs/GH9889/product.md
with maintainers before implementation; this is the most
reversible decision.

5. **Telemetry.** Add a one-time `setting_changed` event when the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] [SECURITY] Define the setting_changed payload and privacy guardrails here: it should not include timestamp values, conversation IDs, message content, or bypass existing telemetry opt-out controls.

Bot review (warpdotdev#10128) identified:
- The data-source section named fields and locations that do not
  exist in the codebase (start_ts/completed_ts at conversation.rs
  lines 3218-3219 — those are unrelated SerializedBlock fields).
- The UI integration section pointed at agent_message_bar.rs which
  renders the bottom input bar, not the per-exchange prompt/response
  bubbles.
- The hard-coded English 12-hour format string contradicted product
  B2's locale-preferred-format requirement.
- Missing privacy guardrails for the new opt-out telemetry.

Fixes:

- Correct facts on AIAgentExchange (defined in app/src/ai/agent/mod.rs:2835
  with start_time: DateTime<Local> and finish_time: Option<DateTime<Local>>).
- Cite the right helpers: Conversation::start_time_from_exchange_messages
  at conversation.rs:556, finish_time_from_exchange_messages at
  conversation.rs:536, and recompute call sites at 1776/1894/1961.
- Point UI integration at the correct render sites:
  app/src/ai/blocklist/block/view_impl/query.rs::render_query for the
  prompt bubble, view_impl/output.rs for the response bubble. Move
  all new files under view_impl/ alongside the consumers; explicitly
  call out that agent_message_bar.rs is NOT modified.
- Replace English 12-hour format with a ClockFormat struct sourced
  from the OS at startup (NSLocale on mac, LOCALE_ITIME on Windows,
  LC_TIME/LANG on Linux). Use chrono::format_localized for weekday
  names. T8/T9 are duplicated as T8b/T9b for non-English/24-hour
  locales.
- Drop submitted_at fallback (start_time is always present). Keep the
  finish_time inconsistency-state warn.
- Add explicit telemetry privacy guardrails to product.md §Risk 5:
  payload shape, no IDs/timestamp values/message content, respect
  global telemetry opt-out, no new server-side channels.

Each correction includes a "Correction (review warpdotdev#10128)" callout so
reviewers can cross-reference the bot's concerns.
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 5, 2026

Pushed corrections addressing all 4 bot review concerns:

  • Codebase facts: corrected AIAgentExchange location to app/src/ai/agent/mod.rs:2835 with the actual fields start_time: DateTime<Local> and finish_time: Option<DateTime<Local>> (not the start_ts/completed_ts I'd written). Updated all helper references to the right line numbers.
  • UI integration site: moved from agent_view/agent_message_bar.rs (which renders the bottom input bar) to the actual prompt/response renderers — ai/blocklist/block/view_impl/query.rs::render_query for the user prompt and view_impl/output.rs for the agent response. New helper module relocated to view_impl/ alongside the consumers.
  • Locale-aware formatting: replaced the hard-coded English 12-hour format with a ClockFormat struct sourced from the OS at startup (NSLocale on mac, LOCALE_ITIME on Windows, LC_TIME/LANG on Linux). Tests T8/T9 are duplicated as T8b/T9b for non-English/24h locales.
  • Telemetry privacy: added explicit guardrails to product.md §Risk 5 — payload shape, no IDs/timestamps/message content, respects global telemetry opt-out, no new server-side channels.

Each correction includes a "Correction (review #10128)" callout in the spec so future reviewers can cross-reference. Ready for re-review.

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 5, 2026

@lonexreb

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This spec defines user-facing behavior and a technical approach for per-message timestamps in Agent Mode. The product scope is generally aligned with the linked issue, and the security/privacy surface is limited by explicit telemetry guardrails.

Concerns

  • The technical spec incorrectly states that both prompt and response render paths already receive an exchange or exchange id; the prompt path needs explicit data plumbing before it can bind a submitted-at timestamp.
  • The proposed locale-aware formatter depends on chrono::Locale / format_localized, but the workspace chrono dependency does not enable the required locale feature.
  • The missing-timestamp fallback is inconsistent with the current model paths, which synthesize Local::now() for missing finish timestamps and would bypass the dash fallback required by the product spec.
  • No additional security findings.

Verdict

Found: 0 critical, 3 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9889/tech.md Outdated
bound to `submitted_at` plus the shared 1Hz tick instead.

Both bubbles already receive an `AIAgentExchange` (or its id) from
the parent block, so neither integration requires plumbing new data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] query::Props / render_query currently receive only query text and input_index; specify the required plumbing from AIBlock/AIBlockModel::exchange_id so the prompt timestamp can actually bind to an exchange.

Comment thread specs/GH9889/tech.md Outdated
/// Locale tag (e.g. "en-US", "de-DE") used by chrono's locale-aware
/// formatters for weekday names. Sourced from the OS at startup; not
/// re-read on every tick.
locale: chrono::Locale,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] chrono::Locale and format_localized require chrono's locale feature, but the workspace dependency only enables serde; specify the dependency feature change or choose a formatter that compiles with current features.

Comment thread specs/GH9889/tech.md Outdated
only fires for `completed_at`, which is `Option<DateTime<Local>>`:

- If the exchange is not in progress (`output_status` reports
finished/errored/cancelled) AND `finish_time` is `None`, the model
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Finished/error/cancelled paths currently set finish_time to Local::now() when message timestamps are missing, so this finish_time is None fallback would not satisfy B7 and could display a fabricated time; specify changing that model fallback or deriving from raw messages before display.

Comment thread specs/GH9889/tech.md
- Per-token / per-tool-call timestamps.
- Cumulative agent CPU time / credits per exchange (overlaps
#10000, #10052).
- Time-zone selection and 24h vs 12h preference.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] This conflicts with the formatter section that explicitly implements and tests 24h-vs-12h preferences; keep time-zone selection out of scope but remove 24h-vs-12h preference from follow-ups.

Round-2 bot review identified 3 important concerns the previous
corrections introduced:

- The prompt path doesn't actually receive an AIAgentExchange — I
  claimed it did. render_query in view_impl/query.rs takes
  primitive parameters via Props (query: &str, user_display_name,
  attachments, etc.) and the timestamp must be plumbed through.
- The proposed locale-aware formatter depended on chrono::Locale
  and format_localized, but the workspace chrono dependency
  (Cargo.toml: chrono = { version = "0.4.38", features = ["serde"]
  }) does not enable the unstable-locales feature these APIs need.
- The missing-timestamp dash fallback can never fire — the model's
  recompute call sites at conversation.rs:1777/1895/1962 do
  finish_time_from_exchange_messages(...).unwrap_or_else(Local::now),
  so finished exchanges always have finish_time: Some(...).

Fixes:

- UI integration: explicit plumbing plan for the prompt path —
  add submitted_at: DateTime<Local> to view_impl/query.rs::Props
  and the props builder. The parent block has AIAgentExchangeId
  context and reads exchange.start_time from the model. One field
  on each of two structs, no new layer.
- Format auto-promotion: drop chrono::Locale / format_localized.
  V1 keeps English weekday names ("Mon", "Tue") which chrono's
  default %a formatter provides without unstable-locales. The
  12h-vs-24h preference is read from the OS — that's the more
  impactful localization knob and works without the feature flag.
  Localized weekday names called out as an explicit V2 follow-up
  with two implementation options.
- Missing-timestamp fallback: the dash glyph is now scoped to two
  defensive-guard cases (finished exchange with finish_time: None
  due to a hypothetical state-machine bug; or a future schema
  change that makes start_time optional). Both fire warn logs at
  most once per exchange. The happy path uses
  ProgressDurationLabel for in-progress and TimestampLabel for
  finished — no dash in normal operation.
- Tests: T8b/T9b updated to test 24h format with English weekdays
  (instead of German weekdays) since V1 doesn't localize day
  names.

Each correction includes a "Correction (re-review warpdotdev#10128)" callout.
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 5, 2026

Round-2 corrections pushed (commit 523d7f2):

  • Prompt path data plumbing: I was wrong — render_query in view_impl/query.rs takes primitive params via Props (query: &str, user_display_name, etc.), not an AIAgentExchange. Spec now plans an explicit submitted_at: DateTime<Local> field on Props plus the call-site builder. One field on each of two structs; no new layer.
  • Locale formatter: dropped chrono::Locale / format_localized (the workspace chrono = { version = "0.4.38", features = ["serde"] } doesn't enable unstable-locales). V1 keeps English weekday names from default %a and reads only the 12h-vs-24h preference from the OS — works without the feature flag. Localized weekdays called out as an explicit V2 follow-up with two implementation options.
  • Missing-timestamp fallback: the model's recompute call sites at conversation.rs:1777/1895/1962 do unwrap_or_else(Local::now), so finished exchanges always have Some(finish_time). The dash glyph is now scoped to defensive guards (state-machine-bug only) with one-time warn logs. The happy path uses ProgressDurationLabel for in-progress and TimestampLabel for finished — no dash in normal operation.

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 5, 2026

@lonexreb

I'm re-reviewing this spec-only pull request in response to a review request.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This spec defines product and technical behavior for showing per-message timestamps in Agent Mode, including formatting, setting gates, restoration behavior, timer strategy, and tests.

Concerns

  • The product and tech specs conflict on missing timestamp behavior: product acceptance requires an em dash, while the tech design renders synthesized Local::now() values for restored conversations with lost timestamps.
  • The proposed setting uses a non-existent SyncToCloud::Always mode, leaving implementation and cloud-sync privacy semantics unspecified.
  • The tech spec simultaneously implements OS-derived 12h/24h formatting and lists 12h vs 24h preference as out of scope.

Verdict

Found: 1 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9889/tech.md
recompute paths above, it is:
- `None` exactly while the exchange is in progress.
- `Some(...)` once the exchange transitions out of in-progress —
including `Some(Local::now())` synthesized for restored
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 [CRITICAL] This contradicts product.md B7/A7 and IT7: rendering Some(Local::now()) for lost stored timestamps shows a fabricated load-time completion instead of . Define a path that detects the synthesized fallback, or change the product acceptance/tests to match the tech behavior.

Comment thread specs/GH9889/tech.md
show_message_timestamps: bool,
AISettings,
SupportedPlatforms::DESKTOP,
SyncToCloud::Always,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] SyncToCloud::Always is not a valid settings sync mode in this repo; existing settings use Globally(RespectUserSyncSetting::Yes/No), PerPlatform(...), or Never. Pick the exact mode so the implementation and privacy semantics are unambiguous.

Comment thread specs/GH9889/tech.md
- Per-token / per-tool-call timestamps.
- Cumulative agent CPU time / credits per exchange (overlaps
#10000, #10052).
- Time-zone selection and 24h vs 12h preference.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] This conflicts with the formatter design above, which makes OS-derived 12h-vs-24h behavior part of V1. Either keep clock-format preference in scope and remove it from follow-ups, or make the V1 formatter fixed-format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant