fix(server): preserve tool lifecycle identity#7151

merged1
main
ui-refresh/tool-activity
openedAug 16, 2026byMmaria
View on GitHubTwo-way sync enabledPR last imported 3d ago

What changed

  • Carries provider item IDs into top-level toolCallId lifecycle identity for started, updated, and completed tool events.
  • Preserves start status and provider command data so later completion events can be folded into the correct activity.
  • Normalizes direct, Claude, and OpenCode command shapes during activity projection.

Screenshots

Not applicable. This is a server-only lifecycle projection change; its visible web consumer is demonstrated in #7152.

Why

This is the sensitive server layer that was hard to approve inside the original UI PR. It is intentionally above every independent UI layer. The stack can merge through #7150 without taking this change.

Review notes

  • Started-event provider data is newly retained in persisted activity payloads.
  • No contracts change is required.
  • Mobile still ignores tool.started; the new web behavior is implemented in #7152.

Validation

  • Activity payload projection tests
  • Provider runtime ingestion tests
  • Affected-package typechecks
  • Changed-file lint and formatting checks

Stack order

  1. #7153 workspace and navigation
  2. #7147 Usage
  3. #7148 Pull Requests
  4. #7149 terminal
  5. #7150 composer
  6. #7151 tool lifecycle projection, this PR
  7. #7152 tool activity UI

Built with GPT-5.6-sol in the Codex harness.


Note

Medium Risk Changes orchestration activity identity and persisted tool.started payloads; deduplication keys shift for in-flight sessions at deploy time, though behavior is covered by projection and ingestion tests.

Overview Propagates provider item IDs into persisted tool activities so started, updated, and completed lifecycle rows share a stable top-level toolCallId. toolLifecycleIdentity now prefers that field when deduplicating to the newest row per call.

Enriches tool.started payloads to include provider status and data (aligned with update/complete), so clients can fold in-flight tool state into the same activity.

Normalizes command strings during activity projection via projectCommandValue, pulling commands from direct data.command, Claude-style data.input.command, or OpenCode-style data.state.input.command before slimming provider blobs.

Reviewed by Cursor Bugbot for commit 102ece2dee9ba6c17c412337f53da5ed7ec5f1a9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix toolLifecycleIdentity to prefer top-level toolCallId

  • toolLifecycleIdentity in ActivityPayloadProjection.ts now checks payload.toolCallId before falling back to nested data.toolCallId; projectActivityPayload uses a new projectCommandValue helper to normalize command fields from Claude and OpenCode payload shapes. Tool activity payloads from runtimeEventToActivities in ProviderRuntimeIngestion.ts now include toolCallId, status, and data when present.
  • Composer UI is redesigned across many components in apps/web/src/components/chat/ to use attached "drawer" styling with glass surfaces, shoulder tabs, and a new Button size "micro". New ComposerTasksBadge and ComposerTasksDrawer show per-turn task progress; ComposerStashBadge and ComposerStashMenu gain inline placement and outside-click dismissal.
  • addPlanStepDurations in session-logic.ts computes durationMs on completed plan steps by scanning plan.update transitions; deriveActivePlanState and deriveTurnPlans now return plans with per-step durations.
  • resolveProviderSkillSourceKind in providerSkills.ts replaces formatProviderSkillInstallSource, returning a normalized union ('app'|'repo'|'project'|'personal'|'system'|'other'); callers in web and mobile use it for skill icons.
  • Risk: toolLifecycleIdentity resolution change may alter which activity row is retained for a given tool call in snapshots โ€” verify dedup logic in ActivityPayloadProjection.ts handles top-level toolCallId collisions. Composer CSS in index.css introduces new glass-surface rules and removes background: transparent from code, which may affect existing code element rendering.

Macroscope summarized 9df356b.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

โš™๏ธ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8587be53-2816-412c-9a70-415dd5266913

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ๐Ÿ” Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands.

Synced from GitHub

Thread transfer impact

โœ… Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire12.6 KiB13.4 KiB+815 B (+6.3%)15.1 KiBโœ…
CodexThread snapshot wire6.3 KiB6.9 KiB+622 B (+9.7%)7.3 KiBโœ…
CodexLive turn WebSocket wire6.3 KiB6.5 KiB+193 B (+3.0%)7.8 KiBโœ…
CodexLive turn WebSocket decoded51.8 KiB55.0 KiB+3.2 KiB (+6.1%)66.4 KiBโœ…
CodexLive turn messages16160 (0.0%)21โœ…
ClaudeTotal thread wire12.6 KiB13.5 KiB+853 B (+6.6%)15.1 KiBโœ…
ClaudeThread snapshot wire6.3 KiB6.9 KiB+627 B (+9.7%)7.3 KiBโœ…
ClaudeLive turn WebSocket wire6.3 KiB6.6 KiB+226 B (+3.5%)7.8 KiBโœ…
ClaudeLive turn WebSocket decoded52.7 KiB55.8 KiB+3.2 KiB (+6.0%)66.4 KiBโœ…
ClaudeLive turn messages16160 (0.0%)21โœ…

Baseline: 792a140 ยท PR result: 102ece2 ยท Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Approved a21dd5a

This is a self-contained bug fix that improves how tool lifecycle identity is extracted and preserved in the orchestration layer. Changes are well-tested and limited to internal activity projection logic with no external API or schema impact.

No code changes detected at 102ece2. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

โ€ฆโ€ฆ@@ -803,6 +803,7 @@ export function runtimeEventToActivities(
803803 summary: event.payload.title ?? "Tool updated",
804804 payload: {
805805 itemType: event.payload.itemType,
806+ ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}),
MmacroscopeappBoton line 806Aug 16, 2026

๐ŸŸก Medium Layers/ProviderRuntimeIngestion.ts:806

event.itemId is stored as top-level payload.toolCallId, but clients only read payload.data.toolCallId, so tool.updated and tool.completed rows whose identity exists only in event.itemId still fall back to title/detail and can fail to fold when those fields change. Store the ID inside data as well, or update the client extractors in the same change.

โœ… Resolved in 8e450c7309922efdbfeaf7f84b795afea292cc70

โ€ฆโ€ฆ@@ -211,6 +211,17 @@ function truncateDetail(value: string, limit = 180): string {
216+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
217+ return { toolCallId, ...data };
218+ }
219+ return {
MmacroscopeappBoton line 219Aug 16, 2026

๐ŸŸก Medium Layers/ProviderRuntimeIngestion.ts:219

For completed or started lifecycle events whose data is a string, array, or other non-object value, withToolCallId replaces payload.data with { toolCallId, value }, so consumers no longer receive the original value at payload.data. Preserve the original top-level data shape and expose toolCallId separately, rather than wrapping opaque Schema.Unknown payloads.

No longer relevant as of ce306d218e5fd1676ad2bd84979a2d88a6b1dd07

Added 1 commit

  • fix(server): preserve tool lifecycle identity102ece2
MmacroscopeappBotreviewedAug 20, 2026
Synced from GitHub

One consistency finding on the new composer tasks tab/drawer: the "Dismiss tasks for this turn" control is hand-rolled as a raw <button> in the tab and drawer placements while the inline placement in the same component uses Button size="icon-micro" variant="ghost-muted". See the inline comment.

Posted via Macroscope โ€” UI Consistency

apps/web/src/components/chat/ComposerTasksBadge.tsxlines 151โ€“159Outdated ยท 9df356bOpen thread
โ€ฆโ€ฆ@@ -148,16 +148,15 @@ export const ComposerTasksBadge = memo(function ComposerTasksBadge({
158157 >
159158 <XIcon aria-hidden className="size-3" />
160- </Button>
159+ </button>
MmacroscopeappBoton lines 151โ€“159Aug 20, 2026

The same "Dismiss tasks for this turn" action is rendered with Button size="icon-micro" variant="ghost-muted" in the inline placement above (lines 102-111), but hand-rolled as a raw <button> here and again in ComposerTasksDrawer (lines 191-199). The raw version drops the primitive's focus-visible ring tokens (focus-visible:ring-2 focus-visible:ring-ring ring-offset-*), the coarse-pointer 44px hit target (pointer-coarse:after:min-h-11/min-w-11), and uses rounded-md/hover:bg-muted/60 instead of the shared rounded-sm/hover:bg-accent treatment, so the identical control behaves differently depending on placement. Consider using the primitive in both places (it is already imported):

suggestion
1 <Button
2 size="icon-micro"
3 variant="ghost-muted"
4 aria-label="Dismiss tasks for this turn"
5 className="shrink-0"
6 onClick={onDismiss}
7 onPointerDown={(event) => event.preventDefault()}
8 >
9 <XIcon aria-hidden className="size-3" />
10 </Button>

Posted via Macroscope โ€” UI Consistency

This thread targets ComposerTasksBadge.tsx from the old stacked diff. PR #7151 now contains only the server lifecycle projection files, so the UI consistency suggestion belongs to the already-merged composer PR and is no longer actionable here.