Skip to content

Commit af89f1c

Browse files
chore: apply pr-reviewer fixes for #140
1 parent 8474e5e commit af89f1c

3 files changed

Lines changed: 88 additions & 6 deletions

File tree

src/main/broker.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,42 @@ type MockClient = {
3232
brokerPid?: number
3333
baseUrl?: string
3434
agentNames: string[]
35+
agentRuntimes: Record<string, 'pty' | 'headless'>
3536
}
3637

3738
const mock = vi.hoisted(() => {
3839
function createMockClient(agentNames: string[] = []): MockClient {
40+
const agentRuntimes = Object.fromEntries(agentNames.map((name) => [name, 'pty' as const]))
3941
const client: MockClient = {
4042
agentNames: [...agentNames],
43+
agentRuntimes,
4144
getSession: vi.fn(async () => ({})),
42-
listAgents: vi.fn(async () => client.agentNames.map((name) => ({ name, runtime: 'pty', channels: [] }))),
45+
listAgents: vi.fn(async () => client.agentNames.map((name) => ({
46+
name,
47+
runtime: client.agentRuntimes[name] || 'pty',
48+
channels: []
49+
}))),
4350
getInboundDeliveryMode: vi.fn(async () => 'passthrough'),
4451
spawnPty: vi.fn(async (input: { name: string }) => {
4552
client.agentNames.push(input.name)
53+
client.agentRuntimes[input.name] = 'pty'
4654
return { name: input.name, runtime: 'pty' }
4755
}),
4856
spawnCli: vi.fn(async (input: { name: string }) => {
4957
client.agentNames.push(input.name)
58+
client.agentRuntimes[input.name] = 'headless'
5059
return { name: input.name, runtime: 'headless' }
5160
}),
5261
setInboundDeliveryMode: vi.fn(async (_name: string, mode: string) => ({ mode, flushed: 0 })),
5362
snapshot: vi.fn(async () => ({ rows: 24, cols: 80, cursor: { x: 0, y: 0 }, screen: 'aGVsbG8=' })),
5463
resizePty: vi.fn(async () => undefined),
5564
getPending: vi.fn(async () => []),
5665
getStatus: vi.fn(async () => ({
57-
agents: client.agentNames.map((name) => ({ name, runtime: 'pty', channels: [] })),
66+
agents: client.agentNames.map((name) => ({
67+
name,
68+
runtime: client.agentRuntimes[name] || 'pty',
69+
channels: []
70+
})),
5871
pending_delivery_count: 0
5972
})),
6073
onEvent: vi.fn(() => () => undefined),
@@ -542,6 +555,60 @@ describe('BrokerManager local + cloud coexistence', () => {
542555
await manager.shutdown()
543556
})
544557

558+
it('spawns OpenCode with headless runtime and skips PTY attach operations', async () => {
559+
const manager = new BrokerManager()
560+
const local = await startLocal(manager, [])
561+
562+
const spawned = await manager.spawnAgent(PROJECT_ID, { name: 'opencode-1', cli: 'opencode' })
563+
const attached = await manager.attachTerminal(PROJECT_ID, {
564+
name: spawned.name,
565+
mode: 'passthrough',
566+
rows: 24,
567+
cols: 80
568+
})
569+
570+
expect(spawned).toEqual({ name: 'opencode-1', runtime: 'headless' })
571+
expect(local.spawnCli).toHaveBeenCalledWith(expect.objectContaining({
572+
name: 'opencode-1',
573+
cli: expect.stringContaining('opencode'),
574+
transport: 'headless'
575+
}))
576+
expect(local.spawnPty).not.toHaveBeenCalled()
577+
expect(local.resizePty).not.toHaveBeenCalled()
578+
expect(local.snapshot).not.toHaveBeenCalled()
579+
expect(attached).toEqual({
580+
name: 'opencode-1',
581+
mode: 'auto_inject',
582+
previousMode: 'passthrough',
583+
pending: 0,
584+
runtime: 'headless'
585+
})
586+
587+
await manager.shutdown()
588+
})
589+
590+
it('remembers headless runtime from discovered agents before attaching', async () => {
591+
const manager = new BrokerManager()
592+
const local = await startLocal(manager, [])
593+
local.listAgents.mockResolvedValue([
594+
{ name: 'opencode-1', runtime: 'headless', channels: [] }
595+
])
596+
597+
const attached = await manager.attachTerminal(PROJECT_ID, {
598+
name: 'opencode-1',
599+
mode: 'passthrough',
600+
rows: 24,
601+
cols: 80
602+
})
603+
604+
expect(local.setInboundDeliveryMode).toHaveBeenCalledWith('opencode-1', 'auto_inject')
605+
expect(local.resizePty).not.toHaveBeenCalled()
606+
expect(local.snapshot).not.toHaveBeenCalled()
607+
expect(attached.runtime).toBe('headless')
608+
609+
await manager.shutdown()
610+
})
611+
545612
it('coalesces concurrent duplicate spawn requests', async () => {
546613
const manager = new BrokerManager()
547614
const local = await startLocal(manager, [])

src/main/broker.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ export interface AttachTerminalResult {
323323
mode: InboundDeliveryMode
324324
previousMode?: InboundDeliveryMode
325325
pending: number
326+
runtime?: 'pty' | 'headless'
326327
snapshot?: {
327328
rows: number
328329
cols: number
@@ -2050,7 +2051,6 @@ export class BrokerManager {
20502051
} else if (event.kind === 'agent_exit' && event.name) {
20512052
this.closeInputStream(this.getInputStreamKey(sessionKey, event.name), 1000, 'agent closed')
20522053
this.forgetAgentSession(event.name, sessionKey)
2053-
this.agentRuntimes.delete(event.name)
20542054
void client.release(event.name, 'agent exit').catch((err) => {
20552055
if (!isMissingAgentError(err)) {
20562056
console.warn(`[broker] Failed to release exited agent ${event.name}:`, err)
@@ -2059,7 +2059,6 @@ export class BrokerManager {
20592059
} else if ((event.kind === 'agent_exited' || event.kind === 'agent_released') && event.name) {
20602060
this.closeInputStream(this.getInputStreamKey(sessionKey, event.name), 1000, 'agent closed')
20612061
this.forgetAgentSession(event.name, sessionKey)
2062-
this.agentRuntimes.delete(event.name)
20632062
} else if ('name' in event && typeof event.name === 'string') {
20642063
this.rememberAgentSession(event.name, sessionKey)
20652064
} else if ('from' in event && typeof event.from === 'string') {
@@ -2193,12 +2192,19 @@ export class BrokerManager {
21932192
this.agentSessions.set(name, sessionKeys)
21942193
}
21952194

2195+
private rememberAgentRuntime(agent: Pick<ListAgent, 'name' | 'runtime'>): void {
2196+
if (agent.runtime === 'pty' || agent.runtime === 'headless') {
2197+
this.agentRuntimes.set(agent.name, agent.runtime)
2198+
}
2199+
}
2200+
21962201
private forgetAgentSession(name: string, sessionKey: string): void {
21972202
const sessionKeys = this.agentSessions.get(name)
21982203
if (!sessionKeys) return
21992204
sessionKeys.delete(sessionKey)
22002205
if (sessionKeys.size === 0) {
22012206
this.agentSessions.delete(name)
2207+
this.agentRuntimes.delete(name)
22022208
}
22032209
}
22042210

@@ -2554,7 +2560,11 @@ export class BrokerManager {
25542560
while (Date.now() < deadline) {
25552561
try {
25562562
const agents = await session.client.listAgents()
2557-
if (agents.some((agent) => agent.name === name)) return true
2563+
const agent = agents.find((candidate) => candidate.name === name)
2564+
if (agent) {
2565+
this.rememberAgentRuntime(agent)
2566+
return true
2567+
}
25582568
} catch {
25592569
// Transient broker errors during the wait are fine — keep polling
25602570
// until the deadline; if the broker is genuinely down the downstream
@@ -2586,8 +2596,10 @@ export class BrokerManager {
25862596
while (Date.now() < deadline) {
25872597
for (const candidate of candidates) {
25882598
const agents = await candidate.client.listAgents().catch(() => null)
2589-
if (agents?.some((agent) => agent.name === name)) {
2599+
const agent = agents?.find((entry) => entry.name === name)
2600+
if (agent) {
25902601
this.rememberAgentSession(name, sessionKeyFor(candidate))
2602+
this.rememberAgentRuntime(agent)
25912603
return { session: candidate, registered: true }
25922604
}
25932605
}
@@ -3281,6 +3293,7 @@ export class BrokerManager {
32813293
const sessionKey = sessionKeyFor(session)
32823294
for (const agent of agents) {
32833295
this.rememberAgentSession(agent.name, sessionKey)
3296+
this.rememberAgentRuntime(agent)
32843297
}
32853298
const brokerKind = session.cloudSandboxId ? ('cloud' as const) : ('local' as const)
32863299
return Promise.all(
@@ -3461,6 +3474,7 @@ export class BrokerManager {
34613474
sessionKeys.delete(sessionKey)
34623475
if (sessionKeys.size === 0) {
34633476
this.agentSessions.delete(agentName)
3477+
this.agentRuntimes.delete(agentName)
34643478
}
34653479
}
34663480
}

src/shared/types/ipc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ export interface BrokerAttachTerminalResult {
369369
mode: InboundDeliveryMode
370370
previousMode?: InboundDeliveryMode
371371
pending: number
372+
runtime?: 'pty' | 'headless'
372373
snapshot?: {
373374
rows: number
374375
cols: number

0 commit comments

Comments
 (0)