mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-27 22:51:43 +00:00
* fix(cursor): exclusive agent spawn lease for list-models vs ACP (#1520) Add a proper-lockfile spawn lease beside agent-acp-active so model probes and ACP transport acquire mutual exclusion atomically before spawning agent children, closing the post-#1518 check-then-act overlap window. Fixes #1520 Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): fix markdown-a test HappyChatContext mock for typecheck Adds showSessionSummaryInChat to chatContext() so CI typecheck passes on the PR branch (pre-existing main breakage unrelated to #1520). Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "chore(web): fix markdown-a test HappyChatContext mock for typecheck" This reverts commit 4973f321a31fda772e8990ea6cda20517ca906aa. * fix(cursor): scope spawn lease to agent spawn window only (#1520) Hold agent-cli.spawn only around spawn('agent') in AcpStdioTransport, not for the full ACP session. Restores N concurrent cursor sessions per host; list-models probe lease unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): tighten spawn lease lifecycle for babysit (#1520) Acquire spawn lease before ACP marker publish; unregister on spawn failure. Hold list-models probe lease until child exit on timeout. Add missing showSessionSummaryInChat to markdown-a test mock (unblocks CI typecheck). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): re-check ACP marker after spawn lease acquire (#1520) Close check-then-act window where ACP could publish its marker between the inactive guard read and list-models spawn. Add regression test. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cursor): fix ACP-after-acquire mock call order (#1520) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): async spawn lease + force-kill probe timeout (#1520) Add acquireAgentCliSpawnLease (setTimeout yields) for ACP create path; AcpStdioTransport.create() async factory. Probe timeout uses killProcessByChildProcess(force) while holding lease until child exit. Addresses Bugbot Majors: session-lifetime mutex (fe07b708d), probe lease release, post-acquire re-check (137baa779), sync loop starvation, timeout escalation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(acp): coalesce concurrent initialize() transport spawns (#1520) Await shared bootstrapTransport promise so overlapping initialize calls do not spawn duplicate ACP children while create() is in flight. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
import { describe, expect, test, afterEach } from 'bun:test'
|
|
import { existsSync, mkdtempSync, mkdirSync, utimesSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import {
|
|
acquireAgentCliSpawnLease,
|
|
acquireAgentCliSpawnLeaseSync,
|
|
getAgentCliSpawnLockTarget,
|
|
releaseAgentCliSpawnLeaseFromAcpRegisterSync,
|
|
releaseAgentCliSpawnLeaseSync,
|
|
tryAcquireAgentCliSpawnLeaseSync,
|
|
_resetAgentCliSpawnLeaseForTests,
|
|
} from './agentCliSpawnLease'
|
|
|
|
describe('agentCliSpawnLease', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'hapi-agent-cli-spawn-lease-'))
|
|
|
|
afterEach(() => {
|
|
_resetAgentCliSpawnLeaseForTests(dir)
|
|
})
|
|
|
|
test('exclusive lease blocks a second non-blocking acquirer', () => {
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(true)
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(false)
|
|
releaseAgentCliSpawnLeaseSync()
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(true)
|
|
releaseAgentCliSpawnLeaseSync()
|
|
})
|
|
|
|
test('ACP register depth shares one lease until the last unregister', () => {
|
|
acquireAgentCliSpawnLeaseSync(dir)
|
|
acquireAgentCliSpawnLeaseSync(dir)
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(false)
|
|
releaseAgentCliSpawnLeaseFromAcpRegisterSync()
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(false)
|
|
releaseAgentCliSpawnLeaseFromAcpRegisterSync()
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(true)
|
|
releaseAgentCliSpawnLeaseSync()
|
|
})
|
|
|
|
test('ACP blocking acquire succeeds after probe releases the lease', () => {
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(true)
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(false)
|
|
releaseAgentCliSpawnLeaseSync()
|
|
acquireAgentCliSpawnLeaseSync(dir)
|
|
releaseAgentCliSpawnLeaseFromAcpRegisterSync()
|
|
})
|
|
|
|
test('async blocking acquire yields while same-process probe holds lease', async () => {
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(true)
|
|
const acquirePromise = acquireAgentCliSpawnLease(dir)
|
|
let probeReleased = false
|
|
setTimeout(() => {
|
|
releaseAgentCliSpawnLeaseSync()
|
|
probeReleased = true
|
|
}, 50)
|
|
await acquirePromise
|
|
expect(probeReleased).toBe(true)
|
|
releaseAgentCliSpawnLeaseFromAcpRegisterSync()
|
|
})
|
|
|
|
test('anchors spawn lease beside the agent-acp-active marker directory', () => {
|
|
const target = getAgentCliSpawnLockTarget(dir)
|
|
expect(target.endsWith(join('locks', 'agent-cli.spawn'))).toBe(true)
|
|
acquireAgentCliSpawnLeaseSync(dir)
|
|
expect(existsSync(join(dir, 'locks', 'agent-acp-active'))).toBe(false)
|
|
expect(existsSync(target)).toBe(true)
|
|
releaseAgentCliSpawnLeaseFromAcpRegisterSync()
|
|
})
|
|
|
|
test('reclaims a stale spawn lease left by a crashed holder', () => {
|
|
const target = getAgentCliSpawnLockTarget(dir)
|
|
const lockDir = spawnLockfilePath(target)
|
|
mkdirSync(lockDir)
|
|
const past = new Date(Date.now() - 300_000)
|
|
utimesSync(lockDir, past, past)
|
|
|
|
expect(tryAcquireAgentCliSpawnLeaseSync(dir)).toBe(true)
|
|
releaseAgentCliSpawnLeaseSync()
|
|
expect(existsSync(lockDir)).toBe(false)
|
|
})
|
|
})
|
|
|
|
function spawnLockfilePath(lockTarget: string): string {
|
|
return `${lockTarget}.hapi.lock`
|
|
}
|