refactor(tmux-subagent): state-first architecture with decision engine (#1125)

* refactor(tmux-subagent): add state-first architecture with decision engine

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(tmux): add pane spawn callbacks for background and sync sessions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
justsisyphus
2026-01-26 12:02:37 +09:00
committed by GitHub
parent 3a79b8761b
commit 68aa913499
15 changed files with 1390 additions and 255 deletions
@@ -0,0 +1,130 @@
import type { WindowState, PaneAction, SpawnDecision, CapacityConfig } from "./types"
export interface SessionMapping {
sessionId: string
paneId: string
createdAt: Date
}
export function calculateCapacity(
windowWidth: number,
config: CapacityConfig
): number {
const availableForAgents = windowWidth - config.mainPaneMinWidth
if (availableForAgents <= 0) return 0
return Math.floor(availableForAgents / config.agentPaneWidth)
}
function calculateAvailableWidth(
windowWidth: number,
mainPaneMinWidth: number,
agentPaneCount: number,
agentPaneWidth: number
): number {
const usedByAgents = agentPaneCount * agentPaneWidth
return windowWidth - mainPaneMinWidth - usedByAgents
}
function findOldestSession(mappings: SessionMapping[]): SessionMapping | null {
if (mappings.length === 0) return null
return mappings.reduce((oldest, current) =>
current.createdAt < oldest.createdAt ? current : oldest
)
}
function getRightmostPane(state: WindowState): string {
if (state.agentPanes.length > 0) {
const rightmost = state.agentPanes.reduce((r, p) => (p.left > r.left ? p : r))
return rightmost.paneId
}
return state.mainPane?.paneId ?? ""
}
export function decideSpawnActions(
state: WindowState,
sessionId: string,
description: string,
config: CapacityConfig,
sessionMappings: SessionMapping[]
): SpawnDecision {
if (!state.mainPane) {
return { canSpawn: false, actions: [], reason: "no main pane found" }
}
const availableWidth = calculateAvailableWidth(
state.windowWidth,
config.mainPaneMinWidth,
state.agentPanes.length,
config.agentPaneWidth
)
if (availableWidth >= config.agentPaneWidth) {
const targetPaneId = getRightmostPane(state)
return {
canSpawn: true,
actions: [
{
type: "spawn",
sessionId,
description,
targetPaneId,
},
],
}
}
if (state.agentPanes.length > 0) {
const oldest = findOldestSession(sessionMappings)
if (oldest) {
return {
canSpawn: true,
actions: [
{ type: "close", paneId: oldest.paneId, sessionId: oldest.sessionId },
{
type: "spawn",
sessionId,
description,
targetPaneId: state.mainPane.paneId,
},
],
reason: "closing oldest session to make room",
}
}
const leftmostPane = state.agentPanes.reduce((l, p) => (p.left < l.left ? p : l))
return {
canSpawn: true,
actions: [
{ type: "close", paneId: leftmostPane.paneId, sessionId: "" },
{
type: "spawn",
sessionId,
description,
targetPaneId: state.mainPane.paneId,
},
],
reason: "closing leftmost pane to make room",
}
}
return {
canSpawn: false,
actions: [],
reason: `window too narrow: available=${availableWidth}, needed=${config.agentPaneWidth}`,
}
}
export function decideCloseAction(
state: WindowState,
sessionId: string,
sessionMappings: SessionMapping[]
): PaneAction | null {
const mapping = sessionMappings.find((m) => m.sessionId === sessionId)
if (!mapping) return null
const paneExists = state.agentPanes.some((p) => p.paneId === mapping.paneId)
if (!paneExists) return null
return { type: "close", paneId: mapping.paneId, sessionId }
}