fix(tmux): scope isolated session name per plugin instance (Oracle review)

Oracle flagged the previous commit: "omo-agents" was a shared constant,
so when two plugin instances ran in the same tmux server they wrote into
the same session. One instance's cleanup would then kill-session on the
shared name and tear down the other instance's live attached panes.

Replace the const ISOLATED_SESSION_NAME with getIsolatedSessionName(pid)
which defaults to process.pid, so every opencode process owns its own
"omo-agents-<pid>" session. spawnTmuxSession and cleanup both resolve
the name through this helper. Discovery is straightforward from the
host tmux via 'tmux list-sessions | grep omo-agents-'.

Manager test covers two concurrent managers and asserts each kills a
per-pid session name, proving they no longer collide on a global name.
This commit is contained in:
YeonGyu-Kim
2026-04-18 19:47:36 +09:00
parent f8a1a11bb7
commit 257b6cf951
4 changed files with 45 additions and 14 deletions
+28 -3
View File
@@ -101,7 +101,7 @@ mock.module('../../shared/tmux', () => {
spawnTmuxWindow: mockSpawnTmuxWindow,
spawnTmuxSession: mockSpawnTmuxSession,
killTmuxSessionIfExists: mockKillTmuxSessionIfExists,
ISOLATED_SESSION_NAME: 'omo-agents',
getIsolatedSessionName: (pid: number = 12345) => `omo-agents-${pid}`,
}
})
@@ -1856,7 +1856,7 @@ describe('TmuxSessionManager', () => {
expect(mockExecuteAction).toHaveBeenCalledTimes(2)
})
test('#given tmux isolation is "session" #when cleanup runs #then killTmuxSessionIfExists is invoked for the isolated session', async () => {
test('#given tmux isolation is "session" #when cleanup runs #then killTmuxSessionIfExists is invoked for the per-pid isolated session', async () => {
// given
mockKillTmuxSessionIfExists.mockClear()
const { TmuxSessionManager } = await import('./manager')
@@ -1870,7 +1870,32 @@ describe('TmuxSessionManager', () => {
// then
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1)
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledWith('omo-agents')
expect(mockKillTmuxSessionIfExists.mock.calls[0]?.[0]).toMatch(/^omo-agents-\d+$/)
})
test('#given two manager instances #when both cleanup #then each kills its own isolated session name, not a shared one', async () => {
// given
mockKillTmuxSessionIfExists.mockClear()
const { TmuxSessionManager } = await import('./manager')
const managerA = new TmuxSessionManager(createMockContext(), createTmuxConfig({
enabled: true,
isolation: 'session',
}), mockTmuxDeps)
const managerB = new TmuxSessionManager(createMockContext(), createTmuxConfig({
enabled: true,
isolation: 'session',
}), mockTmuxDeps)
// when
await managerA.cleanup()
await managerB.cleanup()
// then
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(2)
const firstTarget = mockKillTmuxSessionIfExists.mock.calls[0]?.[0]
const secondTarget = mockKillTmuxSessionIfExists.mock.calls[1]?.[0]
expect(firstTarget).toMatch(/^omo-agents-\d+$/)
expect(secondTarget).toMatch(/^omo-agents-\d+$/)
})
test('#given tmux isolation is "inline" #when cleanup runs #then killTmuxSessionIfExists is NOT invoked', async () => {
+5 -4
View File
@@ -11,7 +11,7 @@ import {
spawnTmuxWindow,
spawnTmuxSession,
killTmuxSessionIfExists,
ISOLATED_SESSION_NAME,
getIsolatedSessionName,
} from "../../shared/tmux"
import { queryWindowState } from "./pane-state-querier"
import { decideSpawnActions, decideCloseAction, type SessionMapping } from "./decision-engine"
@@ -970,15 +970,16 @@ export class TmuxSessionManager {
this.isolatedWindowPaneId = undefined
if (this.tmuxConfig.isolation === "session") {
const isolatedSessionName = getIsolatedSessionName()
try {
const killed = await killTmuxSessionIfExists(ISOLATED_SESSION_NAME)
const killed = await killTmuxSessionIfExists(isolatedSessionName)
log("[tmux-session-manager] isolated session teardown", {
session: ISOLATED_SESSION_NAME,
session: isolatedSessionName,
killed,
})
} catch (error) {
log("[tmux-session-manager] isolated session teardown failed", {
session: ISOLATED_SESSION_NAME,
session: isolatedSessionName,
error: String(error),
})
}