From e741883b83e0ebef57d3d4807b5c8c15051568eb Mon Sep 17 00:00:00 2001 From: Luis Gustavo Couto Wacker Date: Wed, 19 Aug 2026 19:26:35 -0300 Subject: [PATCH 01/10] fix(web): usage hourly breakdown lists every hour chronologically (#7595) (cherry picked from commit 62654d279fcdd910bb755e106dfd51bc8940c263) --- apps/web/src/components/usage/UsagePage.tsx | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index 269b3bb3a..cd6dd86a8 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -68,12 +68,15 @@ export function UsagePage() { : enumerateHourStarts(window.sinceTime, window.untilTime), [window.sinceTime, window.untilTime], ); - // Newest first: the window can run 90 periods, so the interesting end - // belongs at the top of the table. - const breakdownPeriods = useMemo( - () => (isPast24Hours ? merged.hourly : merged.daily).toReversed(), - [isPast24Hours, merged.daily, merged.hourly], - ); + // The hourly window is small enough to render every period: the table then + // reads chronologically like the chart, instead of jumping between the hours + // that happened to have activity. Daily windows can run 90 periods, so those + // stay newest-first with the interesting end on top. + const breakdownPeriods = useMemo(() => { + if (!isPast24Hours) return merged.daily.toReversed(); + const byHour = new Map(merged.hourly.map((entry) => [entry.hourStart, entry])); + return hours.map((hourStart) => byHour.get(hourStart) ?? zeroHour(hourStart)); + }, [isPast24Hours, merged.daily, merged.hourly, hours]); const selectWindow = (days: number) => { setWindowSelection({ @@ -441,6 +444,17 @@ export function UsagePage() { ); } +/** A zero-filled hourly period so the breakdown lists every hour in the window. */ +function zeroHour(hourStart: string): HourlyTotals { + return { + day: "", + hourStart, + costUsd: 0, + totalTokens: 0, + byProvider: new Map(), + }; +} + /** Brand mark for the harness a row belongs to. */ function ProviderMark({ provider, From e574bd1cb98da11c6a862379e39ae733b04c34a5 Mon Sep 17 00:00:00 2001 From: Jake Leventhal Date: Wed, 19 Aug 2026 18:26:45 -0400 Subject: [PATCH 02/10] fix(web): remove the terminal pane's app-canvas gutter (#6222) Co-authored-by: Claude Opus 5 (1M context) (cherry picked from commit 105cd5e0c57ab8b6df3bd8af5b534a5bc682fdce) --- apps/web/src/components/ThreadTerminalDrawer.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index d9fb8973b..ec2e63d41 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -978,7 +978,7 @@ export function TerminalViewport({ return (
); } @@ -1483,7 +1483,12 @@ export default function ThreadTerminalDrawer({ )}
-
+
{isSplitView ? (
-
+
) : ( -
+
Date: Wed, 19 Aug 2026 22:09:49 -0400 Subject: [PATCH 03/10] chore(desktop): use stable Clerk Electron release (#7602) (cherry picked from commit 9027d62679c6669d8e45e2e419b311ec8dfdaa4f) --- pnpm-lock.yaml | 64 ++++++++++++++++++++++----------------------- pnpm-workspace.yaml | 11 +++----- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34499402e..bc5b5c937 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ overrides: '@clerk/clerk-js>@solana/wallet-adapter-react': '-' '@clerk/clerk-js>@solana/wallet-standard': '-' '@clerk/clerk-js>@wallet-standard/core': '-' - '@clerk/electron': 0.0.34-canary.v20260819050620 - '@clerk/electron-passkeys': 0.0.4-canary.v20260819050620 + '@clerk/electron': 0.0.34 + '@clerk/electron-passkeys': 0.0.3 '@clerk/expo': 4.2.0 '@clerk/react': 6.14.4 '@clerk/shared': 4.29.2 @@ -118,11 +118,11 @@ importers: apps/desktop: dependencies: '@clerk/electron': - specifier: 0.0.34-canary.v20260819050620 - version: 0.0.34-canary.v20260819050620(@clerk/electron-passkeys@0.0.4-canary.v20260819050620)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.34 + version: 0.0.34(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron-passkeys': - specifier: 0.0.4-canary.v20260819050620 - version: 0.0.4-canary.v20260819050620 + specifier: 0.0.3 + version: 0.0.3 '@effect/platform-node': specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(ioredis@5.11.0)(utf-8-validate@6.0.6) @@ -524,8 +524,8 @@ importers: specifier: ^1.4.1 version: 1.5.0(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron': - specifier: 0.0.34-canary.v20260819050620 - version: 0.0.34-canary.v20260819050620(@clerk/electron-passkeys@0.0.4-canary.v20260819050620)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.34 + version: 0.0.34(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': specifier: 6.14.4 version: 6.14.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -1748,35 +1748,35 @@ packages: resolution: {integrity: sha512-4i4RE+ZQ0hKDDFSRKCISQPBy07SfFeFAhBUhNj2j01Nx2n4pqV+5iyg2Vf27+NKFNkgq7kdmZGvtug24D++8WQ==} engines: {node: '>=20.9.0'} - '@clerk/electron-passkeys-darwin-arm64@0.0.4-canary.v20260819050620': - resolution: {integrity: sha512-avxygtEfWBD3YPkxws4lB5mL/75SQvVBMQCg+b1gjFsQInnVOgAqAv4wHR8wgUsEbXY2zEkvqaYjFSnEveC0VA==} + '@clerk/electron-passkeys-darwin-arm64@0.0.3': + resolution: {integrity: sha512-oAlrd+GLqP0oREP3hNRc6yNZhXGaQgJss9iCWRQqXjpw1yZD1ZU+L+E2Gfg84vMj/NSUhxbU0Djy0RXhmeUwgg==} cpu: [arm64] os: [darwin] - '@clerk/electron-passkeys-darwin-x64@0.0.4-canary.v20260819050620': - resolution: {integrity: sha512-clShldbwDcC2ek6gJT85QKsMfaPyOhEgfz61u2n3YKivbeui33XjJu261d+LO20mqDjXZy3flLGPTIh/Kii80g==} + '@clerk/electron-passkeys-darwin-x64@0.0.3': + resolution: {integrity: sha512-0UPAWQEni7o8gjWwd2sP97fPshJmT2w5b8eV/jOPVSP8NqIcFCm5yIFSEx1SfB53LGfzB01A4iEGi2pxiCHqyQ==} cpu: [x64] os: [darwin] - '@clerk/electron-passkeys-win32-arm64-msvc@0.0.4-canary.v20260819050620': - resolution: {integrity: sha512-alIfXJSxyeulnNeHoT4X7+qO0o9/EBhiRWMhCXwKJIbWfwzqnnVrRH1P7HfYRnDepKhqRzsUaBuYTqQzisHG+A==} + '@clerk/electron-passkeys-win32-arm64-msvc@0.0.3': + resolution: {integrity: sha512-bbRvifm9A/gfPkwC1UyMyF9sYGEs0aen7J2kCcaOIW3K0PXcAbHtsR/gnx4dT0bjFdd21vSa2zlIdERvHc1BiA==} cpu: [arm64] os: [win32] - '@clerk/electron-passkeys-win32-x64-msvc@0.0.4-canary.v20260819050620': - resolution: {integrity: sha512-QSKKMCdV9AhOt74ls8ExdwR+2ikoLGgN2LdWIjBgNk6epJVs2L1FXVZOM0ZqfVHFSl5y7vLmlOACR6WyJ2eoLg==} + '@clerk/electron-passkeys-win32-x64-msvc@0.0.3': + resolution: {integrity: sha512-gFaVqKlOKTF2SJ3lMwkZ0oonycQn93p1qSrp5kzzeZBeQ+tGB3gQnjXecrhQy21AzDUyWqVJJ8KrP2HRAwifWg==} cpu: [x64] os: [win32] - '@clerk/electron-passkeys@0.0.4-canary.v20260819050620': - resolution: {integrity: sha512-zOoFEsvOrArv7p4ERNAdkGbYoD86ragDhysLG1NdxJm2jnfw+AfwxUv0FkOPZ7iNuQ0rbJaR/urxdvEwVcuKRA==} + '@clerk/electron-passkeys@0.0.3': + resolution: {integrity: sha512-OHhIe88qDL+FxyBalXdXNHAS5eEramr6Rerp+6iNkfkjqT8rx4hHNmfpmjg5/T1/am8QfknbOBZkqoXZlCrjPg==} engines: {node: '>=20.9.0'} - '@clerk/electron@0.0.34-canary.v20260819050620': - resolution: {integrity: sha512-sfKWkH2SigKcxtR+h36G1NGnmMI5xOlwvqHpr3JYcacS/uokQbeAjCCQzEeV/9zqWYtRL4et5wCD13+gWv4b6w==} + '@clerk/electron@0.0.34': + resolution: {integrity: sha512-ZY6v8G1ArieIWvutMEt5HBJtpHN4ys4F4newNlOTElv//cpW29zaeZ65M8WuKttKr7fB+MH6wzzCoX/Ha+fiSg==} engines: {node: '>=20.9.0'} peerDependencies: - '@clerk/electron-passkeys': 0.0.4-canary.v20260819050620 + '@clerk/electron-passkeys': 0.0.3 electron: '>=28' electron-store: ^8.2.0 react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 @@ -11869,26 +11869,26 @@ snapshots: - react - react-dom - '@clerk/electron-passkeys-darwin-arm64@0.0.4-canary.v20260819050620': + '@clerk/electron-passkeys-darwin-arm64@0.0.3': optional: true - '@clerk/electron-passkeys-darwin-x64@0.0.4-canary.v20260819050620': + '@clerk/electron-passkeys-darwin-x64@0.0.3': optional: true - '@clerk/electron-passkeys-win32-arm64-msvc@0.0.4-canary.v20260819050620': + '@clerk/electron-passkeys-win32-arm64-msvc@0.0.3': optional: true - '@clerk/electron-passkeys-win32-x64-msvc@0.0.4-canary.v20260819050620': + '@clerk/electron-passkeys-win32-x64-msvc@0.0.3': optional: true - '@clerk/electron-passkeys@0.0.4-canary.v20260819050620': + '@clerk/electron-passkeys@0.0.3': optionalDependencies: - '@clerk/electron-passkeys-darwin-arm64': 0.0.4-canary.v20260819050620 - '@clerk/electron-passkeys-darwin-x64': 0.0.4-canary.v20260819050620 - '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.4-canary.v20260819050620 - '@clerk/electron-passkeys-win32-x64-msvc': 0.0.4-canary.v20260819050620 + '@clerk/electron-passkeys-darwin-arm64': 0.0.3 + '@clerk/electron-passkeys-darwin-x64': 0.0.3 + '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.3 + '@clerk/electron-passkeys-win32-x64-msvc': 0.0.3 - '@clerk/electron@0.0.34-canary.v20260819050620(@clerk/electron-passkeys@0.0.4-canary.v20260819050620)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/electron@0.0.34(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@clerk/clerk-js': 6.29.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': 6.14.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -11897,7 +11897,7 @@ snapshots: react: 19.2.6 tslib: 2.8.1 optionalDependencies: - '@clerk/electron-passkeys': 0.0.4-canary.v20260819050620 + '@clerk/electron-passkeys': 0.0.3 electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2b243c8c4..c1a88e9a1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,8 +25,8 @@ allowBuilds: catalog: "@clerk/backend": 3.14.0 "@clerk/clerk-js": 6.29.2 - "@clerk/electron": 0.0.34-canary.v20260819050620 - "@clerk/electron-passkeys": 0.0.4-canary.v20260819050620 + "@clerk/electron": 0.0.34 + "@clerk/electron-passkeys": 0.0.3 "@clerk/expo": 4.2.0 "@clerk/react": 6.14.4 "@clerk/shared": 4.29.2 @@ -56,8 +56,7 @@ catalog: minimumReleaseAgeExclude: - "@clerk/backend@3.14.0" - "@clerk/clerk-js@6.29.2" - - "@clerk/electron@0.0.34-canary.v20260819050620" - - "@clerk/electron-passkeys@0.0.4-canary.v20260819050620" + - "@clerk/electron@0.0.34" - "@clerk/expo@4.2.0" - "@clerk/react@6.14.4" - "@clerk/shared@4.29.2" @@ -79,10 +78,6 @@ minimumReleaseAgeExclude: - alchemy@2.0.0-beta.70 - effect@4.0.0-beta.103 - "@legendapp/list@3.3.5" - - "@clerk/electron-passkeys-darwin-arm64@0.0.4-canary.v20260819050620" - - "@clerk/electron-passkeys-darwin-x64@0.0.4-canary.v20260819050620" - - "@clerk/electron-passkeys-win32-arm64-msvc@0.0.4-canary.v20260819050620" - - "@clerk/electron-passkeys-win32-x64-msvc@0.0.4-canary.v20260819050620" overrides: "@clerk/backend": "catalog:" From ba6397284d3169eed7994d1b0c2e7d2a8733403b Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:07:32 -0400 Subject: [PATCH 04/10] test(web): remove redundant timestamp assertions (#7633) Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> (cherry picked from commit f708f63fa9bcd7e51f1f62531f6f9ed966b71807) --- apps/web/src/timestampFormat.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/timestampFormat.test.ts b/apps/web/src/timestampFormat.test.ts index f35c1c1fd..7cc61e02c 100644 --- a/apps/web/src/timestampFormat.test.ts +++ b/apps/web/src/timestampFormat.test.ts @@ -197,12 +197,10 @@ describe("formatDayAwareTimestamp", () => { describe("invalid timestamp inputs", () => { it("returns an empty timestamp instead of throwing", () => { - expect(() => formatTimestamp("not-a-date", "12-hour")).not.toThrow(); expect(formatTimestamp("not-a-date", "12-hour")).toBe(""); }); it("returns an empty short timestamp instead of throwing", () => { - expect(() => formatShortTimestamp("not-a-date", "12-hour")).not.toThrow(); expect(formatShortTimestamp("not-a-date", "12-hour")).toBe(""); }); From 0e677133871d2d3e088c115f4804067c42810a7f Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:11:20 -0400 Subject: [PATCH 05/10] fix(web): import dependency-heavy Open VSX themes (#7642) Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com> (cherry picked from commit beab6886f45bf42906d0bd01aefe5dfe9e66a867) --- apps/web/src/openVsxThemes.test.ts | 5 +++++ apps/web/src/openVsxThemes.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/src/openVsxThemes.test.ts b/apps/web/src/openVsxThemes.test.ts index 4775a0188..b4794f619 100644 --- a/apps/web/src/openVsxThemes.test.ts +++ b/apps/web/src/openVsxThemes.test.ts @@ -293,6 +293,11 @@ describe("Open VSX themes", () => { it("downloads a verified VSIX, reads JSONC includes, and pairs contributed variants", async () => { const zip = new JSZip(); zip.file("extension/.gitkeep", ""); + // Theme extensions sometimes publish their development dependencies too. + // Those unused files should not prevent importing the small theme payload. + for (let index = 0; index < 3_000; index += 1) { + zip.file(`extension/node_modules/package-${index}.js`, ""); + } zip.file( "extension/themes/base.jsonc", `{ diff --git a/apps/web/src/openVsxThemes.ts b/apps/web/src/openVsxThemes.ts index 90d0d62ce..8b03d8486 100644 --- a/apps/web/src/openVsxThemes.ts +++ b/apps/web/src/openVsxThemes.ts @@ -17,8 +17,8 @@ const MAX_DETAIL_BYTES = 256 * 1024; const MAX_MANIFEST_BYTES = 256 * 1024; const SEARCH_REQUEST_TIMEOUT_MS = 10_000; const MAX_THEME_BYTES = 256 * 1024; -const MAX_ZIP_ENTRIES = 2_000; -const MAX_UNCOMPRESSED_BYTES = 50 * 1024 * 1024; +const MAX_ZIP_ENTRIES = 5_000; +const MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024; const MAX_COMPRESSION_RATIO = 200; const MAX_THEMES_PER_EXTENSION = 40; const MAX_INCLUDE_DEPTH = 8; From 9eb6bf908b50e904a204c173fc89d1a03c247db8 Mon Sep 17 00:00:00 2001 From: maria Date: Wed, 19 Aug 2026 21:28:40 -0400 Subject: [PATCH 06/10] fix(server): preserve tool lifecycle identity (#7151) (cherry picked from commit b2e2ccfdb462c45a8de394ff9ff0f58a4a689900) --- .../ActivityPayloadProjection.test.ts | 41 ++++++++++++++++++- .../ActivityPayloadProjection.ts | 36 ++++++++++++---- .../Layers/ProviderRuntimeIngestion.test.ts | 31 ++++++++++---- .../Layers/ProviderRuntimeIngestion.ts | 3 ++ 4 files changed, 93 insertions(+), 18 deletions(-) diff --git a/apps/server/src/orchestration/ActivityPayloadProjection.test.ts b/apps/server/src/orchestration/ActivityPayloadProjection.test.ts index 229c7f8ab..2cdfef19f 100644 --- a/apps/server/src/orchestration/ActivityPayloadProjection.test.ts +++ b/apps/server/src/orchestration/ActivityPayloadProjection.test.ts @@ -20,7 +20,7 @@ function activity(payload: Record): OrchestrationThreadActivity * If slimming ever moves to an allowlist over the whole payload, these * assertions are the tripwire. */ -describe("projectActivityPayload agent-field survival", () => { +describe("projectActivityPayload", () => { it("preserves tool attribution (agentId/parentToolUseId) through data slimming", () => { const projected = projectActivityPayload( activity({ @@ -97,6 +97,45 @@ describe("projectActivityPayload agent-field survival", () => { expect(JSON.stringify(acp.payload).length).toBeLessThan(500); }); + it("normalizes Claude and OpenCode command inputs before slimming provider data", () => { + const claude = projectActivityPayload( + activity({ + itemType: "command_execution", + toolCallId: "claude-call-1", + data: { + toolName: "Bash", + input: { command: "vp test run" }, + result: { content: "x".repeat(5_000) }, + }, + }), + ); + const openCode = projectActivityPayload( + activity({ + itemType: "command_execution", + toolCallId: "opencode-call-1", + data: { + tool: "bash", + state: { + status: "running", + input: { command: "vp lint" }, + output: "x".repeat(5_000), + }, + }, + }), + ); + + expect(claude.payload).toMatchObject({ + toolCallId: "claude-call-1", + data: { command: "vp test run" }, + }); + expect(openCode.payload).toMatchObject({ + toolCallId: "opencode-call-1", + data: { command: "vp lint" }, + }); + expect(JSON.stringify(claude.payload).length).toBeLessThan(200); + expect(JSON.stringify(openCode.payload).length).toBeLessThan(200); + }); + it("slims Codex-shaped mcp_tool_call items to rendered fields plus a result summary", () => { const projected = projectActivityPayload( activity({ diff --git a/apps/server/src/orchestration/ActivityPayloadProjection.ts b/apps/server/src/orchestration/ActivityPayloadProjection.ts index e6333a1ca..103b267d2 100644 --- a/apps/server/src/orchestration/ActivityPayloadProjection.ts +++ b/apps/server/src/orchestration/ActivityPayloadProjection.ts @@ -125,6 +125,24 @@ function projectCommandData(data: Record): Record 0 ? projectedItem : undefined; } +function projectCommandValue(data: Record): unknown { + if (data.command !== undefined) { + return data.command; + } + + const input = asRecord(data.input); + if (input?.command !== undefined) { + return input.command; + } + + const stateInput = asRecord(asRecord(data.state)?.input); + if (stateInput?.command !== undefined) { + return stateInput.command; + } + + return undefined; +} + function summarizeToolTextOutput(value: string): string | null { const lines: string[] = []; for (const rawLine of value.split(/\r?\n/u)) { @@ -339,8 +357,9 @@ export function projectActivityPayload( if (item) { projectedData.item = item; } - if ("command" in data) { - projectedData.command = data.command; + const command = projectCommandValue(data); + if (command !== undefined) { + projectedData.command = command; } const changedFiles: string[] = []; @@ -418,12 +437,10 @@ function dropStaleContextWindowActivities( } /** - * Identity both clients use to fold a tool lifecycle row into the call it - * belongs to (`deriveToolLifecycleCollapseKey` in web's `session-logic` and - * mobile's `threadActivity`): an explicit `data.toolCallId` when the adapter - * emits one, otherwise the itemType/title/detail triple. Returns null for rows - * with no identity at all — those never collapse on the client either, so they - * must not be dropped here. + * Identity used to retain only the newest lifecycle row for each call in a + * thread snapshot. Prefer the runtime item id, then the legacy nested id, and + * finally the itemType/title/detail triple. Rows without any identity remain + * untouched. */ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string | null { const payload = asRecord(activity.payload); @@ -431,7 +448,8 @@ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string | return null; } - const toolCallId = asTrimmedString(asRecord(payload.data)?.toolCallId); + const toolCallId = + asTrimmedString(payload.toolCallId) ?? asTrimmedString(asRecord(payload.data)?.toolCallId); if (toolCallId) { return `id:${toolCallId}`; } diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 9fbcc21fc..4687acc4b 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -3312,11 +3312,16 @@ describe("ProviderRuntimeIngestion", () => { createdAt: now, threadId: asThreadId("thread-1"), turnId: asTurnId("turn-9"), + itemId: asItemId("tool-call-9"), payload: { itemType: "command_execution", - status: "in_progress", - title: "Read file", - detail: "/tmp/file.ts", + status: "inProgress", + title: "Command run", + detail: "Bash: vp test run", + data: { + toolName: "Bash", + input: { command: "vp test run" }, + }, }, }); @@ -3331,11 +3336,20 @@ describe("ProviderRuntimeIngestion", () => { ); expect(thread.session?.status).toBe("ready"); - expect( - thread.activities.some( - (activity: ProviderRuntimeTestActivity) => activity.kind === "tool.started", - ), - ).toBe(true); + const activity = thread.activities.find( + (entry: ProviderRuntimeTestActivity) => entry.kind === "tool.started", + ); + const payload = activity?.payload as Record | undefined; + expect(payload).toMatchObject({ + itemType: "command_execution", + toolCallId: "tool-call-9", + status: "inProgress", + detail: "Bash: vp test run", + data: { + toolName: "Bash", + input: { command: "vp test run" }, + }, + }); }); it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => { @@ -3453,6 +3467,7 @@ describe("ProviderRuntimeIngestion", () => { expect(toolUpdate?.kind).toBe("tool.updated"); expect(toolUpdatePayload?.itemType).toBe("command_execution"); expect(toolUpdatePayload?.status).toBe("in_progress"); + expect(toolUpdatePayload?.toolCallId).toBe("item-p1-tool"); const warning = thread.activities.find( (activity: ProviderRuntimeTestActivity) => activity.id === "evt-runtime-warning", diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 050a2c9a6..7a770eb15 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -1256,6 +1256,7 @@ export function runtimeEventToActivities( summary: toolLifecycleActivityTitle(event, "Tool updated"), payload: { itemType: event.payload.itemType, + ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), ...(event.payload.status ? { status: event.payload.status } : {}), ...(primeAgentTool || !event.payload.detail ? {} @@ -1395,6 +1396,7 @@ export function runtimeEventToActivities( summary: toolLifecycleActivityTitle(event, "Tool"), payload: { itemType: event.payload.itemType, + ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), ...(primeAgentTool ? toolLifecycleActivityStatus(event) : {}), ...(primeAgentTool || !event.payload.detail ? {} @@ -1455,6 +1457,7 @@ export function runtimeEventToActivities( summary: `${toolLifecycleActivityTitle(event, "Tool")} started`, payload: { itemType: event.payload.itemType, + ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), ...(primeAgentTool ? toolLifecycleActivityStatus(event) : {}), ...(primeAgentTool || !event.payload.detail ? {} From 0d70d9aecafe91f0e7cf5b7eb25129f9c3d8c159 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 20 Aug 2026 11:22:07 -0600 Subject: [PATCH 07/10] fix(server): keep Prime tool ids out of projected activities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopting #7151 as a plain union added upstream's `toolCallId: event.itemId` unconditionally, next to Pylon's `primeAgentTool` gating rather than behind it. A Prime tool's itemId is a canonical filesystem path, which is exactly the native detail that gating exists to withhold — so the projection began carrying `canonical-prime-tool-/private/native-secret`. Pylon's own test asserts that string must not appear, and caught it. `toolCallId` now sits behind the same gate. Two further gaps in the same resolution are closed while here: upstream's unconditional `status` is kept for non-Prime tools alongside Pylon's derived Prime status, and `tool.started` regains the `data` spread upstream added. 115 tests across the four changed files pass, including both that failed. --- .../Layers/ProviderRuntimeIngestion.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 7a770eb15..2702fe6db 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -1256,7 +1256,9 @@ export function runtimeEventToActivities( summary: toolLifecycleActivityTitle(event, "Tool updated"), payload: { itemType: event.payload.itemType, - ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), + // A Prime tool's itemId is a canonical path, so it stays behind the + // same gate as the rest of the native detail. + ...(primeAgentTool || event.itemId === undefined ? {} : { toolCallId: event.itemId }), ...(event.payload.status ? { status: event.payload.status } : {}), ...(primeAgentTool || !event.payload.detail ? {} @@ -1396,8 +1398,14 @@ export function runtimeEventToActivities( summary: toolLifecycleActivityTitle(event, "Tool"), payload: { itemType: event.payload.itemType, - ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), - ...(primeAgentTool ? toolLifecycleActivityStatus(event) : {}), + // A Prime tool's itemId is a canonical path, so it stays behind the + // same gate as the rest of the native detail. + ...(primeAgentTool || event.itemId === undefined ? {} : { toolCallId: event.itemId }), + ...(primeAgentTool + ? toolLifecycleActivityStatus(event) + : event.payload.status + ? { status: event.payload.status } + : {}), ...(primeAgentTool || !event.payload.detail ? {} : { detail: truncateDetail(event.payload.detail) }), @@ -1457,11 +1465,20 @@ export function runtimeEventToActivities( summary: `${toolLifecycleActivityTitle(event, "Tool")} started`, payload: { itemType: event.payload.itemType, - ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}), - ...(primeAgentTool ? toolLifecycleActivityStatus(event) : {}), + // A Prime tool's itemId is a canonical path, so it stays behind the + // same gate as the rest of the native detail. + ...(primeAgentTool || event.itemId === undefined ? {} : { toolCallId: event.itemId }), + ...(primeAgentTool + ? toolLifecycleActivityStatus(event) + : event.payload.status + ? { status: event.payload.status } + : {}), ...(primeAgentTool || !event.payload.detail ? {} : { detail: truncateDetail(event.payload.detail) }), + ...(primeAgentTool || event.payload.data === undefined + ? {} + : { data: event.payload.data }), ...(primeAgentTool || !event.payload.agentId ? {} : { agentId: event.payload.agentId }), ...(primeAgentTool || !event.payload.parentToolUseId ? {} From 2cf95d5445eaaa54dd092f7cfdcce498f2d55694 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 20 Aug 2026 11:37:08 -0600 Subject: [PATCH 08/10] fix(server): simplify the tool status spread and cover the widened dedupe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from an xhigh review of this batch. The Prime/non-Prime status ternary I added while gating `toolCallId` had two behaviorally identical arms: `toolLifecycleActivityStatus` already returns `{}` when `payload.status` is undefined, and `RuntimeItemStatus` is a non-empty union, so the falsy check could never differ. Both sites collapse to the helper. `#7151` also widened the snapshot dedupe: `toolLifecycleIdentity` now prefers a payload-level `toolCallId`, and that commit sets one on every tool activity, so identity is `id:` where it used to fall back to itemType/title/detail. No test exercised the new branch — the fixture only ever set `data.toolCallId`. Added one that pins it, including the consequence: updates whose detail differs from the completion's now collapse into it, which the clients do not mirror because they read only `data.toolCallId`. --- .../Layers/ProviderRuntimeIngestion.ts | 12 ++----- .../test/ActivityPayloadProjection.test.ts | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 2702fe6db..8f0d4484f 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -1401,11 +1401,7 @@ export function runtimeEventToActivities( // A Prime tool's itemId is a canonical path, so it stays behind the // same gate as the rest of the native detail. ...(primeAgentTool || event.itemId === undefined ? {} : { toolCallId: event.itemId }), - ...(primeAgentTool - ? toolLifecycleActivityStatus(event) - : event.payload.status - ? { status: event.payload.status } - : {}), + ...toolLifecycleActivityStatus(event), ...(primeAgentTool || !event.payload.detail ? {} : { detail: truncateDetail(event.payload.detail) }), @@ -1468,11 +1464,7 @@ export function runtimeEventToActivities( // A Prime tool's itemId is a canonical path, so it stays behind the // same gate as the rest of the native detail. ...(primeAgentTool || event.itemId === undefined ? {} : { toolCallId: event.itemId }), - ...(primeAgentTool - ? toolLifecycleActivityStatus(event) - : event.payload.status - ? { status: event.payload.status } - : {}), + ...toolLifecycleActivityStatus(event), ...(primeAgentTool || !event.payload.detail ? {} : { detail: truncateDetail(event.payload.detail) }), diff --git a/apps/server/test/ActivityPayloadProjection.test.ts b/apps/server/test/ActivityPayloadProjection.test.ts index 49f1b532a..6d8f5e98b 100644 --- a/apps/server/test/ActivityPayloadProjection.test.ts +++ b/apps/server/test/ActivityPayloadProjection.test.ts @@ -266,9 +266,16 @@ describe("superseded tool.updated snapshot dedup", () => { readonly title?: string; readonly detail?: string; readonly toolCallId?: string; + readonly payloadToolCallId?: string; } = {}, ): OrchestrationThreadActivity { - const { turn = "turn-a", title = "File change", detail, toolCallId } = options; + const { + turn = "turn-a", + title = "File change", + detail, + toolCallId, + payloadToolCallId, + } = options; return { id: EventId.make(id), tone: "tool", @@ -277,6 +284,7 @@ describe("superseded tool.updated snapshot dedup", () => { payload: { itemType: "file_change", title, + ...(payloadToolCallId ? { toolCallId: payloadToolCallId } : {}), ...(detail ? { detail } : {}), data: { ...(toolCallId ? { toolCallId } : {}), @@ -317,6 +325,30 @@ describe("superseded tool.updated snapshot dedup", () => { expect(projectedIds([otherCall, update, completed])).toEqual([otherCall.id, completed.id]); }); + it("matches on a payload-level toolCallId, even when details differ", () => { + // `preserve tool lifecycle identity` (#7151) sets `payload.toolCallId` on + // every tool activity, and the identity prefers it over the + // itemType/title/detail triple. That is deliberately broader than the + // fallback: updates whose detail differs from the completion's — an + // accumulating command output, say — now collapse into it. The clients read + // only `data.toolCallId`, so they keep those rows live and the snapshot + // does not. Change this expectation only on purpose. + const first = makeToolLifecycleActivity("upd-1", "tool.updated", { + payloadToolCallId: "call-x", + detail: "writing 1/3", + }); + const second = makeToolLifecycleActivity("upd-2", "tool.updated", { + payloadToolCallId: "call-x", + detail: "writing 2/3", + }); + const completed = makeToolLifecycleActivity("done-x", "tool.completed", { + payloadToolCallId: "call-x", + detail: "wrote 3 files", + }); + + expect(projectedIds([first, second, completed])).toEqual([completed.id]); + }); + it("keeps updates with no matching completion", () => { const inFlight = makeToolLifecycleActivity("upd-live", "tool.updated", { title: "Running" }); const other = makeToolLifecycleActivity("upd-other", "tool.updated", { title: "Reading" }); From a226d3d542ca4fbbd5bb808d24e2bf1b57c91c3e Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 20 Aug 2026 11:38:23 -0600 Subject: [PATCH 09/10] docs(upstream): record the 2026-08-20 T3 Code batch --- .agents/upstream-review.md | 82 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index 43af7212e..dc78d0fc9 100644 --- a/.agents/upstream-review.md +++ b/.agents/upstream-review.md @@ -1,8 +1,8 @@ --- remote: t3code-upstream branch: main -reviewed-through: "a850895f6833b99d90fc6c50192b5eaa4966d5c7" -reviewed-through-date: "2026-08-19" +reviewed-through: "beab6886f45bf42906d0bd01aefe5dfe9e66a867" +reviewed-through-date: "2026-08-20" --- # T3 upstream review log @@ -1871,6 +1871,79 @@ unioned; the `legacy-plan-mode` settings-search id is registered against F6 and H1 stay in their original batch tables as historical record — they describe what was decided then. This entry is what supersedes them. +## 2026-08-20 — `a850895f6833b99d90fc6c50192b5eaa4966d5c7..beab6886f45bf42906d0bd01aefe5dfe9e66a867` + +Eight upstream commits, four change sets. **Six adopted** onto +`upstream/2026-08-20-batch`; **two deferred** (see DEF-5). `git cherry` reported every +one absent from Pylon. The register was empty going in and gains one entry. + +**`#7602` closes the canary follow-up opened by the second 2026-08-19 batch.** It moves +`@clerk/electron` to stable `0.0.34`, `@clerk/electron-passkeys` back to `0.0.3`, and +deletes all four platform-specific native canary entries from `minimumReleaseAgeExclude`. +Verified against the condition the ledger recorded: `npm view @clerk/electron dist-tags` +now reports `latest: 0.0.34`. Pylon no longer ships prerelease native binaries, and the +"only darwin-arm64 was ever exercised" risk retires with them. + +**`#7150` and `#7152` are deferred, not skipped.** `#7150` rewrites 1084 lines of +`ChatComposer.tsx` — a file with 50 Pylon commits, structurally divergent at 4837 lines +against upstream's 2824. A full pass was made: all 11 conflict blocks were classified and +individually resolved, keeping Pylon's settle loop, `resolvedRuntimeMode`, the 169 lines +of Quick question and session-resource controls, and the ThreadHandoffTab wrapper, while +taking upstream's drawer measurement and its 335-line drawer structure. The result did +not compile — 8 JSX errors — because upstream flattens the fragment and nested +provider-frame divs into one `
` and its added tree cannot be hosted by Pylon's +layers. The two are ends of one restructure. `#7152` then conflicts on +`MessagesTimeline` because it genuinely depends on `#7150`. + +**One conflict resolution leaked a secret and Pylon's own test caught it.** Adopting +`#7151` as a plain union put upstream's unconditional `toolCallId: event.itemId` beside +Pylon's `primeAgentTool` gating rather than behind it. A Prime tool's itemId is a +canonical filesystem path, so the projection began carrying +`canonical-prime-tool-/private/native-secret` — exactly what the gating exists to +withhold. Fixed in `0d70d9aec`; the guarding assertion lives in +`ProviderRuntimeIngestion.test.ts`. + +Conflicts and adaptations, all resolved Pylon-first: + +| File | Conflict | Resolution | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` | `#7151` adds `toolCallId`, `status`, and `data` spreads to three tool payloads; Pylon gates the same payloads on `primeAgentTool` | `toolCallId` placed behind Pylon's gate; upstream's `status` kept for non-Prime alongside Pylon's derived Prime status; `tool.started` regained upstream's `data`. A later review collapsed the status ternary — both arms were identical. | + +Review and validation: + +- 4 changed test files, **115 tests, 0 failures**; the projection suite is 20 after the + test added below. Typecheck clean across 7 packages, each confirmed to have run. +- Validated live in the web client against a seeded copy of real data: pairing, thread + view, the Usage page including the "Hourly cost by provider" chart, and Settings → + Appearance with its theme list and Import theme. No error surfaces anywhere. + **Not verified:** `#7595`'s chronological hour ordering — the chart exposes no hour + labels to the DOM, so ordering could not be asserted from the browser. +- An xhigh review produced 13 findings. Two were fixed in `2cf95d544`. The rest are + upstream design decisions adopted as-is; the ones worth acting on later are below. + +Open questions raised by this batch: + +- **The snapshot dedupe widened.** `toolLifecycleIdentity` now prefers a payload-level + `toolCallId`, and `#7151` sets one on every tool activity, so identity is `id:` + where it used to fall back to itemType/title/detail. The clients read only + `data.toolCallId` (`session-logic.ts` `extractToolCallId`, mobile's + `deriveToolLifecycleCollapseKey`), so they cannot mirror it: in-flight rows with + differing details render live and collapse after a reload. A test now pins the + behavior; whether it is the behavior Pylon wants is undecided. The doc comment on + `dropSupersededToolUpdatedActivities` still asserts an invariant measured under the old + identity. +- **`toolLifecycleIdentity`'s fallback joins with no separator** (`[itemType, label, +detail].join("")`), where both clients join on a unit-separator character. Distinct + rows can collide. Latent and pre-existing, but the function was touched here. +- **`tool.started` now persists the provider's full unprojected `data`.** The + `item.updated` branch wraps in `projectActivityPayload`; `tool.started` does not, so a + large Write stores the whole body — and both clients skip `tool.started` rows entirely. +- **`#7642` doubles `MAX_UNCOMPRESSED_BYTES` to 100 MB** and raises `MAX_ZIP_ENTRIES`, a + relaxation of an anti-zip-bomb guard, to accommodate extensions shipping `node_modules` + the importer never reads. Bounding only the theme payload would fix the class. +- **`#7595` made the Past-24h empty state unreachable**: `hours` is always 24 entries, so + an idle day renders 24 `$0.00` rows instead of "No activity in this window." + ## Deferred register _The register is currently empty. DEF-1 and DEF-2 were adopted on 2026-08-11 @@ -1888,5 +1961,6 @@ Every review must read this register before reporting new candidates, re-evaluate each `Revisit when` against the current upstream head, and report the outcome. See Phase 2.5 of the `review-t3-upstream` skill. -| ID | Upstream | Deferred on | Revisit when | Why deferred | -| --- | -------- | ----------- | ------------ | ------------ | +| ID | Upstream | Deferred on | Revisit when | Why deferred | +| ----- | ----------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DEF-5 | `#7150` `792a1404f` + `#7152` `4a9edff4c` | 2026-08-20 | Whenever the composer is next worked on deliberately. This is scheduled work, not a bet on upstream changing, so there is no waiting condition to poll. Before starting, check `git log --oneline 792a1404f..t3code-upstream/main -- apps/web/src/components/chat/ChatComposer.tsx` for follow-ups that should land with it. | `#7150` rewrites 1084 lines of `ChatComposer.tsx` against 50 Pylon commits and a 4837-vs-2824 line divergence. A full block-by-block resolution compiled to 8 JSX errors: upstream flattens the fragment and provider-frame divs into one ``, so its 335-line drawer tree cannot be hosted by Pylon's layers. Needs the composer rebuilt on upstream's structure with Pylon's ThreadHandoffTab, provider frame classes, settle loop, `resolvedRuntimeMode`, and Quick question / session-resource controls grafted back. `#7152` depends on it. | From 4a5f99a10a6613ef7728b012ad5c4e8afa1e2568 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 20 Aug 2026 11:53:03 -0600 Subject: [PATCH 10/10] docs(upstream): withdraw the identity-separator finding --- .agents/upstream-review.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index dc78d0fc9..49e84cd18 100644 --- a/.agents/upstream-review.md +++ b/.agents/upstream-review.md @@ -1932,9 +1932,11 @@ Open questions raised by this batch: behavior; whether it is the behavior Pylon wants is undecided. The doc comment on `dropSupersededToolUpdatedActivities` still asserts an invariant measured under the old identity. -- **`toolLifecycleIdentity`'s fallback joins with no separator** (`[itemType, label, -detail].join("")`), where both clients join on a unit-separator character. Distinct - rows can collide. Latent and pre-existing, but the function was touched here. +- ~~`toolLifecycleIdentity`'s fallback joins with no separator.~~ **Withdrawn — false + positive.** The separator is a unit-separator character, which is invisible in source + output, so the call reads as `join("")` in a terminal and in review tooling; `cat -v` + shows the real `join("^_")`. Pylon and upstream are byte-identical here. Recorded so + the same illusion does not get re-reported. - **`tool.started` now persists the provider's full unprojected `data`.** The `item.updated` branch wraps in `projectActivityPayload`; `tool.started` does not, so a large Write stores the whole body — and both clients skip `tool.started` rows entirely.