refactor(tmux): export sweepTmuxSessionsWith and add runtime tests

This commit is contained in:
YeonGyu-Kim
2026-04-28 10:44:56 +09:00
parent 96161cc0e6
commit c9e544a667
4 changed files with 189 additions and 36 deletions
@@ -1,5 +1,13 @@
const STALE_SESSION_PATTERN = /^omo-agents-(\d+)$/
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message
}
return String(error)
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0)
@@ -10,36 +18,48 @@ function isProcessAlive(pid: number): boolean {
}
}
async function listOmoAgentSessionsViaTmux(tmux: string): Promise<string[]> {
const { spawn } = await import("./spawn-process")
const proc = spawn([tmux, "list-sessions", "-F", "#{session_name}"], {
stdout: "pipe",
stderr: "pipe",
})
const [stdout, , exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
])
async function listTmuxSessionsViaTmux(tmux: string): Promise<string[]> {
const { runTmuxCommand } = await import("../runner")
const result = await runTmuxCommand(tmux, ["list-sessions", "-F", "#{session_name}"])
if (exitCode !== 0) {
if (result.exitCode !== 0) {
return []
}
return stdout
return result.output
.split("\n")
.map((line) => line.trim())
.filter((name) => STALE_SESSION_PATTERN.test(name))
.filter((name) => name.length > 0)
}
export type SweepDeps = {
export type SweepTmuxSessionsDeps = {
isInsideTmux: () => boolean
getTmuxPath: () => Promise<string | null | undefined>
listCandidateSessions: (tmux: string) => Promise<string[]>
killSession: (sessionName: string) => Promise<boolean>
log: (message: string, payload?: unknown) => void
}
export type SweepDeps = SweepTmuxSessionsDeps & {
processAlive: (pid: number) => boolean
currentPid: number
log: (message: string, payload?: unknown) => void
}
export type SweepTmuxSessionsOptions = {
prefix?: string
predicate?: (sessionName: string) => boolean
}
function matchesSweepOptions(sessionName: string, options: SweepTmuxSessionsOptions): boolean {
if (options.predicate) {
return options.predicate(sessionName)
}
if (options.prefix) {
return sessionName.startsWith(options.prefix)
}
return true
}
async function buildRuntimeDeps(): Promise<SweepDeps> {
@@ -53,7 +73,7 @@ async function buildRuntimeDeps(): Promise<SweepDeps> {
return {
isInsideTmux,
getTmuxPath,
listCandidateSessions: listOmoAgentSessionsViaTmux,
listCandidateSessions: listTmuxSessionsViaTmux,
killSession: killTmuxSessionIfExists,
processAlive: isProcessAlive,
currentPid: process.pid,
@@ -61,36 +81,75 @@ async function buildRuntimeDeps(): Promise<SweepDeps> {
}
}
export async function sweepStaleOmoAgentSessionsWith(deps: SweepDeps): Promise<number> {
export async function sweepTmuxSessionsWith(
deps: SweepTmuxSessionsDeps,
options: SweepTmuxSessionsOptions,
): Promise<string[]> {
if (!deps.isInsideTmux()) {
return 0
return []
}
const tmux = await deps.getTmuxPath()
if (!tmux) {
return 0
return []
}
const candidateSessions = await deps.listCandidateSessions(tmux)
let killedCount = 0
let candidateSessions: string[]
try {
candidateSessions = await deps.listCandidateSessions(tmux)
} catch (error) {
deps.log("[sweepTmuxSessionsWith] failed to list candidate sessions", {
error: getErrorMessage(error),
})
return []
}
const killedSessionNames: string[] = []
for (const sessionName of candidateSessions) {
const pidMatch = sessionName.match(STALE_SESSION_PATTERN)
if (!pidMatch) continue
if (!matchesSweepOptions(sessionName, options)) {
continue
}
const pid = Number.parseInt(pidMatch[1], 10)
if (!Number.isFinite(pid)) continue
if (pid === deps.currentPid) continue
if (deps.processAlive(pid)) continue
deps.log("[sweepStaleOmoAgentSessions] killing stale session", { sessionName, deadPid: pid })
const killed = await deps.killSession(sessionName)
if (killed) {
killedCount += 1
try {
const killed = await deps.killSession(sessionName)
if (killed) {
killedSessionNames.push(sessionName)
}
} catch (error) {
deps.log("[sweepTmuxSessionsWith] failed to kill stale session", {
error: getErrorMessage(error),
sessionName,
})
}
}
return killedCount
return killedSessionNames
}
export async function sweepStaleOmoAgentSessionsWith(deps: SweepDeps): Promise<number> {
const killedSessionNames = await sweepTmuxSessionsWith(deps, {
predicate: (sessionName) => {
const pidMatch = sessionName.match(STALE_SESSION_PATTERN)
if (!pidMatch) {
return false
}
const pid = Number.parseInt(pidMatch[1], 10)
if (!Number.isFinite(pid)) {
return false
}
if (pid === deps.currentPid) {
return false
}
return !deps.processAlive(pid)
},
})
return killedSessionNames.length
}
export async function sweepStaleOmoAgentSessions(): Promise<number> {