Files
hapi/cli/src/modules/common/cursorModelsSharedCache.ts
T
c69a88afae fix(cursor): close ACP list-models race and false exit 143 window (#1518)
* fix(cursor): close ACP list-models race and false exit 143 window

Register the agent-acp-active guard before spawn, hold it until stdio
close (not bare exit), record the ACP child PID, and align lock/cache
home with resolveHapiHomeDir so runner and session children agree.
Richer exit attribution distinguishes live-PID transport disruption
from confirmed child death. Fixes residual #1472 after #835.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(cursor): isolate ACP guard teardown from ~/.hapi

Reset afterEach under the temp HAPI_HOME only, and restore the
isolated home before teardown in the unset-HAPI_HOME case, so tests
cannot wipe a live agent-acp-active lock. Use distinct child PIDs in
registration tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): publish ACP lock pid before count

Fail-closed reservation order: write pids/<hostPid> before count so
concurrent reconcile cannot treat a mid-register lock as stale and
clear it for list-models. Keep a short mtime grace only when pids/ is
missing (mkdir gap). Regression covers mid-publish readers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): keep empty pids/ ACP reservation fail-closed

Between mkdir(pids) and the host pid writeFile, reconcile could see
liveCount=0 and clear the lock. Keep that window when count is still
absent and the lock is fresh; re-scan for pids published mid-reconcile.
Regression hooks the mkdir/write gap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): keep ACP lock across last-unregister publish race

Write a short-lived registering marker before pids/count so empty pids
with leftover count cannot erase a concurrent mid-addLockPid reservation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): use per-pid ACP registering markers

Crash/reboot must not pin list-models forever on a bare registering
file; prune dead owners and only keep live registrar PIDs.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 09:55:26 +08:00

56 lines
1.7 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import type { CursorModelsResponse } from '@hapi/protocol/apiTypes';
import { resolveHapiHomeDir } from '@/configuration';
function getHapiHomeDir(): string {
return resolveHapiHomeDir();
}
function getSharedCachePath(): string {
return join(getHapiHomeDir(), 'cache', 'cursor-models.json');
}
function isUsableModelsResponse(response: CursorModelsResponse | null): response is CursorModelsResponse {
return Boolean(
response?.success
&& (response.availableModels?.length ?? 0) > 0
);
}
/** Cross-process catalog for New Session while an ACP lock blocks `agent --list-models`. */
export function readSharedCursorModelsCache(): CursorModelsResponse | null {
const path = getSharedCachePath();
if (!existsSync(path)) {
return null;
}
try {
const parsed = JSON.parse(readFileSync(path, 'utf8')) as CursorModelsResponse;
return isUsableModelsResponse(parsed) ? parsed : null;
} catch {
return null;
}
}
export function writeSharedCursorModelsCache(response: CursorModelsResponse): void {
if (!isUsableModelsResponse(response)) {
return;
}
const path = getSharedCachePath();
try {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify(response), 'utf8');
} catch {
// Best effort — in-process cache still works in the session child.
}
}
export function _resetSharedCursorModelsCacheForTests(): void {
const path = getSharedCachePath();
if (existsSync(path)) {
rmSync(path, { force: true });
}
}