bd3928e158
`createManagers` unconditionally called `markServerRunningInProcess()` whenever `tmuxConfig.enabled` was true, which made `isServerRunning()` short-circuit to `true` for *any* serverUrl — bypassing the guard in `createTeamLayout` that is supposed to skip pane creation when the opencode server is not running. When a user launches vanilla `opencode` (no `opencode serve` / `opencode web`), `ctx.serverUrl` is undefined and the TmuxSessionManager falls back to `http://localhost:4096`. With the in-process flag set, the team layout proceeded to spawn tmux panes whose `opencode attach` commands then failed with "Unable to connect" — exactly the symptom in the bug report. Only mark the server as in-process running when the SDK actually provides `ctx.serverUrl`. When it does not, `isServerRunning()` falls back to a real HTTP probe, the existing guard in `createTeamLayout` returns null, and no panes are created. Closes #3894
163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
import type { OhMyOpenCodeConfig } from "./config"
|
|
import type { ModelCacheState } from "./plugin-state"
|
|
import type { PluginContext, TmuxConfig } from "./plugin/types"
|
|
|
|
import type { SubagentSessionCreatedEvent } from "./features/background-agent"
|
|
import { BackgroundManager } from "./features/background-agent"
|
|
import { SkillMcpManager } from "./features/skill-mcp-manager"
|
|
import { cleanupSessionTeamRuns } from "./features/team-mode/team-runtime/session-cleanup"
|
|
import { createModelFallbackControllerAccessor } from "./hooks/model-fallback"
|
|
import { initTaskToastManager } from "./features/task-toast-manager"
|
|
import { TmuxSessionManager } from "./features/tmux-subagent"
|
|
import * as openclawRuntimeDispatch from "./openclaw/runtime-dispatch"
|
|
import { registerManagerForCleanup } from "./features/background-agent/process-cleanup"
|
|
import { createConfigHandler } from "./plugin-handlers"
|
|
import { log } from "./shared"
|
|
import { markServerRunningInProcess } from "./shared/tmux/tmux-utils/server-health"
|
|
import type { ModelFallbackControllerAccessor } from "./hooks/model-fallback"
|
|
|
|
type CreateManagersDeps = {
|
|
BackgroundManagerClass: typeof BackgroundManager
|
|
SkillMcpManagerClass: typeof SkillMcpManager
|
|
TmuxSessionManagerClass: typeof TmuxSessionManager
|
|
initTaskToastManagerFn: typeof initTaskToastManager
|
|
registerManagerForCleanupFn: typeof registerManagerForCleanup
|
|
cleanupSessionTeamRunsFn: typeof cleanupSessionTeamRuns
|
|
createConfigHandlerFn: typeof createConfigHandler
|
|
markServerRunningInProcessFn: typeof markServerRunningInProcess
|
|
}
|
|
|
|
const defaultCreateManagersDeps: CreateManagersDeps = {
|
|
BackgroundManagerClass: BackgroundManager,
|
|
SkillMcpManagerClass: SkillMcpManager,
|
|
TmuxSessionManagerClass: TmuxSessionManager,
|
|
initTaskToastManagerFn: initTaskToastManager,
|
|
registerManagerForCleanupFn: registerManagerForCleanup,
|
|
cleanupSessionTeamRunsFn: cleanupSessionTeamRuns,
|
|
createConfigHandlerFn: createConfigHandler,
|
|
markServerRunningInProcessFn: markServerRunningInProcess,
|
|
}
|
|
|
|
export type Managers = {
|
|
tmuxSessionManager: TmuxSessionManager
|
|
backgroundManager: BackgroundManager
|
|
skillMcpManager: SkillMcpManager
|
|
configHandler: ReturnType<typeof createConfigHandler>
|
|
modelFallbackControllerAccessor: ModelFallbackControllerAccessor
|
|
}
|
|
|
|
export function createManagers(args: {
|
|
ctx: PluginContext
|
|
pluginConfig: OhMyOpenCodeConfig
|
|
tmuxConfig: TmuxConfig
|
|
modelCacheState: ModelCacheState
|
|
backgroundNotificationHookEnabled: boolean
|
|
deps?: Partial<CreateManagersDeps>
|
|
}): Managers {
|
|
const { ctx, pluginConfig, tmuxConfig, modelCacheState, backgroundNotificationHookEnabled } = args
|
|
const deps = { ...defaultCreateManagersDeps, ...args.deps }
|
|
|
|
// Only mark the server as in-process when the SDK actually exposes a
|
|
// serverUrl. `tmuxConfig.enabled` alone is not proof of a running server —
|
|
// a vanilla `opencode` session (no `opencode serve`/`opencode web`) leaves
|
|
// `ctx.serverUrl` undefined, and marking it running would make
|
|
// `isServerRunning` short-circuit to true. That bypasses the guard in
|
|
// `createTeamLayout` and lets it spawn tmux panes whose `opencode attach`
|
|
// command then fails because nothing is actually listening on the
|
|
// fallback port (issue #3894).
|
|
if (tmuxConfig.enabled && ctx.serverUrl) {
|
|
deps.markServerRunningInProcessFn()
|
|
}
|
|
const tmuxSessionManager = new deps.TmuxSessionManagerClass(ctx, tmuxConfig)
|
|
const modelFallbackControllerAccessor = createModelFallbackControllerAccessor()
|
|
let backgroundManager: BackgroundManager | undefined
|
|
|
|
const cleanupTeamModeRuns = async (): Promise<void> => {
|
|
if (!pluginConfig.team_mode?.enabled) return
|
|
const report = await deps.cleanupSessionTeamRunsFn({
|
|
config: pluginConfig.team_mode,
|
|
tmuxMgr: tmuxSessionManager,
|
|
bgMgr: backgroundManager,
|
|
})
|
|
if (report.cleanedTeamRunIds.length > 0 || report.errors.length > 0) {
|
|
log("[create-managers] team-mode session cleanup complete", report)
|
|
}
|
|
}
|
|
|
|
deps.registerManagerForCleanupFn({
|
|
shutdown: async () => {
|
|
await cleanupTeamModeRuns().catch((error) => {
|
|
log("[create-managers] team-mode cleanup error during process shutdown:", error)
|
|
})
|
|
await tmuxSessionManager.cleanup().catch((error) => {
|
|
log("[create-managers] tmux cleanup error during process shutdown:", error)
|
|
})
|
|
},
|
|
})
|
|
|
|
backgroundManager = new deps.BackgroundManagerClass({
|
|
pluginContext: ctx,
|
|
config: pluginConfig.background_task,
|
|
tmuxConfig,
|
|
onSubagentSessionCreated: async (event: SubagentSessionCreatedEvent) => {
|
|
log("[create-managers] onSubagentSessionCreated callback received", {
|
|
sessionID: event.sessionID,
|
|
parentID: event.parentID,
|
|
title: event.title,
|
|
})
|
|
|
|
await tmuxSessionManager.onSessionCreated({
|
|
type: "session.created",
|
|
properties: {
|
|
info: {
|
|
id: event.sessionID,
|
|
parentID: event.parentID,
|
|
title: event.title,
|
|
},
|
|
},
|
|
})
|
|
|
|
if (pluginConfig.openclaw) {
|
|
await openclawRuntimeDispatch.dispatchOpenClawEvent({
|
|
config: pluginConfig.openclaw,
|
|
rawEvent: "session.created",
|
|
context: {
|
|
sessionId: event.sessionID,
|
|
projectPath: ctx.directory,
|
|
tmuxPaneId: tmuxSessionManager.getTrackedPaneId?.(event.sessionID) ?? process.env.TMUX_PANE,
|
|
},
|
|
})
|
|
}
|
|
|
|
log("[create-managers] onSubagentSessionCreated callback completed")
|
|
},
|
|
onShutdown: async () => {
|
|
await cleanupTeamModeRuns().catch((error) => {
|
|
log("[create-managers] team-mode cleanup error during shutdown:", error)
|
|
})
|
|
await tmuxSessionManager.cleanup().catch((error) => {
|
|
log("[create-managers] tmux cleanup error during shutdown:", error)
|
|
})
|
|
},
|
|
enableParentSessionNotifications: backgroundNotificationHookEnabled,
|
|
modelFallbackControllerAccessor,
|
|
})
|
|
|
|
deps.initTaskToastManagerFn(ctx.client)
|
|
|
|
const skillMcpManager = new deps.SkillMcpManagerClass()
|
|
|
|
const configHandler = deps.createConfigHandlerFn({
|
|
ctx: { directory: ctx.directory, client: ctx.client },
|
|
pluginConfig,
|
|
modelCacheState,
|
|
})
|
|
return {
|
|
tmuxSessionManager,
|
|
backgroundManager,
|
|
skillMcpManager,
|
|
configHandler,
|
|
modelFallbackControllerAccessor,
|
|
}
|
|
}
|