Merge pull request #4063 from PeterPonyu/fix/3963-server-url-port-zero

fix(team-mode): surface port-0 fallback and silent layout skip (#3963)
This commit is contained in:
YeonGyu-Kim
2026-05-21 00:42:20 +09:00
committed by GitHub
3 changed files with 91 additions and 2 deletions
@@ -122,7 +122,17 @@ export async function createTeamLayout(teamRunId: string, members: Array<TeamLay
try {
const serverUrl = tmuxMgr.getServerUrl()
if (!(await deps.isServerRunning(serverUrl))) {
log("opencode server not reachable, skipping team layout", { serverUrl })
const ctxServerUrl = tmuxMgr.getCtxServerUrl?.()
log("opencode server not reachable, skipping team layout (see issue #3963)", {
kind: "warning",
teamRunId,
serverUrl,
ctxServerUrl: ctxServerUrl && ctxServerUrl !== serverUrl ? ctxServerUrl : undefined,
hint:
ctxServerUrl && ctxServerUrl !== serverUrl
? "ctx.serverUrl was discarded (likely port 0); launch opencode with --port N and OPENCODE_PORT=N to bind a real port"
: "no opencode server is listening on the fallback URL",
})
return null
}
@@ -468,6 +468,69 @@ describe('TmuxSessionManager', () => {
// then
expect(getManagerInternals(manager).serverUrl).toBe('http://localhost:4096')
})
test('logs a structured warning when ctx.serverUrl has port 0 (#3963)', async () => {
// given
const previousOpenCodePort = process.env.OPENCODE_PORT
delete process.env.OPENCODE_PORT
const logCalls: Array<{ message: string; data?: unknown }> = []
const trackingDeps: TmuxUtilDeps = {
...mockTmuxDeps,
log: (message, data) => { logCalls.push({ message, data }) },
}
try {
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'),
}
const config = createTmuxConfig({ enabled: true })
// when
const manager = new TmuxSessionManager(ctx, config, trackingDeps)
// then
const warning = logCalls.find((entry) => entry.message.includes('ctx.serverUrl has port 0'))
expect(warning).toBeDefined()
expect(warning?.data).toMatchObject({
kind: 'warning',
ctxServerUrl: 'http://127.0.0.1:0/',
fallbackUrl: 'http://localhost:4096',
})
expect(manager.getCtxServerUrl()).toBe('http://127.0.0.1:0/')
} finally {
if (previousOpenCodePort === undefined) {
delete process.env.OPENCODE_PORT
} else {
process.env.OPENCODE_PORT = previousOpenCodePort
}
}
})
test('does not warn when ctx.serverUrl has a real port', async () => {
// given
const logCalls: Array<{ message: string; data?: unknown }> = []
const trackingDeps: TmuxUtilDeps = {
...mockTmuxDeps,
log: (message, data) => { logCalls.push({ message, data }) },
}
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:12345/'),
}
const config = createTmuxConfig({ enabled: true })
// when
const manager = new TmuxSessionManager(ctx, config, trackingDeps)
// then
const warning = logCalls.find((entry) => entry.message.includes('ctx.serverUrl has port 0'))
expect(warning).toBeUndefined()
expect(manager.getCtxServerUrl()).toBe('http://127.0.0.1:12345/')
})
})
describe('getServerUrl', () => {
+17 -1
View File
@@ -90,6 +90,7 @@ export class TmuxSessionManager {
private tmuxConfig: TmuxConfig
private projectDirectory: string
private serverUrl: string
private ctxServerUrl: string | undefined
private sourcePaneId: string | undefined
private sessions = new Map<string, TrackedSession>()
private pendingSessions = new Set<string>()
@@ -122,11 +123,22 @@ export class TmuxSessionManager {
: "4096"
const fallbackUrl = `http://localhost:${defaultPort}`
const rawServerUrl = ctx.serverUrl?.toString()
this.ctxServerUrl = rawServerUrl
try {
if (rawServerUrl) {
const parsed = new URL(rawServerUrl)
const port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80')
this.serverUrl = port === '0' ? fallbackUrl : rawServerUrl
if (port === '0') {
this.deps.log(
"[tmux-session-manager] ctx.serverUrl has port 0; falling back. " +
"team_mode tmux visualization will silently skip if nothing is listening on the fallback URL. " +
"Launch opencode with --port N and OPENCODE_PORT=N to bind a real port (see issue #3963).",
{ kind: "warning", ctxServerUrl: rawServerUrl, fallbackUrl },
)
this.serverUrl = fallbackUrl
} else {
this.serverUrl = rawServerUrl
}
} else {
this.serverUrl = fallbackUrl
}
@@ -256,6 +268,10 @@ export class TmuxSessionManager {
return this.serverUrl
}
getCtxServerUrl(): string | undefined {
return this.ctxServerUrl
}
private removeTrackedSession(sessionId: string): void {
this.sessions.delete(sessionId)