Skip to content

Commit 2863ede

Browse files
saphidclaude
andauthored
fix(server): deleting a thread also removes its agent preview recordings (#29853)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c609500 commit 2863ede

4 files changed

Lines changed: 44 additions & 3 deletions

File tree

‎apps/server/src/attachmentStore.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import * as NodeFS from "node:fs";
44
import * as NodePath from "node:path";
55

66
import type { ChatAttachment } from "@t3tools/contracts";
7-
import { htmlRenderFromToolItem, mcpAppFromToolItem } from "@t3tools/shared/toolOutput";
7+
import {
8+
htmlRenderFromToolItem,
9+
mcpAppFromToolItem,
10+
previewRecordingIdFromToolItem,
11+
} from "@t3tools/shared/toolOutput";
812

913
import {
1014
normalizeAttachmentRelativePath,
@@ -133,7 +137,9 @@ export function threadHtmlRenderAttachmentIds(
133137
if (segment === null) return [];
134138
return Array.from(items).flatMap((item) => {
135139
const attachmentId =
136-
htmlRenderFromToolItem(item)?.attachmentId ?? mcpAppFromToolItem(item)?.attachmentId;
140+
htmlRenderFromToolItem(item)?.attachmentId ??
141+
mcpAppFromToolItem(item)?.attachmentId ??
142+
previewRecordingIdFromToolItem(item);
137143
return attachmentId !== undefined &&
138144
parseThreadSegmentFromAttachmentId(attachmentId) === segment
139145
? [attachmentId]

‎apps/server/src/orchestration-v2/ProjectionStore.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5504,7 +5504,8 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
55045504
FROM orchestration_v2_projection_turn_items
55055505
WHERE thread_id = ${threadId}
55065506
AND type = 'dynamic_tool'
5507-
AND (payload_json LIKE '%htmlRender%' OR payload_json LIKE ${`%${MCP_APP_OUTPUT_KEY}%`})
5507+
AND (payload_json LIKE '%htmlRender%' OR payload_json LIKE ${`%${MCP_APP_OUTPUT_KEY}%`}
5508+
OR payload_json LIKE '%preview_recording_stop%')
55085509
`,
55095510
sql<{ id: string }>`
55105511
SELECT DISTINCT json_extract(attachment.value, '$.id') AS id

‎apps/server/src/orchestration-v2/TurnStartReads.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,11 +406,30 @@ it.effect.each(["sqlite"] as const)(
406406
},
407407
},
408408
});
409+
const recordingId = "thread-completion-reads-0b9f6c1e-3c1d-4f7a-9d2e-5a6b7c8d9e0f-webm";
410+
yield* store.apply({
411+
id: EventId.make("attachment:recording-event"),
412+
type: "turn-item.updated",
413+
threadId,
414+
runId: oldRunId,
415+
occurredAt: now,
416+
payload: {
417+
...item,
418+
id: TurnItemId.make("attachment:recording"),
419+
type: "dynamic_tool",
420+
toolName: "mcp__t3-code__preview_recording_stop",
421+
input: {},
422+
output: [
423+
{ type: "text", text: JSON.stringify({ id: recordingId, mimeType: "video/webm" }) },
424+
],
425+
},
426+
});
409427
assert.deepEqual((yield* store.getThreadAttachmentIds(threadId)).toSorted(), [
410428
"answer-document",
411429
"answer-image",
412430
"attachment:old",
413431
"message-file",
432+
recordingId,
414433
]);
415434
}).pipe(Effect.provide(layerDatabase)),
416435
);

‎packages/shared/src/toolOutput.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,21 @@ export function htmlRenderFromToolItem(item: {
191191
return output?.isError ? undefined : output?.htmlRender;
192192
}
193193

194+
/** The thread-owned file `preview_recording_stop` stored for the agent. */
195+
export function previewRecordingIdFromToolItem(item: {
196+
readonly toolName: string | null | undefined;
197+
readonly output?: unknown;
198+
}): string | undefined {
199+
if (resolveT3McpToolId(item.toolName) !== "preview_recording_stop") return undefined;
200+
const budget: ResultReadBudget = {
201+
remainingBytes: MAX_PARSED_BYTES,
202+
remainingNodes: MAX_ENVELOPE_NODES,
203+
exceeded: false,
204+
};
205+
const result = readResult(item.output, budget);
206+
return result.failed || budget.exceeded ? undefined : boundedId(result.data?.id);
207+
}
208+
194209
/** Accept app references only when server and tool match the item, so tools cannot impersonate another app source. */
195210
export function mcpAppFromToolItem(item: {
196211
readonly toolName: string | null | undefined;

0 commit comments

Comments
 (0)