fix: preserve image attachments in live and restored conversations
This commit is contained in:
@@ -462,6 +462,7 @@ export function createCodingComposition(
|
||||
),
|
||||
isAuthenticationError: isCodingProviderAuthenticationError,
|
||||
refreshCredential: refreshCodingProviderCredential,
|
||||
projectImage: async ({ data, mime }) => await attachments.put(Buffer.from(data, 'base64'), mime),
|
||||
resolveImages: async (refs) => await Promise.all(refs.map(async ({ attachmentId }) => {
|
||||
const record = await attachments.read(attachmentId);
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
|
||||
@@ -22,14 +22,14 @@ export interface CodingAttachmentStoreOptions {
|
||||
}
|
||||
|
||||
export class CodingAttachmentStore {
|
||||
private readonly createId: () => string;
|
||||
private readonly createId: (() => string) | undefined;
|
||||
private readonly maxBytes: number;
|
||||
|
||||
constructor(
|
||||
private readonly rootDir: string,
|
||||
options: CodingAttachmentStoreOptions = {},
|
||||
) {
|
||||
this.createId = options.createId ?? randomUUID;
|
||||
this.createId = options.createId;
|
||||
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
|
||||
}
|
||||
|
||||
@@ -39,11 +39,21 @@ export class CodingAttachmentStore {
|
||||
if (data.byteLength === 0 || data.byteLength > this.maxBytes) {
|
||||
throw new Error('Attachment size is invalid');
|
||||
}
|
||||
const attachmentId = this.createId();
|
||||
// Live events and session hydration repeat the same large image. Reuse its
|
||||
// stored bytes, including the original upload, instead of writing a new copy.
|
||||
const attachmentId = this.createId?.() ?? createHash('sha256')
|
||||
.update(normalizedMime).update('\0').update(data).digest('hex');
|
||||
if (!ATTACHMENT_ID_PATTERN.test(attachmentId)) throw new Error('Attachment id is invalid');
|
||||
await mkdir(this.rootDir, { recursive: true });
|
||||
const dataPath = path.join(this.rootDir, `${attachmentId}.bin`);
|
||||
const metadataPath = path.join(this.rootDir, `${attachmentId}.json`);
|
||||
if (!this.createId) {
|
||||
const existing = await stat(metadataPath).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
if (existing) return { attachmentId, mime: normalizedMime, byteLength: data.byteLength };
|
||||
}
|
||||
const temporaryDataPath = `${dataPath}.${randomUUID()}.tmp`;
|
||||
const temporaryMetadataPath = `${metadataPath}.${randomUUID()}.tmp`;
|
||||
const reference: CodingAttachmentRef = {
|
||||
|
||||
@@ -104,7 +104,9 @@ export interface PiConversationRuntimeOptions {
|
||||
pool: PiWorkerPool;
|
||||
registry: PiSessionRegistry;
|
||||
resolveModel(model: ProductModelRef): Promise<PiProviderSelection>;
|
||||
resolveImages?(attachments: Array<{ attachmentId: string }>): Promise<unknown[]>;
|
||||
resolveImages?(attachments: Array<{ attachmentId: string }>): Promise<Array<{
|
||||
type: 'image'; data: string; mimeType: string;
|
||||
}>>;
|
||||
projectImage?: PiEventProjectorOptions['projectImage'];
|
||||
createId?(kind: RuntimeIdKind): string;
|
||||
now?: () => number;
|
||||
@@ -813,9 +815,17 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
||||
clientRequestId: input.clientRequestId,
|
||||
role: 'user',
|
||||
status: 'optimistic',
|
||||
blocks: input.text.length > 0
|
||||
? [{ kind: 'text', id: `${messageId}:content:0`, text: input.text, status: 'complete' }]
|
||||
: [],
|
||||
blocks: [
|
||||
...(input.text.length > 0
|
||||
? [{ kind: 'text' as const, id: `${messageId}:content:0`, text: input.text, status: 'complete' as const }]
|
||||
: []),
|
||||
...input.attachments.map(({ attachmentId }, index) => ({
|
||||
kind: 'image' as const,
|
||||
id: `${messageId}:content:${index + (input.text.length > 0 ? 1 : 0)}`,
|
||||
attachmentId,
|
||||
mime: images[index].mimeType,
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
const command: PiRpcCommand = {
|
||||
|
||||
Reference in New Issue
Block a user