diff --git a/src/shared/tmux/constants.ts b/src/shared/tmux/constants.ts
index 5299d3964..71205a886 100644
--- a/src/shared/tmux/constants.ts
+++ b/src/shared/tmux/constants.ts
@@ -1,11 +1,15 @@
// Polling interval for background session status checks
export const POLL_INTERVAL_BACKGROUND_MS = 2000
-// Maximum idle time before session considered stale
-export const SESSION_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes
+// Long-running subagent work can legitimately stay open for a while.
+// The tmux-subagent stability fixes raised this guard from 10 minutes after
+// polling closed active panes during long tasks.
+export const SESSION_TIMEOUT_MS = 60 * 60 * 1000 // 60 minutes
-// Grace period for missing session before cleanup
-export const SESSION_MISSING_GRACE_MS = 6000 // 6 seconds
+// Status queries can transiently miss live sessions under load.
+// The tmux-subagent stability fixes raised this guard from 6 seconds after
+// false missing detections closed healthy panes.
+export const SESSION_MISSING_GRACE_MS = 30 * 1000 // 30 seconds
// Session readiness polling config
export const SESSION_READY_POLL_INTERVAL_MS = 500
diff --git a/src/shared/tmux/index.ts b/src/shared/tmux/index.ts
index a86723661..b523bf642 100644
--- a/src/shared/tmux/index.ts
+++ b/src/shared/tmux/index.ts
@@ -1,3 +1,4 @@
export * from "./types"
export * from "./constants"
+export * from "./runner"
export * from "./tmux-utils"
diff --git a/src/shared/tmux/runner.test.ts b/src/shared/tmux/runner.test.ts
new file mode 100644
index 000000000..9832c99b2
--- /dev/null
+++ b/src/shared/tmux/runner.test.ts
@@ -0,0 +1,127 @@
+///
+
+import { afterAll, describe, expect, test } from "bun:test"
+import { randomUUID } from "node:crypto"
+import fs from "node:fs/promises"
+import os from "node:os"
+import path from "node:path"
+
+import { runTmuxCommand } from "./runner"
+
+const temporaryDirectories: string[] = []
+
+async function createTemporaryDirectory(): Promise {
+ const directoryPath = await fs.mkdtemp(path.join(os.tmpdir(), "tmux-runner-"))
+ temporaryDirectories.push(directoryPath)
+ return directoryPath
+}
+
+async function readInvocationCount(counterFilePath: string): Promise {
+ const count = await fs.readFile(counterFilePath, "utf8")
+ return Number.parseInt(count, 10)
+}
+
+afterAll(async () => {
+ for (const directoryPath of temporaryDirectories) {
+ await fs.rm(directoryPath, { recursive: true, force: true })
+ }
+})
+
+describe("runTmuxCommand", () => {
+ test("#given command exits 0 with stdout #when run #then success true, output and stdout equal trimmed value, stderr empty", async () => {
+ // given
+ const commandArguments = ["-c", "printf '%s\\n' '%42'"]
+
+ // when
+ const result = await runTmuxCommand("sh", commandArguments)
+
+ // then
+ expect(result).toEqual({
+ success: true,
+ output: "%42",
+ stdout: "%42",
+ stderr: "",
+ exitCode: 0,
+ })
+ })
+
+ test("#given command exits 1 with stderr #when run #then success false, stderr populated", async () => {
+ // given
+ const commandArguments = ["-c", "printf '%s\\n' 'some error' >&2; exit 1"]
+
+ // when
+ const result = await runTmuxCommand("sh", commandArguments)
+
+ // then
+ expect(result.success).toBe(false)
+ expect(result.stderr).toBe("some error")
+ expect(result.exitCode).toBe(1)
+ })
+
+ test("#given retry=2 and first exit nonzero #when run #then calls spawn twice before returning failure", async () => {
+ // given
+ const temporaryDirectory = await createTemporaryDirectory()
+ const counterFilePath = path.join(temporaryDirectory, `${randomUUID()}.count`)
+ const commandScript = `counter_file="$1"; count=0; if [ -f "$counter_file" ]; then count=$(cat "$counter_file"); fi; count=$((count + 1)); printf '%s' "$count" > "$counter_file"; printf '%s\\n' 'temporary error' >&2; exit 1`
+
+ // when
+ const result = await runTmuxCommand("sh", ["-c", commandScript, "sh", counterFilePath], { retry: 2 })
+
+ // then
+ expect(result.success).toBe(false)
+ expect(result.stderr).toBe("temporary error")
+ expect(await readInvocationCount(counterFilePath)).toBe(3)
+ })
+
+ test("#given retry=2 and stderr contains 'can't find pane' #when run #then does NOT retry", async () => {
+ // given
+ const temporaryDirectory = await createTemporaryDirectory()
+ const counterFilePath = path.join(temporaryDirectory, `${randomUUID()}.count`)
+ const commandScript = `counter_file="$1"; count=0; if [ -f "$counter_file" ]; then count=$(cat "$counter_file"); fi; count=$((count + 1)); printf '%s' "$count" > "$counter_file"; printf '%s\\n' "can't find pane: %1" >&2; exit 1`
+
+ // when
+ const result = await runTmuxCommand("sh", ["-c", commandScript, "sh", counterFilePath], { retry: 2 })
+
+ // then
+ expect(result.success).toBe(false)
+ expect(result.stderr).toContain("can't find pane")
+ expect(await readInvocationCount(counterFilePath)).toBe(1)
+ })
+
+ test("#given timeoutMs=50 and command sleeps 500ms #when run #then returns timeout failure", async () => {
+ // given
+ const commandArguments = ["-c", "sleep 0.5"]
+
+ // when
+ const result = await runTmuxCommand("sh", commandArguments, { timeoutMs: 50 })
+
+ // then
+ expect(result.success).toBe(false)
+ expect(result.exitCode).toBe(-1)
+ expect(result.stderr).toContain("timeout")
+ })
+
+ test("#given stdout contains trailing newline #when run #then output is trimmed", async () => {
+ // given
+ const commandArguments = ["-c", "printf '%s\\n\\n' '%7'"]
+
+ // when
+ const result = await runTmuxCommand("sh", commandArguments)
+
+ // then
+ expect(result.output).toBe("%7")
+ expect(result.stdout).toBe("%7")
+ })
+
+ test("#given backward-compat consumer destructures {success, output} #when result returned #then both fields present and correct", async () => {
+ // given
+ const commandArguments = ["-c", "printf '%s\\n' '%9'"]
+
+ // when
+ const { success, output } = await runTmuxCommand("sh", commandArguments)
+
+ // then
+ expect(success).toBe(true)
+ expect(output).toBe("%9")
+ })
+})
diff --git a/src/shared/tmux/runner.ts b/src/shared/tmux/runner.ts
new file mode 100644
index 000000000..0290a96aa
--- /dev/null
+++ b/src/shared/tmux/runner.ts
@@ -0,0 +1,107 @@
+import { spawn } from "bun"
+
+type RunTmuxOptions = {
+ retry?: number
+ timeoutMs?: number
+}
+
+export type TmuxCommandResult = {
+ success: boolean
+ output: string
+ stdout: string
+ stderr: string
+ exitCode: number
+}
+
+const TERMINAL_TMUX_ERROR_PATTERN = /can't find (pane|session)/i
+
+function createTmuxCommandResult(stdout: string, stderr: string, exitCode: number): TmuxCommandResult {
+ return {
+ success: exitCode === 0,
+ output: stdout,
+ stdout,
+ stderr,
+ exitCode,
+ }
+}
+
+function isTerminalTmuxError(stderr: string): boolean {
+ return TERMINAL_TMUX_ERROR_PATTERN.test(stderr)
+}
+
+/**
+ * Detect whether we are running inside cmux (cmux omo).
+ * When cmux-omo sets up the environment it injects a tmux shim and sets
+ * CMUX_SOCKET_PATH / TMUX. If detected, redirect tmux commands to
+ * `cmux __tmux-compat` so they become native cmux splits instead of
+ * failing because there is no real tmux server running.
+ */
+function resolveTmuxExecutable(tmuxPath: string): string[] {
+ const inCmux = Boolean(process.env.CMUX_SOCKET_PATH) ||
+ process.env.TMUX?.includes("cmuxterm") === true
+ if (inCmux) {
+ return ["cmux", "__tmux-compat"]
+ }
+ return [tmuxPath]
+}
+
+async function runTmuxCommandOnce(tmuxPath: string, args: Array, timeoutMs?: number): Promise {
+ const abortController = new AbortController()
+ const subprocess = spawn([...resolveTmuxExecutable(tmuxPath), ...args], {
+ stdout: "pipe",
+ stderr: "pipe",
+ signal: abortController.signal,
+ })
+ const stdoutPromise = new Response(subprocess.stdout).text()
+ const stderrPromise = new Response(subprocess.stderr).text()
+
+ let timeoutId: ReturnType | undefined
+
+ try {
+ const exitCodeOrTimeout = timeoutMs === undefined
+ ? await subprocess.exited
+ : await Promise.race(([
+ subprocess.exited,
+ new Promise<"timeout">((resolve) => {
+ timeoutId = setTimeout(() => {
+ abortController.abort()
+ resolve("timeout")
+ }, timeoutMs)
+ }),
+ ]))
+
+ if (exitCodeOrTimeout === "timeout") {
+ void subprocess.exited.catch(() => undefined)
+ void stdoutPromise.catch(() => "")
+ void stderrPromise.catch(() => "")
+ return createTmuxCommandResult("", "timeout", -1)
+ }
+
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise])
+ return createTmuxCommandResult(stdout.trim(), stderr.trim(), exitCodeOrTimeout)
+ } finally {
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId)
+ }
+ }
+}
+
+export async function runTmuxCommand(tmuxPath: string, args: string[], options: RunTmuxOptions = {}): Promise {
+ const retryCount = Math.max(0, options.retry ?? 0)
+ let lastResult = createTmuxCommandResult("", "", 1)
+
+ for (let attempt = 0; attempt <= retryCount; attempt += 1) {
+ const result = await runTmuxCommandOnce(tmuxPath, args, options.timeoutMs)
+ lastResult = result
+
+ if (result.exitCode === 0) {
+ return result
+ }
+
+ if (attempt === retryCount || isTerminalTmuxError(result.stderr)) {
+ return result
+ }
+ }
+
+ return lastResult
+}