Skip to content

Commit ad038bb

Browse files
swear01cursoragent
andauthored
fix(cursor): merge SKU catalog under ACP lock and refcount agent guard (#835)
Fixes incomplete cliModelSkus while agent acp holds the CLI lock (#831) and replace single-pid ACP lock with cross-process refcount (#832). Web picker merges machine/session catalogs and waits for SKU readiness before showing variant labels. Fixes #831 Fixes #832 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6d2d0d4 commit ad038bb

15 files changed

Lines changed: 890 additions & 64 deletions

cli/src/agent/backends/acp/agentCliGuard.test.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ function lockDir(): string {
1515
return join(testHome, 'locks', 'agent-acp-active');
1616
}
1717

18+
function writeTestAcpLock(args: { count: number; pids: number[] }): void {
19+
const dir = lockDir();
20+
mkdirSync(join(dir, 'pids'), { recursive: true });
21+
writeFileSync(join(dir, 'count'), String(args.count), 'utf8');
22+
for (const pid of args.pids) {
23+
writeFileSync(join(dir, 'pids', String(pid)), String(pid), 'utf8');
24+
}
25+
}
26+
27+
function writeLegacyAcpLock(pid: number): void {
28+
const dir = lockDir();
29+
mkdirSync(dir, { recursive: true });
30+
writeFileSync(join(dir, 'pid'), String(pid), 'utf8');
31+
}
32+
1833
describe('agentCliGuard', () => {
1934
const previousHome = process.env.HAPI_HOME;
2035

@@ -35,32 +50,72 @@ describe('agentCliGuard', () => {
3550
expect(isAgentAcpTransportActive()).toBe(false);
3651
});
3752

38-
test('clears stale cross-process lock when pid is not running', () => {
53+
test('keeps cross-process lock until the last transport unregisters', () => {
3954
process.env.HAPI_HOME = testHome;
40-
const dir = lockDir();
41-
mkdirSync(dir, { recursive: true });
42-
writeFileSync(join(dir, 'pid'), '99999999');
55+
registerActiveAcpTransport();
56+
registerActiveAcpTransport();
57+
58+
unregisterActiveAcpTransport();
59+
expect(isAgentAcpTransportActive()).toBe(true);
60+
expect(existsSync(lockDir())).toBe(true);
4361

62+
unregisterActiveAcpTransport();
4463
expect(isAgentAcpTransportActive()).toBe(false);
45-
expect(existsSync(dir)).toBe(false);
64+
expect(existsSync(lockDir())).toBe(false);
4665
});
4766

48-
test('keeps lock when pid file points at a live process', () => {
67+
test('leaves refcount at one after the first of two in-process unregisters', () => {
4968
process.env.HAPI_HOME = testHome;
69+
registerActiveAcpTransport();
70+
registerActiveAcpTransport();
71+
5072
const dir = lockDir();
51-
mkdirSync(dir, { recursive: true });
52-
writeFileSync(join(dir, 'pid'), String(process.pid));
73+
unregisterActiveAcpTransport();
5374

5475
expect(isAgentAcpTransportActive()).toBe(true);
5576
expect(existsSync(dir)).toBe(true);
77+
expect(existsSync(join(dir, 'pids', String(process.pid)))).toBe(true);
78+
});
79+
80+
test('clears stale cross-process lock when pid is not running', () => {
81+
process.env.HAPI_HOME = testHome;
82+
writeLegacyAcpLock(99999999);
83+
84+
expect(isAgentAcpTransportActive()).toBe(false);
85+
expect(existsSync(lockDir())).toBe(false);
86+
});
87+
88+
test('keeps legacy lock when pid file points at a live process', () => {
89+
process.env.HAPI_HOME = testHome;
90+
writeLegacyAcpLock(process.pid);
91+
92+
expect(isAgentAcpTransportActive()).toBe(true);
93+
expect(existsSync(lockDir())).toBe(true);
5694
});
5795

58-
test('clears lock when pid file is missing or invalid', () => {
96+
test('clears refcount lock when pid entries are missing or invalid', () => {
5997
process.env.HAPI_HOME = testHome;
6098
const dir = lockDir();
6199
mkdirSync(dir, { recursive: true });
100+
writeFileSync(join(dir, 'count'), '1', 'utf8');
62101

63102
expect(isAgentAcpTransportActive()).toBe(false);
64103
expect(existsSync(dir)).toBe(false);
65104
});
105+
106+
test('clears refcount lock when all pid entries are stale', () => {
107+
process.env.HAPI_HOME = testHome;
108+
writeTestAcpLock({ count: 2, pids: [99999998, 99999999] });
109+
110+
expect(isAgentAcpTransportActive()).toBe(false);
111+
expect(existsSync(lockDir())).toBe(false);
112+
});
113+
114+
test('reconciles refcount lock down to live pid entries', () => {
115+
process.env.HAPI_HOME = testHome;
116+
writeTestAcpLock({ count: 3, pids: [process.pid, 99999999] });
117+
118+
expect(isAgentAcpTransportActive()).toBe(true);
119+
expect(existsSync(lockDir())).toBe(true);
120+
});
66121
});

cli/src/agent/backends/acp/agentCliGuard.ts

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
readdirSync,
5+
readFileSync,
6+
rmSync,
7+
writeFileSync
8+
} from 'node:fs';
29
import { join } from 'node:path';
310
import { tmpdir } from 'node:os';
411

@@ -17,6 +24,10 @@ function getAcpLockDir(): string {
1724
return join(home, 'locks', 'agent-acp-active');
1825
}
1926

27+
function getPidsDir(lockDir: string): string {
28+
return join(lockDir, 'pids');
29+
}
30+
2031
function readLockPid(lockDir: string): number | null {
2132
const pidPath = join(lockDir, 'pid');
2233
if (!existsSync(pidPath)) {
@@ -35,6 +46,46 @@ function readLockPid(lockDir: string): number | null {
3546
}
3647
}
3748

49+
function readLockCount(lockDir: string): number {
50+
const countPath = join(lockDir, 'count');
51+
if (!existsSync(countPath)) {
52+
return 0;
53+
}
54+
55+
try {
56+
const raw = readFileSync(countPath, 'utf8').trim();
57+
const count = Number(raw);
58+
if (!Number.isInteger(count) || count < 0) {
59+
return 0;
60+
}
61+
return count;
62+
} catch {
63+
return 0;
64+
}
65+
}
66+
67+
function writeLockCount(lockDir: string, count: number): void {
68+
writeFileSync(join(lockDir, 'count'), String(Math.max(0, count)), 'utf8');
69+
}
70+
71+
function addLockPid(lockDir: string, pid: number): void {
72+
const pidsDir = getPidsDir(lockDir);
73+
mkdirSync(pidsDir, { recursive: true });
74+
writeFileSync(join(pidsDir, String(pid)), String(pid), 'utf8');
75+
}
76+
77+
function removeLockPid(lockDir: string, pid: number): void {
78+
try {
79+
rmSync(join(getPidsDir(lockDir), String(pid)), { force: true });
80+
} catch {
81+
// Best effort.
82+
}
83+
}
84+
85+
function isLegacyLock(lockDir: string): boolean {
86+
return existsSync(join(lockDir, 'pid')) && !existsSync(join(lockDir, 'count'));
87+
}
88+
3889
function isProcessAlive(pid: number): boolean {
3990
try {
4091
process.kill(pid, 0);
@@ -58,44 +109,117 @@ function removeAcpLockDir(): void {
58109
}
59110
}
60111

112+
function reconcileRefcountLock(lockDir: string): boolean {
113+
const pidsDir = getPidsDir(lockDir);
114+
if (!existsSync(pidsDir)) {
115+
removeAcpLockDir();
116+
return false;
117+
}
118+
119+
let liveCount = 0;
120+
for (const entry of readdirSync(pidsDir)) {
121+
const pid = Number(entry);
122+
if (!Number.isInteger(pid) || pid <= 0) {
123+
try {
124+
rmSync(join(pidsDir, entry), { force: true });
125+
} catch {
126+
// Best effort.
127+
}
128+
continue;
129+
}
130+
131+
if (isProcessAlive(pid)) {
132+
liveCount += 1;
133+
continue;
134+
}
135+
136+
try {
137+
rmSync(join(pidsDir, entry), { force: true });
138+
} catch {
139+
// Best effort.
140+
}
141+
}
142+
143+
if (liveCount <= 0) {
144+
removeAcpLockDir();
145+
return false;
146+
}
147+
148+
writeLockCount(lockDir, liveCount);
149+
return true;
150+
}
151+
61152
/** Remove lock directories left behind by SIGKILL / crash / reboot. */
62153
function clearStaleAcpLockIfNeeded(): void {
63154
const lockDir = getAcpLockDir();
64155
if (!existsSync(lockDir)) {
65156
return;
66157
}
67158

68-
const pid = readLockPid(lockDir);
69-
if (pid === null || !isProcessAlive(pid)) {
70-
removeAcpLockDir();
159+
if (isLegacyLock(lockDir)) {
160+
const pid = readLockPid(lockDir);
161+
if (pid === null || !isProcessAlive(pid)) {
162+
removeAcpLockDir();
163+
}
164+
return;
71165
}
166+
167+
reconcileRefcountLock(lockDir);
72168
}
73169

74170
export function registerActiveAcpTransport(): void {
75171
activeAcpTransportCount += 1;
76172
const lockDir = getAcpLockDir();
77173
try {
78174
mkdirSync(lockDir, { recursive: true });
79-
writeFileSync(join(lockDir, 'pid'), String(process.pid));
175+
writeLockCount(lockDir, readLockCount(lockDir) + 1);
176+
addLockPid(lockDir, process.pid);
80177
} catch {
81178
// Another process may have created the lock; in-process guard still applies.
82179
}
83180
}
84181

85182
export function unregisterActiveAcpTransport(): void {
86183
activeAcpTransportCount = Math.max(0, activeAcpTransportCount - 1);
87-
if (activeAcpTransportCount > 0) {
184+
185+
const lockDir = getAcpLockDir();
186+
if (!existsSync(lockDir)) {
88187
return;
89188
}
90-
removeAcpLockDir();
189+
190+
if (isLegacyLock(lockDir)) {
191+
if (activeAcpTransportCount <= 0) {
192+
removeAcpLockDir();
193+
}
194+
return;
195+
}
196+
197+
try {
198+
if (activeAcpTransportCount <= 0) {
199+
removeLockPid(lockDir, process.pid);
200+
}
201+
reconcileRefcountLock(lockDir);
202+
} catch {
203+
// Best effort.
204+
}
91205
}
92206

93207
export function isAgentAcpTransportActive(): boolean {
94208
if (activeAcpTransportCount > 0) {
95209
return true;
96210
}
97211
clearStaleAcpLockIfNeeded();
98-
return existsSync(getAcpLockDir());
212+
const lockDir = getAcpLockDir();
213+
if (!existsSync(lockDir)) {
214+
return false;
215+
}
216+
217+
if (isLegacyLock(lockDir)) {
218+
const pid = readLockPid(lockDir);
219+
return pid !== null && isProcessAlive(pid);
220+
}
221+
222+
return readLockCount(lockDir) > 0;
99223
}
100224

101225
export function _resetAgentCliGuardForTests(): void {

cli/src/cursor/cursorAcpRemoteLauncher.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import { setCursorAcpModelsSnapshot } from './utils/cursorAcpModelsBridge';
1717
import { buildCursorModelsSnapshotFromAcp } from './utils/cursorAcpModelsSnapshot';
1818
import { CursorExtensionAdapter } from './utils/cursorExtensionAdapter';
1919
import { applyCursorAcpMode, applyCursorAcpModel, wireIdForCursorSessionState } from './utils/cursorModeConfig';
20-
import { seedCursorModelsCache } from '@/modules/common/cursorModels';
20+
import { buildCursorModelsSeedPayload, seedCursorModelsCache } from '@/modules/common/cursorModels';
21+
import { readSharedCursorModelsCache } from '@/modules/common/cursorModelsSharedCache';
2122
import type { AcpSdkBackend } from '@/agent/backends/acp';
2223

2324
class CursorAcpRemoteLauncher extends RemoteLauncherBase {
@@ -442,8 +443,9 @@ function syncCursorModelsFromAcp(backend: AcpSdkBackend, acpSessionId: string):
442443
return;
443444
}
444445

446+
const payload = buildCursorModelsSeedPayload(snapshot, readSharedCursorModelsCache());
445447
setCursorAcpModelsSnapshot(snapshot);
446-
seedCursorModelsCache({ success: true, ...snapshot });
448+
seedCursorModelsCache(payload);
447449
}
448450

449451
function toAcpMcpServers(config: Record<string, { command: string; args: string[] }>): McpServerStdio[] {

0 commit comments

Comments
 (0)