From 29b8b7939c13599cb5e25b40056d9ce645c7be96 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 15 May 2026 18:13:59 -0700 Subject: [PATCH] Fix Grok adapter review regressions --- .../grok-local/src/server/execute.test.ts | 47 ++ .../adapters/grok-local/src/server/execute.ts | 582 +++++++++--------- .../grok-local/src/ui/parse-stdout.test.ts | 9 + .../grok-local/src/ui/parse-stdout.ts | 9 +- 4 files changed, 355 insertions(+), 292 deletions(-) diff --git a/packages/adapters/grok-local/src/server/execute.test.ts b/packages/adapters/grok-local/src/server/execute.test.ts index 0da0b172a38..b520bf13e69 100644 --- a/packages/adapters/grok-local/src/server/execute.test.ts +++ b/packages/adapters/grok-local/src/server/execute.test.ts @@ -137,4 +137,51 @@ describe("grok_local execute", () => { expect(await pathExists(path.join(root, ".claude", "skills", "paperclip"))).toBe(false); expect(logs.map((entry) => entry.chunk)).not.toEqual([]); }); + + it("cleans up staged assets when setup fails before the Grok process starts", async () => { + const root = await makeTempRoot(); + const instructionsPath = path.join(root, "managed", "AGENTS.md"); + const skillSource = path.join(root, "runtime-skills", "paperclip"); + await fs.mkdir(path.dirname(instructionsPath), { recursive: true }); + await fs.writeFile(instructionsPath, "You are Grok.\n", "utf8"); + await fs.mkdir(skillSource, { recursive: true }); + await fs.writeFile(path.join(skillSource, "SKILL.md"), "---\nname: paperclip\ndescription: test\n---\n", "utf8"); + ensureCommandMock.mockRejectedValueOnce(new Error("grok not installed")); + + const ctx: AdapterExecutionContext = { + runId: "run-setup-fail", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Grok Agent", + adapterType: "grok_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + cwd: root, + instructionsFilePath: instructionsPath, + paperclipRuntimeSkills: [{ + key: "paperclip", + runtimeName: "paperclip", + source: skillSource, + required: false, + }], + paperclipSkillSync: { desiredSkills: ["paperclip"] }, + }, + context: {}, + authToken: "run-token", + onLog: async () => {}, + }; + + await expect(execute(ctx)).rejects.toThrow("grok not installed"); + expect(runProcessMock).not.toHaveBeenCalled(); + expect(await pathExists(path.join(root, "Agents.md"))).toBe(false); + expect(await pathExists(path.join(root, ".claude", "skills", "paperclip"))).toBe(false); + }); }); diff --git a/packages/adapters/grok-local/src/server/execute.ts b/packages/adapters/grok-local/src/server/execute.ts index 115e539b7f4..0d25845adae 100644 --- a/packages/adapters/grok-local/src/server/execute.ts +++ b/packages/adapters/grok-local/src/server/execute.ts @@ -237,96 +237,47 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0; - const env: Record = { ...buildPaperclipEnv(agent) }; - env.PAPERCLIP_RUN_ID = runId; - const wakeTaskId = - (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) || - (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) || - null; - const wakeReason = - typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0 - ? context.wakeReason.trim() - : null; - const wakeCommentId = - (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) || - (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) || - null; - const approvalId = - typeof context.approvalId === "string" && context.approvalId.trim().length > 0 - ? context.approvalId.trim() - : null; - const approvalStatus = - typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0 - ? context.approvalStatus.trim() - : null; - const linkedIssueIds = Array.isArray(context.issueIds) - ? context.issueIds.filter((value: unknown): value is string => typeof value === "string" && value.trim().length > 0) - : []; - const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake); - const issueWorkMode = readPaperclipIssueWorkModeFromContext(context); - if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId; - if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode; - if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason; - if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId; - if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId; - if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus; - if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); - if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - refreshPaperclipWorkspaceEnvForExecution({ - env, - envConfig, - workspaceCwd: effectiveWorkspaceCwd, - workspaceSource, - workspaceId, - workspaceRepoUrl, - workspaceRepoRef, - workspaceHints, - agentHome, - executionTargetIsRemote, - executionCwd: effectiveExecutionCwd, - }); - if (!hasExplicitApiKey && authToken) { - env.PAPERCLIP_API_KEY = authToken; - } - - const timeoutSec = resolveAdapterExecutionTargetTimeoutSec( - executionTarget, - asNumber(config.timeoutSec, 0), - ); - const graceSec = asNumber(config.graceSec, 20); - await ensureAdapterExecutionTargetRuntimeCommandInstalled({ - runId, - target: executionTarget, - installCommand: ctx.runtimeCommandSpec?.installCommand, - detectCommand: ctx.runtimeCommandSpec?.detectCommand, - cwd, - env, - timeoutSec, - graceSec, - onLog, - }); - let restoreRemoteWorkspace: (() => Promise) | null = null; - if (executionTargetIsRemote) { - await onLog( - "stdout", - `[paperclip] Syncing Grok workspace to ${describeAdapterExecutionTarget(executionTarget)}.\n`, - ); - const preparedExecutionTargetRuntime = await prepareAdapterExecutionTargetRuntime({ - runId, - target: executionTarget, - adapterKey: "grok", - workspaceLocalDir: cwd, - timeoutSec, - installCommand: ctx.runtimeCommandSpec?.installCommand ?? null, - detectCommand: ctx.runtimeCommandSpec?.detectCommand ?? command, - }); - restoreRemoteWorkspace = () => preparedExecutionTargetRuntime.restoreWorkspace(); - effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; + + try { + const envConfig = parseObject(config.env); + const hasExplicitApiKey = + typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0; + const env: Record = { ...buildPaperclipEnv(agent) }; + env.PAPERCLIP_RUN_ID = runId; + const wakeTaskId = + (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) || + (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) || + null; + const wakeReason = + typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0 + ? context.wakeReason.trim() + : null; + const wakeCommentId = + (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) || + (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) || + null; + const approvalId = + typeof context.approvalId === "string" && context.approvalId.trim().length > 0 + ? context.approvalId.trim() + : null; + const approvalStatus = + typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0 + ? context.approvalStatus.trim() + : null; + const linkedIssueIds = Array.isArray(context.issueIds) + ? context.issueIds.filter((value: unknown): value is string => typeof value === "string" && value.trim().length > 0) + : []; + const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake); + const issueWorkMode = readPaperclipIssueWorkModeFromContext(context); + if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId; + if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode; + if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason; + if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId; + if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId; + if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus; + if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); + if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; refreshPaperclipWorkspaceEnvForExecution({ env, envConfig, @@ -340,224 +291,273 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", - ), - ); - const runtimeEnv = ensurePathInEnv(effectiveEnv); - await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { - installCommand: ctx.runtimeCommandSpec?.installCommand ?? null, - timeoutSec, - }); - const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv); - const loggedEnv = buildInvocationEnvForLogs(env, { - runtimeEnv, - includeRuntimeKeys: ["HOME"], - resolvedCommand, - }); - const billingType = resolveBillingType(effectiveEnv); - - const runtimeSessionParams = parseObject(runtime.sessionParams); - const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? ""); - const runtimeSessionCwd = asString(runtimeSessionParams.cwd, ""); - const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution); - const canResumeSession = - runtimeSessionId.length > 0 && - (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(effectiveExecutionCwd)) && - adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget); - const sessionId = canResumeSession ? runtimeSessionId : null; - if (executionTargetIsRemote && runtimeSessionId && !canResumeSession) { - await onLog( - "stdout", - `[paperclip] Grok session "${runtimeSessionId}" does not match the current remote execution identity and will not be resumed in "${effectiveExecutionCwd}". Starting a fresh remote session.\n`, + const timeoutSec = resolveAdapterExecutionTargetTimeoutSec( + executionTarget, + asNumber(config.timeoutSec, 0), ); - } else if (runtimeSessionId && !canResumeSession) { - await onLog( - "stdout", - `[paperclip] Grok session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".\n`, - ); - } - - const commandNotes = (() => { - const notes: string[] = ["Prompt is passed to Grok via --single in headless mode."]; - if (alwaysApprove) notes.push("Added --always-approve for unattended execution."); - if (stagedAssets.stagedInstructionsPath) { - notes.push(`Staged project instructions at ${stagedAssets.stagedInstructionsPath} for native Grok discovery.`); - } - if (stagedAssets.rulesFilePath) { - notes.push(`Applied fallback instructions via --rules @${stagedAssets.rulesFilePath}.`); - } - if (stagedAssets.stagedSkillsCount > 0) { - notes.push(`Staged ${stagedAssets.stagedSkillsCount} Paperclip skill(s) into .claude/skills for native Grok discovery.`); - } - return notes; - })(); - - const templateData = { - agentId: agent.id, - companyId: agent.companyId, - runId, - company: { id: agent.companyId }, - agent, - run: { id: runId, source: "on_demand" }, - context, - }; - const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); - const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; - const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); - const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); - const paperclipEnvNote = renderPaperclipEnvNote(env); - const apiAccessNote = renderApiAccessNote(env); - const prompt = joinPromptSections([ - wakePrompt, - sessionHandoffNote, - paperclipEnvNote, - apiAccessNote, - renderedPrompt, - ]); - const promptMetrics = { - promptChars: prompt.length, - wakePromptChars: wakePrompt.length, - sessionHandoffChars: sessionHandoffNote.length, - runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length, - heartbeatPromptChars: renderedPrompt.length, - }; - - const buildArgs = (resumeSessionId: string | null) => { - const args = ["--cwd", effectiveExecutionCwd, "--output-format", "streaming-json"]; - if (resumeSessionId) args.push("--resume", resumeSessionId); - if (model && model !== DEFAULT_GROK_LOCAL_MODEL) args.push("--model", model); - if (reasoningEffort) args.push("--reasoning-effort", reasoningEffort); - if (maxTurns > 0) args.push("--max-turns", String(maxTurns)); - if (permissionMode) args.push("--permission-mode", permissionMode); - if (alwaysApprove) args.push("--always-approve"); - if (disableWebSearch) args.push("--disable-web-search"); - if (stagedAssets.rulesFilePath) args.push("--rules", `@${stagedAssets.rulesFilePath}`); - const extraArgs = (() => { - const fromExtraArgs = asStringArray(config.extraArgs); - if (fromExtraArgs.length > 0) return fromExtraArgs; - return asStringArray(config.args); - })(); - if (extraArgs.length > 0) args.push(...extraArgs); - args.push("--single", prompt); - return args; - }; - - const runAttempt = async (resumeSessionId: string | null) => { - const args = buildArgs(resumeSessionId); - if (onMeta) { - await onMeta({ - adapterType: "grok_local", - command: resolvedCommand, - cwd: effectiveExecutionCwd, - commandNotes, - commandArgs: args.map((value, index) => ( - index === args.length - 1 ? `` : value - )), - env: loggedEnv, - prompt, - promptMetrics, - context, - }); - } - - const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, { + const graceSec = asNumber(config.graceSec, 20); + await ensureAdapterExecutionTargetRuntimeCommandInstalled({ + runId, + target: executionTarget, + installCommand: ctx.runtimeCommandSpec?.installCommand, + detectCommand: ctx.runtimeCommandSpec?.detectCommand, cwd, env, timeoutSec, graceSec, - onSpawn, onLog, }); - return { - proc, - parsed: parseGrokJsonl(proc.stdout), - }; - }; - const toResult = ( - attempt: { - proc: { - exitCode: number | null; - signal: string | null; - timedOut: boolean; - stdout: string; - stderr: string; + if (executionTargetIsRemote) { + await onLog( + "stdout", + `[paperclip] Syncing Grok workspace to ${describeAdapterExecutionTarget(executionTarget)}.\n`, + ); + const preparedExecutionTargetRuntime = await prepareAdapterExecutionTargetRuntime({ + runId, + target: executionTarget, + adapterKey: "grok", + workspaceLocalDir: cwd, + timeoutSec, + installCommand: ctx.runtimeCommandSpec?.installCommand ?? null, + detectCommand: ctx.runtimeCommandSpec?.detectCommand ?? command, + }); + restoreRemoteWorkspace = () => preparedExecutionTargetRuntime.restoreWorkspace(); + effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; + refreshPaperclipWorkspaceEnvForExecution({ + env, + envConfig, + workspaceCwd: effectiveWorkspaceCwd, + workspaceSource, + workspaceId, + workspaceRepoUrl, + workspaceRepoRef, + workspaceHints, + agentHome, + executionTargetIsRemote, + executionCwd: effectiveExecutionCwd, + }); + } + + const runtimeExecutionTarget = overrideAdapterExecutionTargetRemoteCwd(executionTarget, effectiveExecutionCwd); + const effectiveEnv = Object.fromEntries( + Object.entries({ ...process.env, ...env }).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ); + const runtimeEnv = ensurePathInEnv(effectiveEnv); + await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { + installCommand: ctx.runtimeCommandSpec?.installCommand ?? null, + timeoutSec, + }); + const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv); + const loggedEnv = buildInvocationEnvForLogs(env, { + runtimeEnv, + includeRuntimeKeys: ["HOME"], + resolvedCommand, + }); + const billingType = resolveBillingType(effectiveEnv); + + const runtimeSessionParams = parseObject(runtime.sessionParams); + const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? ""); + const runtimeSessionCwd = asString(runtimeSessionParams.cwd, ""); + const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution); + const canResumeSession = + runtimeSessionId.length > 0 && + (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(effectiveExecutionCwd)) && + adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget); + const sessionId = canResumeSession ? runtimeSessionId : null; + if (executionTargetIsRemote && runtimeSessionId && !canResumeSession) { + await onLog( + "stdout", + `[paperclip] Grok session "${runtimeSessionId}" does not match the current remote execution identity and will not be resumed in "${effectiveExecutionCwd}". Starting a fresh remote session.\n`, + ); + } else if (runtimeSessionId && !canResumeSession) { + await onLog( + "stdout", + `[paperclip] Grok session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".\n`, + ); + } + + const commandNotes = (() => { + const notes: string[] = ["Prompt is passed to Grok via --single in headless mode."]; + if (alwaysApprove) notes.push("Added --always-approve for unattended execution."); + if (stagedAssets.stagedInstructionsPath) { + notes.push(`Staged project instructions at ${stagedAssets.stagedInstructionsPath} for native Grok discovery.`); + } + if (stagedAssets.rulesFilePath) { + notes.push(`Applied fallback instructions via --rules @${stagedAssets.rulesFilePath}.`); + } + if (stagedAssets.stagedSkillsCount > 0) { + notes.push(`Staged ${stagedAssets.stagedSkillsCount} Paperclip skill(s) into .claude/skills for native Grok discovery.`); + } + return notes; + })(); + + const templateData = { + agentId: agent.id, + companyId: agent.companyId, + runId, + company: { id: agent.companyId }, + agent, + run: { id: runId, source: "on_demand" }, + context, + }; + const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) }); + const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0; + const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData); + const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); + const paperclipEnvNote = renderPaperclipEnvNote(env); + const apiAccessNote = renderApiAccessNote(env); + const prompt = joinPromptSections([ + wakePrompt, + sessionHandoffNote, + paperclipEnvNote, + apiAccessNote, + renderedPrompt, + ]); + const promptMetrics = { + promptChars: prompt.length, + wakePromptChars: wakePrompt.length, + sessionHandoffChars: sessionHandoffNote.length, + runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length, + heartbeatPromptChars: renderedPrompt.length, + }; + + const buildArgs = (resumeSessionId: string | null) => { + const args = ["--cwd", effectiveExecutionCwd, "--output-format", "streaming-json"]; + if (resumeSessionId) args.push("--resume", resumeSessionId); + if (model && model !== DEFAULT_GROK_LOCAL_MODEL) args.push("--model", model); + if (reasoningEffort) args.push("--reasoning-effort", reasoningEffort); + if (maxTurns > 0) args.push("--max-turns", String(maxTurns)); + if (permissionMode) args.push("--permission-mode", permissionMode); + if (alwaysApprove) args.push("--always-approve"); + if (disableWebSearch) args.push("--disable-web-search"); + if (stagedAssets.rulesFilePath) args.push("--rules", `@${stagedAssets.rulesFilePath}`); + const extraArgs = (() => { + const fromExtraArgs = asStringArray(config.extraArgs); + if (fromExtraArgs.length > 0) return fromExtraArgs; + return asStringArray(config.args); + })(); + if (extraArgs.length > 0) args.push(...extraArgs); + args.push("--single", prompt); + return args; + }; + + const runAttempt = async (resumeSessionId: string | null) => { + const args = buildArgs(resumeSessionId); + if (onMeta) { + await onMeta({ + adapterType: "grok_local", + command: resolvedCommand, + cwd: effectiveExecutionCwd, + commandNotes, + commandArgs: args.map((value, index) => ( + index === args.length - 1 ? `` : value + )), + env: loggedEnv, + prompt, + promptMetrics, + context, + }); + } + + const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, { + cwd, + env, + timeoutSec, + graceSec, + onSpawn, + onLog, + }); + return { + proc, + parsed: parseGrokJsonl(proc.stdout), }; - parsed: ReturnType; - }, - clearSessionOnMissingSession = false, - isRetry = false, - ): AdapterExecutionResult => { - if (attempt.proc.timedOut) { + }; + + const toResult = ( + attempt: { + proc: { + exitCode: number | null; + signal: string | null; + timedOut: boolean; + stdout: string; + stderr: string; + }; + parsed: ReturnType; + }, + clearSessionOnMissingSession = false, + isRetry = false, + ): AdapterExecutionResult => { + if (attempt.proc.timedOut) { + return { + exitCode: attempt.proc.exitCode, + signal: attempt.proc.signal, + timedOut: true, + errorMessage: `Timed out after ${timeoutSec}s`, + clearSession: clearSessionOnMissingSession, + }; + } + + const failed = (attempt.proc.exitCode ?? 0) !== 0; + const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : ""; + const stderrLine = firstNonEmptyLine(attempt.proc.stderr); + const fallbackErrorMessage = + parsedError || + stderrLine || + `Grok exited with code ${attempt.proc.exitCode ?? -1}`; + + const canFallbackToRuntimeSession = !isRetry; + const resolvedSessionId = attempt.parsed.sessionId + ?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null); + const resolvedSessionParams = resolvedSessionId + ? ({ + sessionId: resolvedSessionId, + cwd: effectiveExecutionCwd, + ...(workspaceId ? { workspaceId } : {}), + ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}), + ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}), + ...(executionTargetIsRemote + ? { + remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget), + } + : {}), + } as Record) + : null; + return { exitCode: attempt.proc.exitCode, signal: attempt.proc.signal, - timedOut: true, - errorMessage: `Timed out after ${timeoutSec}s`, - clearSession: clearSessionOnMissingSession, - }; - } - - const failed = (attempt.proc.exitCode ?? 0) !== 0; - const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : ""; - const stderrLine = firstNonEmptyLine(attempt.proc.stderr); - const fallbackErrorMessage = - parsedError || - stderrLine || - `Grok exited with code ${attempt.proc.exitCode ?? -1}`; - - const canFallbackToRuntimeSession = !isRetry; - const resolvedSessionId = attempt.parsed.sessionId - ?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null); - const resolvedSessionParams = resolvedSessionId - ? ({ + timedOut: false, + errorMessage: failed ? fallbackErrorMessage : null, + usage: { + inputTokens: 0, + outputTokens: 0, + cachedInputTokens: 0, + }, sessionId: resolvedSessionId, - cwd: effectiveExecutionCwd, - ...(workspaceId ? { workspaceId } : {}), - ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}), - ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}), - ...(executionTargetIsRemote - ? { - remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget), - } - : {}), - } as Record) - : null; - - return { - exitCode: attempt.proc.exitCode, - signal: attempt.proc.signal, - timedOut: false, - errorMessage: failed ? fallbackErrorMessage : null, - usage: { - inputTokens: 0, - outputTokens: 0, - cachedInputTokens: 0, - }, - sessionId: resolvedSessionId, - sessionParams: resolvedSessionParams, - sessionDisplayId: resolvedSessionId, - provider: "xai", - biller: billingType === "api" ? "xai" : "grok", - model, - billingType, - costUsd: null, - resultJson: { - stopReason: attempt.parsed.stopReason, - requestId: attempt.parsed.requestId, - ...(failed ? { stderr: attempt.proc.stderr } : {}), - }, - summary: attempt.parsed.summary, - clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId), + sessionParams: resolvedSessionParams, + sessionDisplayId: resolvedSessionId, + provider: "xai", + biller: billingType === "api" ? "xai" : "grok", + model, + billingType, + costUsd: null, + resultJson: { + stopReason: attempt.parsed.stopReason, + requestId: attempt.parsed.requestId, + ...(failed ? { stderr: attempt.proc.stderr } : {}), + }, + summary: attempt.parsed.summary, + clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId), + }; }; - }; - try { const initial = await runAttempt(sessionId); if ( sessionId && diff --git a/packages/adapters/grok-local/src/ui/parse-stdout.test.ts b/packages/adapters/grok-local/src/ui/parse-stdout.test.ts index 715afc8896a..d60b5b619a2 100644 --- a/packages/adapters/grok-local/src/ui/parse-stdout.test.ts +++ b/packages/adapters/grok-local/src/ui/parse-stdout.test.ts @@ -15,4 +15,13 @@ describe("parseGrokStdoutLine", () => { { kind: "system", ts, text: "stop_reason=EndTurn session=sess-1" }, ]); }); + + it("surfaces structured Grok error payload text", () => { + expect(parseGrokStdoutLine(JSON.stringify({ + type: "error", + error: { message: "Authentication required" }, + }), ts)).toEqual([ + { kind: "stderr", ts, text: "Authentication required" }, + ]); + }); }); diff --git a/packages/adapters/grok-local/src/ui/parse-stdout.ts b/packages/adapters/grok-local/src/ui/parse-stdout.ts index e75e0271b7b..b82a36b4647 100644 --- a/packages/adapters/grok-local/src/ui/parse-stdout.ts +++ b/packages/adapters/grok-local/src/ui/parse-stdout.ts @@ -17,6 +17,13 @@ function asString(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback; } +function extractErrorText(value: unknown): string { + if (typeof value === "string") return value; + const record = asRecord(value); + if (!record) return ""; + return asString(record.message) || asString(record.detail) || asString(record.code); +} + export function parseGrokStdoutLine(line: string, ts: string): TranscriptEntry[] { const parsed = asRecord(safeJsonParse(line)); if (!parsed) { @@ -36,7 +43,7 @@ export function parseGrokStdoutLine(line: string, ts: string): TranscriptEntry[] } if (type === "error") { - const text = asString(parsed.data) || asString(parsed.message) || asString(parsed.error); + const text = asString(parsed.data) || asString(parsed.message) || extractErrorText(parsed.error); return text ? [{ kind: "stderr", ts, text }] : [{ kind: "stderr", ts, text: "Grok error" }]; }