@@ -34,7 +34,7 @@ import type {
3434 WarmQuery,
3535} from "@anthropic-ai/claude-agent-sdk";
3636import { z, type ZodType } from "zod";
37- import { buildClaudeV2Message , inferAttachmentMediaType } from "./buildClaudeV2Message";
37+ import { buildClaudeV2MessageAsync , inferAttachmentMediaType } from "./buildClaudeV2Message";
3838import { ClaudeInputPump } from "./claudeInputPump";
3939import {
4040 isCorruptThinkingTranscriptError,
@@ -87,6 +87,7 @@ import {
8787 hasNullByte,
8888 isEnoentError,
8989 nowIso,
90+ readAgentAccessibleFileBytes,
9091 readFileWithinRootSecure,
9192 resolvePathWithinRoot,
9293 stableStringify,
@@ -3105,15 +3106,16 @@ function normalizeClaudeTodoItems(
31053106 return items.length ? items : null;
31063107}
31073108
3108- function buildStreamingUserContent(
3109+ async function buildStreamingUserContent(
31093110 args: {
31103111 baseText: string;
31113112 attachments: ResolvedAgentChatFileRef[];
31123113 runtimeKind: "claude" | "opencode";
31133114 modelDescriptor?: ModelDescriptor;
31143115 logger?: Logger;
3116+ readAttachmentBytes: (attachment: ResolvedAgentChatFileRef) => Promise<Buffer>;
31153117 },
3116- ): UserContent {
3118+ ): Promise< UserContent> {
31173119 if (!args.attachments.length) {
31183120 return args.baseText;
31193121 }
@@ -3131,7 +3133,7 @@ function buildStreamingUserContent(
31313133 });
31323134 continue;
31333135 }
3134- const data = readFileWithinRootSecure(attachment._rootPath, attachment._resolvedPath );
3136+ const data = await args.readAttachmentBytes(attachment );
31353137 const mediaType = inferAttachmentMediaType(attachment);
31363138
31373139 if (attachment.type === "image") {
@@ -3185,14 +3187,24 @@ function buildStreamingUserContent(
31853187 return parts;
31863188}
31873189
3188- export function buildOpenCodeStreamMessages(args: {
3190+ export async function buildOpenCodeStreamMessages(args: {
31893191 messages: Array<{ role: string; content: string }>;
31903192 persistedTurnUserMessageIndex: number;
31913193 resolvedAttachments: ResolvedAgentChatFileRef[];
31923194 modelDescriptor: ModelDescriptor;
31933195 logger?: Logger;
3194- }): ModelMessage[] {
3195- return args.messages.map((message, index): ModelMessage => {
3196+ getDirtyFileTextForPath?: (absPath: string) => string | undefined | Promise<string | undefined>;
3197+ }): Promise<ModelMessage[]> {
3198+ const readAttachmentBytes = args.getDirtyFileTextForPath
3199+ ? async (attachment: ResolvedAgentChatFileRef) => readAgentAccessibleFileBytes({
3200+ rootPath: attachment._rootPath,
3201+ resolvedPath: attachment._resolvedPath,
3202+ getDirtyFileTextForPath: args.getDirtyFileTextForPath,
3203+ })
3204+ : async (attachment: ResolvedAgentChatFileRef) =>
3205+ readFileWithinRootSecure(attachment._rootPath, attachment._resolvedPath);
3206+
3207+ const mapped = await Promise.all(args.messages.map(async (message, index): Promise<ModelMessage> => {
31963208 const isPersistedTurnUserMessage = index === args.persistedTurnUserMessageIndex && message.role === "user";
31973209 if (!isPersistedTurnUserMessage) {
31983210 return {
@@ -3203,15 +3215,17 @@ export function buildOpenCodeStreamMessages(args: {
32033215
32043216 return {
32053217 role: "user",
3206- content: buildStreamingUserContent({
3218+ content: await buildStreamingUserContent({
32073219 baseText: message.content,
32083220 attachments: args.resolvedAttachments,
32093221 runtimeKind: "opencode",
32103222 modelDescriptor: args.modelDescriptor,
32113223 logger: args.logger,
3224+ readAttachmentBytes,
32123225 }),
32133226 };
3214- });
3227+ }));
3228+ return mapped;
32153229}
32163230
32173231function buildExecutionModeDirective(
@@ -4717,6 +4731,14 @@ export function createAgentChatService(args: {
47174731 if (!getDirtyFileTextForPath) {
47184732 throw new Error("createAgentChatService: getDirtyFileTextForPath is required");
47194733 }
4734+
4735+ const readResolvedAttachmentBytes = async (
4736+ attachment: ResolvedAgentChatFileRef,
4737+ ): Promise<Buffer> => readAgentAccessibleFileBytes({
4738+ rootPath: attachment._rootPath,
4739+ resolvedPath: attachment._resolvedPath,
4740+ getDirtyFileTextForPath,
4741+ });
47204742 if (!issueInventoryService) {
47214743 throw new Error("Issue inventory service is required to initialize agent chat.");
47224744 }
@@ -4777,8 +4799,8 @@ export function createAgentChatService(args: {
47774799 });
47784800 };
47794801
4780- const stageAttachmentForCodexInput = (attachment: ResolvedAgentChatFileRef): string => {
4781- const content = readFileWithinRootSecure (attachment._rootPath, attachment._resolvedPath );
4802+ const stageAttachmentForCodexInput = async (attachment: ResolvedAgentChatFileRef): Promise< string> => {
4803+ const content = await readResolvedAttachmentBytes (attachment);
47824804 const stagedDir = path.join(layout.tmpDir, "agent-chat-attachments");
47834805 fs.mkdirSync(stagedDir, { recursive: true });
47844806 const baseName = path.basename(attachment.path) || path.basename(attachment._resolvedPath) || "attachment";
@@ -8401,6 +8423,7 @@ export function createAgentChatService(args: {
84018423 managed: ManagedChatSession,
84028424 ): UniversalToolSetOptions => ({
84038425 permissionMode: toHarnessPermissionMode(managed.session.permissionMode),
8426+ getDirtyFileTextForPath,
84048427 getTodoItems: () => managed.todoItems,
84058428 onTodoUpdate: (items) => {
84068429 emitChatEvent(managed, {
@@ -9581,7 +9604,7 @@ export function createAgentChatService(args: {
95819604 input.push({ type: "image", url: attachment.url });
95829605 continue;
95839606 }
9584- const stagedPath = stageAttachmentForCodexInput(attachment);
9607+ const stagedPath = await stageAttachmentForCodexInput(attachment);
95859608 if (attachment.type === "image") {
95869609 input.push({ type: "localImage", path: stagedPath });
95879610 continue;
@@ -10025,10 +10048,11 @@ export function createAgentChatService(args: {
1002510048
1002610049 // Build the message after permission-mode recovery, because rebuilding a
1002710050 // fresh Claude SDK session clears runtime.sdkSessionId.
10028- const messageToSend = buildClaudeV2Message (basePromptText, resolvedAttachments, {
10051+ const messageToSend = await buildClaudeV2MessageAsync (basePromptText, resolvedAttachments, {
1002910052 baseDir: managed.laneWorktreePath,
1003010053 sessionId: runtime.sdkSessionId,
1003110054 forceUserMessage: true,
10055+ getDirtyFileTextForPath,
1003210056 }) as unknown as SDKUserMessage;
1003310057 messageToSend.uuid = userMessageId;
1003410058 messageToSend.timestamp = new Date().toISOString();
@@ -17384,34 +17408,25 @@ export function createAgentChatService(args: {
1738417408 /** Maximum bytes to inline for a non-image chat attachment. */
1738517409 const MAX_INLINE_BYTES = 512 * 1024; // 512 KB
1738617410
17387- const buildAgentPromptBlocks = (
17411+ const buildAgentPromptBlocks = async (
1738817412 promptText: string,
1738917413 resolvedAttachments: ResolvedAgentChatFileRef[],
17390- ): Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> => {
17414+ ): Promise< Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> > => {
1739117415 const blocks: Array<
1739217416 { type: "text"; text: string } | { type: "image"; data: string; mimeType: string }
1739317417 > = [{ type: "text", text: promptText }];
1739417418 for (const attachment of resolvedAttachments) {
1739517419 try {
17396- // Check file size before reading the full contents into memory.
17397- let fileSize: number;
17398- try {
17399- fileSize = fs.statSync(attachment._resolvedPath).size;
17400- } catch {
17401- // stat failed -- skip unreadable attachment
17402- continue;
17403- }
17420+ const buf = await readResolvedAttachmentBytes(attachment);
1740417421
1740517422 if (attachment.type === "image") {
17406- const buf = readFileWithinRootSecure(attachment._rootPath, attachment._resolvedPath);
1740717423 blocks.push({
1740817424 type: "image",
1740917425 data: buf.toString("base64"),
1741017426 mimeType: guessImageMimeForPath(attachment._resolvedPath),
1741117427 });
17412- } else if (fileSize <= MAX_INLINE_BYTES) {
17428+ } else if (buf.length <= MAX_INLINE_BYTES) {
1741317429 // Non-image file attachment -- include content as text if not binary
17414- const buf = readFileWithinRootSecure(attachment._rootPath, attachment._resolvedPath);
1741517430 if (hasNullByte(buf)) {
1741617431 blocks.push({
1741717432 type: "text",
@@ -17428,7 +17443,7 @@ export function createAgentChatService(args: {
1742817443 // File is too large to inline -- push a placeholder with a truncated preview.
1742917444 blocks.push({
1743017445 type: "text",
17431- text: `[File: ${attachment.path} omitted: size ${fileSize } bytes]`,
17446+ text: `[File: ${attachment.path} omitted: size ${buf.length } bytes]`,
1743217447 });
1743317448 }
1743417449 } catch {
@@ -17938,7 +17953,7 @@ export function createAgentChatService(args: {
1793817953 }
1793917954 }
1794017955
17941- const promptBlocks = buildAgentPromptBlocks(composed, args.resolvedAttachments);
17956+ const promptBlocks = await buildAgentPromptBlocks(composed, args.resolvedAttachments);
1794217957 const promptText = promptBlocks
1794317958 .filter((block): block is { type: "text"; text: string } => block.type === "text")
1794417959 .map((block) => block.text)
@@ -18280,7 +18295,7 @@ export function createAgentChatService(args: {
1828018295 cloudComposed = `${injected}\n\n${cloudComposed}`;
1828118296 }
1828218297 }
18283- const promptBlocks = buildAgentPromptBlocks(cloudComposed, args.resolvedAttachments);
18298+ const promptBlocks = await buildAgentPromptBlocks(cloudComposed, args.resolvedAttachments);
1828418299 const promptText = promptBlocks
1828518300 .filter((block): block is { type: "text"; text: string } => block.type === "text")
1828618301 .map((block) => block.text)
@@ -19095,7 +19110,7 @@ export function createAgentChatService(args: {
1909519110 "## User Request",
1909619111 composed,
1909719112 ].join("\n");
19098- const promptBlocks = buildAgentPromptBlocks(sdkInput, args.resolvedAttachments);
19113+ const promptBlocks = await buildAgentPromptBlocks(sdkInput, args.resolvedAttachments);
1909919114 const sdkPromptText = promptBlocks
1910019115 .filter((block): block is { type: "text"; text: string } => block.type === "text")
1910119116 .map((block) => block.text)
@@ -19839,7 +19854,7 @@ export function createAgentChatService(args: {
1983919854 input.push({ type: "image", url: attachment.url });
1984019855 continue;
1984119856 }
19842- const stagedPath = stageAttachmentForCodexInput(attachment);
19857+ const stagedPath = await stageAttachmentForCodexInput(attachment);
1984319858 if (attachment.type === "image") {
1984419859 input.push({ type: "localImage", path: stagedPath });
1984519860 continue;
@@ -20038,10 +20053,11 @@ export function createAgentChatService(args: {
2003820053 const dispatchUuid = randomUUID();
2003920054 const contextPrompt = buildChatContextAttachmentPrompt(steer.contextAttachments);
2004020055 const inlineSteerText = contextPrompt ? `${contextPrompt}\n\n${steer.text}` : steer.text;
20041- const sdkMsg = buildClaudeV2Message (inlineSteerText, steer.resolvedAttachments, {
20056+ const sdkMsg = await buildClaudeV2MessageAsync (inlineSteerText, steer.resolvedAttachments, {
2004220057 baseDir: managed.laneWorktreePath,
2004320058 sessionId: runtime.sdkSessionId ?? null,
2004420059 forceUserMessage: true,
20060+ getDirtyFileTextForPath,
2004520061 }) as unknown as SDKUserMessage;
2004620062 sdkMsg.shouldQuery = false;
2004720063 sdkMsg.uuid = dispatchUuid;
0 commit comments