mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(agent-teams): replace panes only after confirmed stop
This commit is contained in:
@@ -189,7 +189,7 @@ describe('ClaudeAgentTeamsService', () => {
|
||||
expect(api.closeTerminal).toHaveBeenLastCalledWith('teammate-2')
|
||||
})
|
||||
|
||||
it('keeps the placeholder handle when the respawn split fails', async () => {
|
||||
it('removes the pane when replacement split fails after a confirmed placeholder stop', async () => {
|
||||
const { service, teamId, token, leaderPane, api } = createServiceWithLeader()
|
||||
const request = (argv: string[], envPane = leaderPane) =>
|
||||
service.handleTmuxCompat({ teamId, token, envPane, argv }, api)
|
||||
@@ -212,10 +212,10 @@ describe('ClaudeAgentTeamsService', () => {
|
||||
request(['respawn-pane', '-k', '-t', '%2', '--', 'claude --agent-id a'])
|
||||
).resolves.toMatchObject({ ok: false, exitCode: 1 })
|
||||
|
||||
// the placeholder terminal is left intact and the fake pane id still resolves.
|
||||
expect(api.closeTerminal).not.toHaveBeenCalled()
|
||||
await request(['kill-pane', '-t', '%2'])
|
||||
expect(api.closeTerminal).toHaveBeenCalledWith('teammate-1')
|
||||
await expect(
|
||||
request(['list-panes', '-t', 'orca:0', '-F', '#{pane_id}'])
|
||||
).resolves.toMatchObject({ stdout: '%1\n' })
|
||||
})
|
||||
|
||||
it('keeps a pane registered when its process stop is unconfirmed', async () => {
|
||||
@@ -239,7 +239,7 @@ describe('ClaudeAgentTeamsService', () => {
|
||||
).resolves.toMatchObject({ stdout: '%1\n%2\n' })
|
||||
})
|
||||
|
||||
it('keeps the replacement addressable when the retired placeholder stop is unconfirmed', async () => {
|
||||
it('does not launch a replacement when the placeholder stop is unconfirmed', async () => {
|
||||
const { service, teamId, token, leaderPane, api, splitCalls } = createServiceWithLeader()
|
||||
const request = (argv: string[], envPane = leaderPane) =>
|
||||
service.handleTmuxCompat({ teamId, token, envPane, argv }, api)
|
||||
@@ -256,14 +256,15 @@ describe('ClaudeAgentTeamsService', () => {
|
||||
request(['respawn-pane', '-k', '-t', '%2', '--', 'claude --agent-id a'])
|
||||
).resolves.toMatchObject({ ok: false, exitCode: 1 })
|
||||
expect(api.closeTerminal).toHaveBeenNthCalledWith(1, 'teammate-1')
|
||||
expect(splitCalls).toHaveLength(1)
|
||||
|
||||
await expect(
|
||||
request(['respawn-pane', '-k', '-t', '%2', '--', 'claude --agent-id a'])
|
||||
).resolves.toMatchObject({ ok: false, exitCode: 1 })
|
||||
expect(splitCalls).toHaveLength(2)
|
||||
expect(splitCalls).toHaveLength(1)
|
||||
|
||||
await request(['kill-pane', '-t', '%2'])
|
||||
expect(api.closeTerminal).toHaveBeenLastCalledWith('teammate-2')
|
||||
expect(api.closeTerminal).toHaveBeenLastCalledWith('teammate-1')
|
||||
})
|
||||
|
||||
it('refuses to respawn the leader pane', async () => {
|
||||
|
||||
@@ -164,31 +164,25 @@ export class ClaudeAgentTeamsTmuxDispatcher {
|
||||
const origin =
|
||||
(pane.splitFromPane ? team.panes.get(pane.splitFromPane) : undefined) ??
|
||||
team.panes.get(team.leaderPane)!
|
||||
// Why: create the replacement before destroying the placeholder so a failed
|
||||
// split leaves the fake pane id pointing at a still-live terminal; on cleanup
|
||||
// failure, discard the new split and keep the placeholder registered.
|
||||
const previousHandle = pane.handle
|
||||
const split = await api.splitTerminal(origin.handle, {
|
||||
direction: pane.splitDirection ?? 'horizontal',
|
||||
command,
|
||||
env: paneEnv(team, pane.fakePaneId),
|
||||
envToDelete: ['TERM_PROGRAM'],
|
||||
activate: false
|
||||
})
|
||||
const close = await api.closeTerminal(previousHandle)
|
||||
if (!close.ptyKilled) {
|
||||
pane.respawnBlockedReason = describeUnconfirmedAgentStop(close)
|
||||
throw new Error(pane.respawnBlockedReason)
|
||||
}
|
||||
try {
|
||||
const close = await api.closeTerminal(previousHandle)
|
||||
if (!close.ptyKilled) {
|
||||
pane.handle = split.handle
|
||||
pane.respawnBlockedReason = describeUnconfirmedAgentStop(close)
|
||||
throw new Error(pane.respawnBlockedReason)
|
||||
}
|
||||
const split = await api.splitTerminal(origin.handle, {
|
||||
direction: pane.splitDirection ?? 'horizontal',
|
||||
command,
|
||||
env: paneEnv(team, pane.fakePaneId),
|
||||
envToDelete: ['TERM_PROGRAM'],
|
||||
activate: false
|
||||
})
|
||||
pane.handle = split.handle
|
||||
} catch (error) {
|
||||
if (pane.handle === previousHandle) {
|
||||
await api.closeTerminal(split.handle).catch(() => {})
|
||||
}
|
||||
this.removePane(team, pane)
|
||||
throw error
|
||||
}
|
||||
pane.handle = split.handle
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -281,13 +275,17 @@ export class ClaudeAgentTeamsTmuxDispatcher {
|
||||
if (!close.ptyKilled) {
|
||||
throw new Error(describeUnconfirmedAgentStop(close))
|
||||
}
|
||||
this.removePane(team, pane)
|
||||
return ''
|
||||
}
|
||||
|
||||
private removePane(team: AgentTeam, pane: TeamPane): void {
|
||||
team.panes.delete(pane.fakePaneId)
|
||||
team.paneOrder = team.paneOrder.filter((id) => id !== pane.fakePaneId)
|
||||
if (team.mainVertical?.lastColumnPane === pane.fakePaneId) {
|
||||
team.mainVertical.lastColumnPane =
|
||||
[...team.paneOrder].toReversed().find((id) => id !== team.leaderPane) ?? null
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
private async lastPane(
|
||||
|
||||
Reference in New Issue
Block a user