diff --git a/apps/daemon/src/runtimes/defs/grok-build.ts b/apps/daemon/src/runtimes/defs/grok-build.ts index 404cbf7f90..bc8a3576ee 100644 --- a/apps/daemon/src/runtimes/defs/grok-build.ts +++ b/apps/daemon/src/runtimes/defs/grok-build.ts @@ -49,11 +49,10 @@ export const grokBuildAgentDef = { label: 'grok-4.20-multi-agent (xAI · orchestration)', }, ], - // Prompt delivered via stdin so Windows `spawn ENAMETOOLONG` and Linux - // `spawn E2BIG` can't truncate large composed prompts. `grok -p` with - // no positional argument reads from piped stdin. - buildArgs: (_prompt, _imagePaths, _extra = [], options = {}) => { - const args = ['-p']; + // Grok Build CLI v0.1.212 enforces `-p, --single ` as value- + // required — stdin piping no longer satisfies it. Inline the prompt. + buildArgs: (prompt, _imagePaths, _extra = [], options = {}) => { + const args = ['-p', prompt]; if (options.model && options.model !== DEFAULT_MODEL_OPTION.id) { args.push('--model', options.model); } @@ -69,7 +68,21 @@ export const grokBuildAgentDef = { { id: 'xhigh', label: 'xhigh' }, { id: 'max', label: 'max' }, ], - promptViaStdin: true, + promptViaStdin: false, + // Guard against prompts that would blow Windows' ~32 KB CreateProcess + // limit (or Linux MAX_ARG_STRLEN on extreme edges) before spawn. Same + // shape as the DeepSeek adapter — the previous stdin path is gone (CLI + // 0.1.212 enforces `-p `), so the composed prompt now rides + // argv and a sufficiently large one — system text + history + skills/ + // design-system content + user message — could surface as a generic + // spawn ENAMETOOLONG / E2BIG instead of a Grok-specific, user- + // actionable message. The /api/chat spawn path checks this byte + // budget against the composed prompt and emits AGENT_PROMPT_TOO_LARGE + // ("reduce skills/design-system context, or pick an adapter with + // stdin support") before calling `spawn`. 30_000 bytes leaves ~2.7 KB + // of argv headroom under the Windows command-line limit for `-p + // --model --effort ` and internal quoting. + maxPromptArgBytes: 30_000, streamFormat: 'plain', installUrl: 'https://x.ai/cli', docsUrl: 'https://x.ai/cli', diff --git a/apps/daemon/src/runtimes/prompt-budget.ts b/apps/daemon/src/runtimes/prompt-budget.ts index ed8fa333e6..7dfc6ded43 100644 --- a/apps/daemon/src/runtimes/prompt-budget.ts +++ b/apps/daemon/src/runtimes/prompt-budget.ts @@ -10,6 +10,12 @@ function promptArgvBudgetMessage( 'Reduce the selected skills/design-system context or conversation length, or use DeepSeek through an API/provider model connection for large contexts. Pick a stdin-capable adapter when the prompt must include large local context.' ); } + if (def.id === 'grok-build') { + return ( + `${def.name} requires the prompt as the value of -p / --single (xAI CLI 0.1.212+ no longer reads piped stdin), and this run's composed prompt exceeds the safe size (${bytes} > ${def.maxPromptArgBytes} bytes). ` + + 'Reduce the selected skills/design-system context or conversation length, or pick an adapter with stdin support (e.g. claude, codex, hermes) when the prompt must include large local context.' + ); + } return ( `${def.name} requires the prompt as a command-line argument and this run's composed prompt exceeds the safe size (${bytes} > ${def.maxPromptArgBytes} bytes). ` + 'Reduce the selected skills/design-system context, shorten the conversation, or pick an adapter with stdin support.' diff --git a/apps/daemon/tests/runtimes/helpers/test-helpers.ts b/apps/daemon/tests/runtimes/helpers/test-helpers.ts index 346f463108..4ed3249052 100644 --- a/apps/daemon/tests/runtimes/helpers/test-helpers.ts +++ b/apps/daemon/tests/runtimes/helpers/test-helpers.ts @@ -86,6 +86,7 @@ export const gemini = requireAgent('gemini'); export const qoder = requireAgent('qoder'); export const qwen = requireAgent('qwen'); export const opencode = requireAgent('opencode'); +export const grokBuild = requireAgent('grok-build'); export const deepseekMaxPromptArgBytes = (() => { assert.ok( deepseek.maxPromptArgBytes !== undefined, @@ -93,6 +94,13 @@ export const deepseekMaxPromptArgBytes = (() => { ); return deepseek.maxPromptArgBytes; })(); +export const grokBuildMaxPromptArgBytes = (() => { + assert.ok( + grokBuild.maxPromptArgBytes !== undefined, + 'grok-build must define maxPromptArgBytes for argv budget tests', + ); + return grokBuild.maxPromptArgBytes; +})(); const originalDisablePlugins = process.env.OD_CODEX_DISABLE_PLUGINS; const originalPath = process.env.PATH; const originalHome = process.env.HOME; diff --git a/apps/daemon/tests/runtimes/prompt-budget.test.ts b/apps/daemon/tests/runtimes/prompt-budget.test.ts index f3d5e56c32..c445d3a033 100644 --- a/apps/daemon/tests/runtimes/prompt-budget.test.ts +++ b/apps/daemon/tests/runtimes/prompt-budget.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import { - assert, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, claude, deepseek, deepseekMaxPromptArgBytes, vibe, + assert, checkPromptArgvBudget, checkWindowsCmdShimCommandLineBudget, checkWindowsDirectExeCommandLineBudget, claude, deepseek, deepseekMaxPromptArgBytes, grokBuild, grokBuildMaxPromptArgBytes, vibe, } from './helpers/test-helpers.js'; import type { TestAgentDef } from './helpers/test-helpers.js'; @@ -107,6 +107,64 @@ test('checkPromptArgvBudget gives DeepSeek-specific guidance for large contexts' assert.match(flagged.message, /stdin-capable adapter/); }); +// Grok Build CLI 0.1.212+ enforces `-p, --single ` as value- +// required, so the prompt rides argv just like DeepSeek. Pin the budget +// field and the byte-vs-codepoint guard so a future runtime-def edit +// can't silently drop the guard or let it drift over the Windows +// CreateProcess limit. +test('grok-build declares a conservative argv-byte budget for the prompt', () => { + assert.equal( + typeof grokBuildMaxPromptArgBytes, + 'number', + 'grok-build must set maxPromptArgBytes so the spawn path can pre-flight oversized prompts before hitting CreateProcess / E2BIG', + ); + assert.ok( + grokBuildMaxPromptArgBytes > 0 && grokBuildMaxPromptArgBytes < 32_768, + `grokBuildMaxPromptArgBytes must stay strictly under the Windows CreateProcess limit (~32 KB); got ${grokBuildMaxPromptArgBytes}`, + ); +}); + +test('checkPromptArgvBudget flags oversized Grok Build prompts and lets short prompts through', () => { + const oversized = 'x'.repeat(grokBuildMaxPromptArgBytes + 1); + const flagged = checkPromptArgvBudget(grokBuild, oversized); + assert.ok(flagged, 'oversized prompts must trip the argv-byte guard'); + assert.equal(flagged.code, 'AGENT_PROMPT_TOO_LARGE'); + assert.equal(flagged.limit, grokBuildMaxPromptArgBytes); + assert.equal(flagged.bytes, grokBuildMaxPromptArgBytes + 1); + assert.match(flagged.message, /Grok Build/); + assert.match(flagged.message, /-p \/ --single/); + assert.match(flagged.message, /stdin/); + + // Happy path: chat must keep working for normal-sized prompts. + assert.equal(checkPromptArgvBudget(grokBuild, 'hello'), null); + + // Exact-budget edge: at-limit prompts pass; guard fires only on strict + // overrun. + const atLimit = 'x'.repeat(grokBuildMaxPromptArgBytes); + assert.equal(checkPromptArgvBudget(grokBuild, atLimit), null); + + // Multi-byte UTF-8 (CJK = 3 bytes) must be byte-counted, not code- + // point-counted — mirrors the DeepSeek byte-count regression guard. + const cjkOversized = '汉'.repeat( + Math.ceil(grokBuildMaxPromptArgBytes / 3) + 1, + ); + const cjkFlagged = checkPromptArgvBudget(grokBuild, cjkOversized); + assert.ok(cjkFlagged, 'byte-counted UTF-8 prompts must also trip the guard'); + assert.equal(cjkFlagged.code, 'AGENT_PROMPT_TOO_LARGE'); +}); + +test('checkPromptArgvBudget gives Grok-Build-specific guidance for large contexts', () => { + const oversized = 'x'.repeat(grokBuildMaxPromptArgBytes + 1); + const flagged = checkPromptArgvBudget(grokBuild, oversized); + + assert.ok(flagged, 'oversized Grok Build prompts must return a diagnostic'); + assert.match(flagged.message, /Grok Build/); + assert.match(flagged.message, /-p \/ --single/); + assert.match(flagged.message, /xAI CLI 0\.1\.212\+/); + assert.match(flagged.message, /no longer reads piped stdin/); + assert.match(flagged.message, /stdin support/); +}); + // Adapters that ship the prompt over stdin (every other code agent // today) don't declare `maxPromptArgBytes` and must skip the guard // entirely — applying it to them would refuse perfectly valid huge