Files
oh-my-openagent/src/features/tmux-subagent/session-created-handler.ts
YeonGyu-Kim 84a83922c3 fix: stop tracking sessions that never become ready
When session readiness times out, immediately close the spawned pane and skip tracking to prevent stale mappings from causing reopen and close anomalies.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-17 03:40:55 +09:00

175 lines
5.0 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin"
import type { TmuxConfig } from "../../config/schema"
import type { CapacityConfig, TrackedSession } from "./types"
import { log } from "../../shared"
import { queryWindowState } from "./pane-state-querier"
import { decideSpawnActions, type SessionMapping } from "./decision-engine"
import { executeActions } from "./action-executor"
import type { SessionCreatedEvent } from "./session-created-event"
type OpencodeClient = PluginInput["client"]
export interface SessionCreatedHandlerDeps {
client: OpencodeClient
tmuxConfig: TmuxConfig
serverUrl: string
sourcePaneId: string | undefined
sessions: Map<string, TrackedSession>
pendingSessions: Set<string>
isInsideTmux: () => boolean
isEnabled: () => boolean
getCapacityConfig: () => CapacityConfig
getSessionMappings: () => SessionMapping[]
waitForSessionReady: (sessionId: string) => Promise<boolean>
startPolling: () => void
}
export async function handleSessionCreated(
deps: SessionCreatedHandlerDeps,
event: SessionCreatedEvent,
): Promise<void> {
const enabled = deps.isEnabled()
log("[tmux-session-manager] onSessionCreated called", {
enabled,
tmuxConfigEnabled: deps.tmuxConfig.enabled,
isInsideTmux: deps.isInsideTmux(),
eventType: event.type,
infoId: event.properties?.info?.id,
infoParentID: event.properties?.info?.parentID,
})
if (!enabled) return
if (event.type !== "session.created") return
const info = event.properties?.info
if (!info?.id || !info?.parentID) return
const sessionId = info.id
const title = info.title ?? "Subagent"
if (deps.sessions.has(sessionId) || deps.pendingSessions.has(sessionId)) {
log("[tmux-session-manager] session already tracked or pending", { sessionId })
return
}
if (!deps.sourcePaneId) {
log("[tmux-session-manager] no source pane id")
return
}
deps.pendingSessions.add(sessionId)
try {
const state = await queryWindowState(deps.sourcePaneId)
if (!state) {
log("[tmux-session-manager] failed to query window state")
return
}
log("[tmux-session-manager] window state queried", {
windowWidth: state.windowWidth,
mainPane: state.mainPane?.paneId,
agentPaneCount: state.agentPanes.length,
agentPanes: state.agentPanes.map((p) => p.paneId),
})
const decision = decideSpawnActions(
state,
sessionId,
title,
deps.getCapacityConfig(),
deps.getSessionMappings(),
)
log("[tmux-session-manager] spawn decision", {
canSpawn: decision.canSpawn,
reason: decision.reason,
actionCount: decision.actions.length,
actions: decision.actions.map((a) => {
if (a.type === "close") return { type: "close", paneId: a.paneId }
if (a.type === "replace") {
return { type: "replace", paneId: a.paneId, newSessionId: a.newSessionId }
}
return { type: "spawn", sessionId: a.sessionId }
}),
})
if (!decision.canSpawn) {
log("[tmux-session-manager] cannot spawn", { reason: decision.reason })
return
}
const result = await executeActions(decision.actions, {
config: deps.tmuxConfig,
serverUrl: deps.serverUrl,
windowState: state,
})
for (const { action, result: actionResult } of result.results) {
if (action.type === "close" && actionResult.success) {
deps.sessions.delete(action.sessionId)
log("[tmux-session-manager] removed closed session from cache", {
sessionId: action.sessionId,
})
}
if (action.type === "replace" && actionResult.success) {
deps.sessions.delete(action.oldSessionId)
log("[tmux-session-manager] removed replaced session from cache", {
oldSessionId: action.oldSessionId,
newSessionId: action.newSessionId,
})
}
}
if (!result.success || !result.spawnedPaneId) {
log("[tmux-session-manager] spawn failed", {
success: result.success,
results: result.results.map((r) => ({
type: r.action.type,
success: r.result.success,
error: r.result.error,
})),
})
return
}
const sessionReady = await deps.waitForSessionReady(sessionId)
if (!sessionReady) {
log("[tmux-session-manager] session not ready after timeout, closing spawned pane", {
sessionId,
paneId: result.spawnedPaneId,
})
await executeActions(
[{ type: "close", paneId: result.spawnedPaneId, sessionId }],
{
config: deps.tmuxConfig,
serverUrl: deps.serverUrl,
windowState: state,
},
)
return
}
const now = Date.now()
deps.sessions.set(sessionId, {
sessionId,
paneId: result.spawnedPaneId,
description: title,
createdAt: new Date(now),
lastSeenAt: new Date(now),
})
log("[tmux-session-manager] pane spawned and tracked", {
sessionId,
paneId: result.spawnedPaneId,
sessionReady,
})
deps.startPolling()
} finally {
deps.pendingSessions.delete(sessionId)
}
}