fix(text-generation): refuse an oversized argv prompt on Linux too

The pre-spawn size guard only ran on Windows. Linux caps a single argv
entry at MAX_ARG_STRLEN (32 pages, 128 KiB on a 4-KiB-page host) and
execve fails with E2BIG past it, so an agent that delivers the whole
prompt as one argument — jcode, and the other argv-delivery agents —
failed on a large staged diff with an error the user could not act on.

The cap is per-argument and in bytes, which is why it is not the Windows
line budget: 40k chars trips Windows and is nowhere near the Linux limit,
so folding them together would have refused prompts Linux runs fine.

Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
This commit is contained in:
Neil
2026-10-03 01:49:40 -07:00
committed by Neil
co-authored by czzczz
parent 07d8aff138
commit 154b2fdab8
2 changed files with 66 additions and 7 deletions
@@ -68,6 +68,45 @@ describe('generateCommitMessageFromContext', () => {
})
})
it('fails clearly before spawning when a jcode argv prompt exceeds the Linux single-argument cap', async () => {
await withPlatform('linux', async () => {
const pending = generateCommitMessageFromContext(
{
branch: 'main',
stagedSummary: 'M\tREADME.md',
// Why past 120 KiB and not the Windows 30k: Linux fails on ONE argument
// over MAX_ARG_STRLEN, which is far larger than the Windows line budget.
stagedPatch: `+${'x'.repeat(140_000)}`
},
{ agentId: 'jcode', model: 'default' },
{ kind: 'local', cwd: '/repo', env: { ...process.env } }
)
await expect(pending).resolves.toMatchObject({
success: false,
error: expect.stringContaining('single command-line argument')
})
expect(spawnMock).not.toHaveBeenCalled()
})
})
it('still spawns on Linux for a prompt that only Windows would refuse', async () => {
await withPlatform('linux', async () => {
// 40k chars trips the Windows line budget but is far under the Linux per-arg cap,
// so the guard must not have become a lowest-common-denominator limit.
await generateCommitMessageFromContext(
{
branch: 'main',
stagedSummary: 'M\tREADME.md',
stagedPatch: `+${'x'.repeat(40_000)}`
},
{ agentId: 'jcode', model: 'default' },
{ kind: 'local', cwd: '/repo', env: { ...process.env } }
)
expect(spawnMock).toHaveBeenCalled()
})
})
it('keeps local commit-message and pull-request cancellation lanes separate', async () => {
const children: {
pid: number
@@ -56,6 +56,28 @@ function exceedsWindowsCommandLineBudget(command: string, args: string[]): boole
return units > WINDOWS_COMMAND_LINE_UNIT_BUDGET
}
// Why separate from the Windows budget: Linux caps a SINGLE argv entry at
// MAX_ARG_STRLEN (32 pages, so 128 KiB on a 4-KiB-page host) and execve fails with
// E2BIG past it, well before the much larger total-argv limit. Agents that deliver the
// whole prompt as one argument trip this on a big staged diff, so the cap is per-arg
// and in bytes, not units. Headroom left for hosts whose page size differs.
const LINUX_SINGLE_ARGUMENT_BYTE_BUDGET = 120 * 1024
function exceedsLinuxArgumentBudget(args: string[]): boolean {
return args.some((arg) => Buffer.byteLength(arg, 'utf8') > LINUX_SINGLE_ARGUMENT_BYTE_BUDGET)
}
/** The user-facing reason this plan cannot be spawned here, or null when it can. */
function argumentBudgetFailure(plan: CommitMessagePlan): string | null {
if (process.platform === 'win32' && exceedsWindowsCommandLineBudget(plan.binary, plan.args)) {
return `${plan.label} prompt is too large for the Windows command line. Stage fewer changes and try again.`
}
if (process.platform === 'linux' && exceedsLinuxArgumentBudget(plan.args)) {
return `${plan.label} prompt is too large to pass as a single command-line argument. Stage fewer changes and try again.`
}
return null
}
export function runLocalSourceControlPlan(input: {
plan: CommitMessagePlan
cwd: string
@@ -74,14 +96,12 @@ export function runLocalSourceControlPlan(input: {
const result = new Promise<InternalTextGenerationResult>((resolve) => {
let child: SpawnedSourceControlAgentProcess
try {
if (process.platform === 'win32' && exceedsWindowsCommandLineBudget(plan.binary, plan.args)) {
// Why before spawn: agents like jcode ride the whole prompt on argv, so a large
// staged diff fails at execve with an error the user cannot act on.
const budgetFailure = argumentBudgetFailure(plan)
if (budgetFailure) {
markProcessClosed()
resolve({
success: false,
// Why: jcode rides the whole prompt on argv; a large staged diff would
// exceed Windows' 32,767-unit command line and fail to spawn at all.
error: `${plan.label} prompt is too large for the Windows command line. Stage fewer changes and try again.`
})
resolve({ success: false, error: budgetFailure })
return
}
child = input.spawnAgent({