fix(cursor-hook): keep npx fallback under the postToolUse host budget

hooks.json grants postToolUse a 10s budget. The npx fallback branch
added a flat +5s on top of the inner 7s spawnSync budget, reaching
12s — past the host deadline. When the local gitnexus package is
absent and npx cold-starts, Cursor kills the hook before the child
finishes, so the augmentation result is always lost on exactly the
machines the fallback exists for.

Cap the fallback at 10000 - 2000 = 8000ms (host budget minus
headroom for node startup and the final stdout write) via
Math.min, and add a source-level regression test asserting the cap
wiring so the fallback can never exceed the host budget again.
This commit is contained in:
luyua9 2026-09-15 12:35:11 +08:00
parent 41fa74cd84
commit cd5329bfc9
2 changed files with 34 additions and 1 deletions

View file

@ -339,6 +339,16 @@ function resolveCliPath() {
}
}
// The Cursor host enforces hooks.json's postToolUse `timeout` (10s) against
// the whole hook process. The npx fallback used to add a flat +5s on top of
// the inner budget (7000 → 12000ms), past the host deadline, so Cursor killed
// the hook before the child finished and the augmentation was always lost on
// cold-start machines — the exact scenario the fallback exists for. Cap the
// fallback under the host budget, leaving headroom for node startup and the
// final stdout write.
const CURSOR_HOST_BUDGET_MS = 10000;
const CURSOR_NPX_HEADROOM_MS = 2000;
function runGitNexusCli(cliPath, args, cwd, timeout) {
const isWin = process.platform === 'win32';
if (cliPath) {
@ -350,9 +360,13 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
windowsHide: true,
});
}
const npxTimeout = Math.min(
timeout + 5000,
CURSOR_HOST_BUDGET_MS - CURSOR_NPX_HEADROOM_MS,
);
return spawnSync(isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], {
encoding: 'utf-8',
timeout: timeout + 5000,
timeout: npxTimeout,
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,

View file

@ -441,6 +441,25 @@ describe('Cursor hook debug logging', () => {
});
});
// ─── Source code regression: npx fallback stays under the host budget ─────
describe('Cursor hook npx fallback host budget', () => {
const source = fs.readFileSync(CURSOR_HOOK, 'utf-8');
it('caps the npx fallback timeout below the hooks.json postToolUse budget', () => {
// hooks.json grants postToolUse 10s. The fallback branch adds +5s on top
// of the inner 7s budget, which used to reach 12s — past the host
// deadline, so Cursor killed the hook and cold-start users never saw
// augmentation. Assert the cap wiring exists and the effective fallback
// budget (7000 + 5000, clamped by 10000 - 2000) can never exceed the
// host budget again.
expect(source).toContain('CURSOR_HOST_BUDGET_MS = 10000');
expect(source).toContain('CURSOR_NPX_HEADROOM_MS = 2000');
expect(source).toContain('Math.min(');
expect(source).not.toMatch(/timeout:\s*timeout \+ 5000/);
});
});
// ─── Source code regression: concurrency guard (#1486) ─────────────
describe('Cursor hook concurrency guard', () => {