From 94df1467a603eb068a94cbe0be8d57cb39cfa383 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 23 Aug 2026 16:56:22 +0800 Subject: [PATCH 01/10] fix(cli): discover resumable sessions from /resume Generated-by: Codex --- .../cli/src/__tests__/pi-tui-runner.test.ts | 29 +++++++++++++++ packages/cli/src/pi-tui-runner.ts | 36 +++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 69c1f8d731..7fcb028578 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3465,6 +3465,35 @@ describe('Maka Pi TUI runner', () => { ]); }); + test('/resume opens a picker containing only resumable sessions when none is attached', async () => { + const terminal = new FakeTerminal(); + const resumable = fakeSessionSummary('resumable', '/repo'); + const unavailable = fakeSessionSummary('unavailable', ''); + const driver = new SlashCommandDriver([resumable, unavailable]); + (driver as unknown as { sessionId: string | null }).sessionId = null; + const run = runMakaPiTui({ + title: 'Maka', + driver, + cwd: '/repo', + model: 'm', + connectionSlug: 'c', + permissionMode: 'bypass', + terminal, + }); + + terminal.input('/resume'); + terminal.input('\r'); + await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); + const output = plainTerminalOutput(terminal.output()); + assert.match(output, /resumabl/); + assert.doesNotMatch(output, /unavailable/); + + terminal.input('\x1b'); + terminal.input('/exit'); + terminal.input('\r'); + await run; + }); + test('surfaces a notice when the foreign-session scan fails', async () => { const terminal = new FakeTerminal(); const driver = new SlashCommandDriver([]); diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 1958aa528a..21b45d0b38 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2262,6 +2262,10 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { }; const resumeSession = async () => { + if (!input.driver.getSessionId()) { + await showSessionList({ onlyResumable: true }); + return; + } if (!input.driver.resumeLatest) { throw new Error('Safe-boundary resume is unavailable on this runtime.'); } @@ -2279,7 +2283,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { } }; - const showSessionList = async () => { + const showSessionList = async (options: { onlyResumable?: boolean } = {}) => { const sessions = await input.driver.listSessions(); const sessionTree = projectRevisionLinkedSessionTree( sessions, @@ -2337,7 +2341,10 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { sessionListScope === 'current' ? projectedSessions.filter(({ session }) => session.cwd === cwd) : projectedSessions; - const items: SelectItem[] = visibleSessions.map(({ session, depth }) => { + const selectableSessions = options.onlyResumable + ? visibleSessions.filter(({ session }) => availability.get(session.id)?.available === true) + : visibleSessions; + const items: SelectItem[] = selectableSessions.map(({ session, depth }) => { const state = availability.get(session.id); const statusBadge = sessionStatusBadge(session, locale); const statusDetail = statusBadge ? ` · ${statusBadge}` : ''; @@ -2404,6 +2411,30 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { renderScope(); }; + const announceResumeAvailability = async (): Promise => { + const sessionId = input.driver.getSessionId(); + try { + const sessions = await input.driver.listSessions(); + const session = + sessions.find((candidate) => candidate.id === sessionId) ?? + sessions.find((candidate) => candidate.cwd === cwd); + if (!session) return; + const availability = + (await input.driver.getSessionResumeAvailability?.(session)) ?? + (await inspectSessionResumeAvailability(session)); + if (availability.available) { + state.entries.push({ + kind: 'notice', + level: 'info', + text: 'This session has an interrupted run — /resume to continue from the safe boundary.', + }); + requestRender(); + } + } catch { + // Resume discovery is advisory and must never prevent the TUI from starting. + } + }; + const showRewindPicker = async () => { if (blockIdentityChangeWhileSideOpen('rewinding')) return; const targets = await input.driver.listRewindTargets(); @@ -3640,6 +3671,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { // line discipline and leaks onto the screen as a stray `^[[I` on launch. terminal.write(ENABLE_FOCUS_REPORTING); if (input.firstRun) void showSetupWizard(); + setTimeout(() => void announceResumeAvailability(), 0); } catch (error) { beginClose(error instanceof Error ? error : new Error(String(error))); } From 471ef5cdafaaf5cfa32d6ecab825027bd92702ac Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 23 Aug 2026 18:32:36 +0800 Subject: [PATCH 02/10] fix(cli): resume the selected interrupted session Generated-by: Codex --- packages/cli/src/__tests__/pi-tui-runner.test.ts | 3 ++- .../__tests__/runtime-host-session-driver.test.ts | 9 +++++++++ packages/cli/src/pi-tui-runner.ts | 7 +++++-- packages/cli/src/runtime-host-session-driver.ts | 14 ++++++++++---- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 7fcb028578..4611974c5b 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3488,7 +3488,8 @@ describe('Maka Pi TUI runner', () => { assert.match(output, /resumabl/); assert.doesNotMatch(output, /unavailable/); - terminal.input('\x1b'); + terminal.input('\r'); + await waitFor(() => driver.resumeCalls === 1); terminal.input('/exit'); terminal.input('\r'); await run; diff --git a/packages/cli/src/__tests__/runtime-host-session-driver.test.ts b/packages/cli/src/__tests__/runtime-host-session-driver.test.ts index b163b6689f..b934a6d4f5 100644 --- a/packages/cli/src/__tests__/runtime-host-session-driver.test.ts +++ b/packages/cli/src/__tests__/runtime-host-session-driver.test.ts @@ -1768,6 +1768,15 @@ class FakeConnection { goal: this.goalQueryResults.shift() ?? null, } as OperationOutput; } + if (operation === 'turn.resume.query') { + return { + sessionId: (input as OperationInput<'turn.resume.query'>).sessionId, + disposition: 'ready', + sourceRunId: 'source-run-1', + sourceTurnId: 'source-turn-1', + sourceRuntimeEventHighWater: 1, + } as OperationOutput; + } if (operation === 'session.configuration.update') { const update = input as OperationInput<'session.configuration.update'>; return { diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 21b45d0b38..558aa51899 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2387,9 +2387,12 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { void importForeignSession(foreign); return; } - if (availability.get(item.value)?.available === false) return; + if (options.onlyResumable && availability.get(item.value)?.available === false) return; closeOverlay(); - void goToSession(item.value); + void (async () => { + await goToSession(item.value); + if (options.onlyResumable) await runControl(resumeSession); + })().catch(reportError); }; list.onCancel = () => closeOverlay(); sessionPickerOverlayOpen = true; diff --git a/packages/cli/src/runtime-host-session-driver.ts b/packages/cli/src/runtime-host-session-driver.ts index 99c7bee950..63cbe4aaf6 100644 --- a/packages/cli/src/runtime-host-session-driver.ts +++ b/packages/cli/src/runtime-host-session-driver.ts @@ -287,7 +287,14 @@ class RuntimeHostMakaSessionDriverImpl implements RuntimeHostMakaSessionDriver { .map(({ session }) => session); } - getSessionResumeAvailability(session: SessionSummary): Promise { + async getSessionResumeAvailability(session: SessionSummary): Promise { + if (this.#executionLocation.kind === 'host') { + if (!session.cwd) return { available: false, reason: 'Missing working directory' }; + const plan = await this.#request('turn.resume.query', { sessionId: session.id }); + return plan.disposition === 'ready' + ? { available: true } + : { available: false, reason: plan.reason }; + } return inspectRuntimeHostSessionResumeAvailability(session, this.#executionLocation); } @@ -1355,9 +1362,8 @@ function inspectRuntimeHostSessionResumeAvailability( if (!summary.cwd) { return Promise.resolve({ available: false, reason: 'Missing working directory' }); } - return location.kind === 'host' - ? Promise.resolve({ available: true }) - : inspectSessionResumeAvailability(summary); + if (location.kind !== 'host') return inspectSessionResumeAvailability(summary); + return Promise.resolve({ available: true }); } async function assertSessionResumeAvailable( From 358c3679ed632251d5c610b4537c109ad5aec706 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Mon, 24 Aug 2026 07:36:28 +0800 Subject: [PATCH 03/10] fix(cli): separate session attach and resume discovery Generated-by: Codex --- .../cli/src/__tests__/pi-tui-runner.test.ts | 30 ++++++++++++++++ packages/cli/src/pi-tui-runner.ts | 35 ++++++++++++++----- .../cli/src/runtime-host-session-driver.ts | 17 +++++---- packages/cli/src/session-driver.ts | 1 + 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 4611974c5b..253a7193a9 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3495,6 +3495,36 @@ describe('Maka Pi TUI runner', () => { await run; }); + test('/session keeps attachable rows when resume discovery fails for another session', async () => { + const terminal = new FakeTerminal(); + const attachable = fakeSessionSummary('attachable', '/repo'); + const archived = fakeSessionSummary('archived', '/repo'); + const driver = new SlashCommandDriver([attachable, archived]); + driver.getSessionResumeAvailability = async (session) => { + if (session.id === archived.id) throw new Error('session archived'); + return { available: true }; + }; + const run = runMakaPiTui({ + title: 'Maka', + driver, + cwd: '/repo', + model: 'm', + connectionSlug: 'c', + permissionMode: 'bypass', + terminal, + }); + + terminal.input('/session'); + terminal.input('\r'); + await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); + assert.match(plainTerminalOutput(terminal.output()), /attachable/); + + terminal.input('\x1b'); + terminal.input('/exit'); + terminal.input('\r'); + await run; + }); + test('surfaces a notice when the foreign-session scan fails', async () => { const terminal = new FakeTerminal(); const driver = new SlashCommandDriver([]); diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 558aa51899..49a6bbb917 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2261,6 +2261,16 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { }); }; + let sessionListPromise: Promise | undefined; + const listSessions = (): Promise => { + if (!sessionListPromise) { + sessionListPromise = input.driver.listSessions().finally(() => { + sessionListPromise = undefined; + }); + } + return sessionListPromise; + }; + const resumeSession = async () => { if (!input.driver.getSessionId()) { await showSessionList({ onlyResumable: true }); @@ -2284,7 +2294,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { }; const showSessionList = async (options: { onlyResumable?: boolean } = {}) => { - const sessions = await input.driver.listSessions(); + const sessions = await listSessions(); const sessionTree = projectRevisionLinkedSessionTree( sessions, input.driver.getSessionId() ?? undefined, @@ -2299,11 +2309,18 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { const [availabilityEntries, foreignScan] = await Promise.all([ Promise.all( sessions.map(async (session) => { - return [ - session.id, - (await input.driver.getSessionResumeAvailability?.(session)) ?? - (await inspectSessionResumeAvailability(session)), - ] as const; + try { + const availability = options.onlyResumable + ? (await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? + (await input.driver.getSessionResumeAvailability?.(session)) ?? + (await inspectSessionResumeAvailability(session)) + : (await input.driver.getSessionResumeAvailability?.(session)) ?? + (await inspectSessionResumeAvailability(session)); + return [session.id, availability] as const; + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + return [session.id, { available: false, reason: detail }] as const; + } }), ), // Foreign (Claude Code / Codex) rows are an import flow: it starts a NEW @@ -2417,14 +2434,14 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { const announceResumeAvailability = async (): Promise => { const sessionId = input.driver.getSessionId(); try { - const sessions = await input.driver.listSessions(); + if (!input.driver.getSessionResumeCandidateAvailability) return; + const sessions = await listSessions(); const session = sessions.find((candidate) => candidate.id === sessionId) ?? sessions.find((candidate) => candidate.cwd === cwd); if (!session) return; const availability = - (await input.driver.getSessionResumeAvailability?.(session)) ?? - (await inspectSessionResumeAvailability(session)); + await input.driver.getSessionResumeCandidateAvailability(session); if (availability.available) { state.entries.push({ kind: 'notice', diff --git a/packages/cli/src/runtime-host-session-driver.ts b/packages/cli/src/runtime-host-session-driver.ts index 63cbe4aaf6..9f94e33c40 100644 --- a/packages/cli/src/runtime-host-session-driver.ts +++ b/packages/cli/src/runtime-host-session-driver.ts @@ -288,16 +288,19 @@ class RuntimeHostMakaSessionDriverImpl implements RuntimeHostMakaSessionDriver { } async getSessionResumeAvailability(session: SessionSummary): Promise { - if (this.#executionLocation.kind === 'host') { - if (!session.cwd) return { available: false, reason: 'Missing working directory' }; - const plan = await this.#request('turn.resume.query', { sessionId: session.id }); - return plan.disposition === 'ready' - ? { available: true } - : { available: false, reason: plan.reason }; - } return inspectRuntimeHostSessionResumeAvailability(session, this.#executionLocation); } + async getSessionResumeCandidateAvailability( + session: SessionSummary, + ): Promise { + if (!session.cwd) return { available: false, reason: 'Missing working directory' }; + const plan = await this.#request('turn.resume.query', { sessionId: session.id }); + return plan.disposition === 'ready' + ? { available: true } + : { available: false, reason: plan.reason }; + } + async preparePrompt( prompt: string, options: MakaPreparePromptOptions = {}, diff --git a/packages/cli/src/session-driver.ts b/packages/cli/src/session-driver.ts index e9fe200a6e..efa775f02d 100644 --- a/packages/cli/src/session-driver.ts +++ b/packages/cli/src/session-driver.ts @@ -100,6 +100,7 @@ export class SkillInvocationBlockedError extends Error { export interface MakaSessionDriver { listSessions(): Promise; getSessionResumeAvailability?(session: SessionSummary): Promise; + getSessionResumeCandidateAvailability?(session: SessionSummary): Promise; preparePrompt( prompt: string, options?: MakaPreparePromptOptions, From aa19f9f3ed0e96692949b35eba7b436769f2963e Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Mon, 24 Aug 2026 16:06:19 +0800 Subject: [PATCH 04/10] style(cli): format resume availability flow --- packages/cli/src/pi-tui-runner.ts | 11 +++++------ packages/cli/src/session-driver.ts | 4 +++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 49a6bbb917..207ca0f140 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2311,11 +2311,11 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { sessions.map(async (session) => { try { const availability = options.onlyResumable - ? (await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? + ? ((await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? (await input.driver.getSessionResumeAvailability?.(session)) ?? - (await inspectSessionResumeAvailability(session)) - : (await input.driver.getSessionResumeAvailability?.(session)) ?? - (await inspectSessionResumeAvailability(session)); + (await inspectSessionResumeAvailability(session))) + : ((await input.driver.getSessionResumeAvailability?.(session)) ?? + (await inspectSessionResumeAvailability(session))); return [session.id, availability] as const; } catch (error) { const detail = error instanceof Error ? error.message : String(error); @@ -2440,8 +2440,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { sessions.find((candidate) => candidate.id === sessionId) ?? sessions.find((candidate) => candidate.cwd === cwd); if (!session) return; - const availability = - await input.driver.getSessionResumeCandidateAvailability(session); + const availability = await input.driver.getSessionResumeCandidateAvailability(session); if (availability.available) { state.entries.push({ kind: 'notice', diff --git a/packages/cli/src/session-driver.ts b/packages/cli/src/session-driver.ts index efa775f02d..b90a1f1130 100644 --- a/packages/cli/src/session-driver.ts +++ b/packages/cli/src/session-driver.ts @@ -100,7 +100,9 @@ export class SkillInvocationBlockedError extends Error { export interface MakaSessionDriver { listSessions(): Promise; getSessionResumeAvailability?(session: SessionSummary): Promise; - getSessionResumeCandidateAvailability?(session: SessionSummary): Promise; + getSessionResumeCandidateAvailability?( + session: SessionSummary, + ): Promise; preparePrompt( prompt: string, options?: MakaPreparePromptOptions, From 0d2a6c1888c6a4c6ac37d3dc611f6dc3255e67f7 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Mon, 24 Aug 2026 23:13:53 +0800 Subject: [PATCH 05/10] fix(cli): reject unavailable sessions before switching --- packages/cli/src/__tests__/pi-tui-runner.test.ts | 2 +- packages/cli/src/pi-tui-runner.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 253a7193a9..057bf57f21 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3517,7 +3517,7 @@ describe('Maka Pi TUI runner', () => { terminal.input('/session'); terminal.input('\r'); await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); - assert.match(plainTerminalOutput(terminal.output()), /attachable/); + assert.match(plainTerminalOutput(terminal.output()), /attachab/); terminal.input('\x1b'); terminal.input('/exit'); diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 207ca0f140..32c4b3fb27 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2310,6 +2310,9 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { Promise.all( sessions.map(async (session) => { try { + if (!session.cwd) { + return [session.id, { available: false, reason: 'Missing working directory' }] as const; + } const availability = options.onlyResumable ? ((await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? (await input.driver.getSessionResumeAvailability?.(session)) ?? @@ -2404,7 +2407,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { void importForeignSession(foreign); return; } - if (options.onlyResumable && availability.get(item.value)?.available === false) return; + if (availability.get(item.value)?.available === false) return; closeOverlay(); void (async () => { await goToSession(item.value); From a4cab4e61f4e5c11413ee56b9220dcb12929c536 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Tue, 25 Aug 2026 14:37:12 +0800 Subject: [PATCH 06/10] fix(cli): keep unavailable sessions attachable --- packages/cli/src/pi-tui-runner.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 32c4b3fb27..cadb0942bf 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2311,7 +2311,10 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { sessions.map(async (session) => { try { if (!session.cwd) { - return [session.id, { available: false, reason: 'Missing working directory' }] as const; + return [ + session.id, + { available: false, reason: 'Missing working directory' }, + ] as const; } const availability = options.onlyResumable ? ((await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? From 4e8521360575d26950acdad2d4f1a81028278100 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Fri, 28 Aug 2026 00:20:12 +0800 Subject: [PATCH 07/10] fix(cli): keep session attachment independent from resume --- .../cli/src/__tests__/pi-tui-runner.test.ts | 23 +++++++++++++++---- packages/cli/src/pi-tui-runner.ts | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 8c46452f74..555764f9ac 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3445,7 +3445,7 @@ describe('Maka Pi TUI runner', () => { const driver = new SlashCommandDriver([attachable, archived]); driver.getSessionResumeAvailability = async (session) => { if (session.id === archived.id) throw new Error('session archived'); - return { available: true }; + return { available: false, reason: 'no resumable turn' }; }; const run = runMakaPiTui({ title: 'Maka', @@ -3462,10 +3462,16 @@ describe('Maka Pi TUI runner', () => { await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); assert.match(plainTerminalOutput(terminal.output()), /attachab/); - terminal.input('\x1b'); - terminal.input('/exit'); - terminal.input('\r'); - await run; + let switched = false; + try { + terminal.input('\r'); + await waitFor(() => driver.sessionIds.includes(attachable.id)); + switched = true; + } finally { + exitMaka(terminal); + await run; + } + assert.equal(switched, true); }); test('surfaces a notice when the foreign-session scan fails', async () => { @@ -4080,6 +4086,7 @@ describe('Maka Pi TUI runner', () => { driver.releaseList(); // The rendered picker is the observable arming signal for the Escape. await waitFor(() => plainTerminalOutput(terminal.screenOutput()).includes('Existing chat')); + assert.equal(driver.listCalls, 1); terminal.input('\x1b'); exitMaka(terminal); @@ -8205,6 +8212,12 @@ class DeferredListSessionsDriver extends SlashCommandDriver { listCalls = 0; private resolveList: (() => void) | null = null; + async getSessionResumeCandidateAvailability( + _session: SessionSummary, + ): Promise { + return { available: true }; + } + override async listSessions(): Promise { this.listCalls += 1; await new Promise((resolve) => { diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index e7c10ccc0e..edf4258dfa 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2390,7 +2390,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { void importForeignSession(foreign); return; } - if (availability.get(item.value)?.available === false) return; + if (options.onlyResumable && availability.get(item.value)?.available === false) return; closeOverlay(); void (async () => { await goToSession(item.value); From ed945f569af75199991994c679ba3a672aa2fb12 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Fri, 28 Aug 2026 02:00:05 +0800 Subject: [PATCH 08/10] fix(cli): preserve no-cwd session guard Generated-by: Codex --- packages/cli/src/pi-tui-runner.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index edf4258dfa..53913e243f 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2390,7 +2390,13 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { void importForeignSession(foreign); return; } - if (options.onlyResumable && availability.get(item.value)?.available === false) return; + const itemAvailability = availability.get(item.value); + if ( + itemAvailability?.available === false && + itemAvailability.reason === 'Missing working directory' + ) { + return; + } closeOverlay(); void (async () => { await goToSession(item.value); From ca1962516b67bde0d9cc3f18b9f3de16936ae7b0 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Fri, 28 Aug 2026 16:09:10 +0800 Subject: [PATCH 09/10] fix(cli): exclude foreign sessions from resume picker --- .../cli/src/__tests__/pi-tui-runner.test.ts | 52 +++++++++++++++++++ packages/cli/src/pi-tui-runner.ts | 20 +++---- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index 555764f9ac..685189fb4e 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3438,6 +3438,58 @@ describe('Maka Pi TUI runner', () => { await run; }); + test('/resume excludes foreign sessions when none is attached', async () => { + const terminal = new FakeTerminal(); + const driver = new SlashCommandDriver([]); + (driver as unknown as { sessionId: string | null }).sessionId = null; + const foreignSession = { + source: 'claude-code' as const, + id: 'foreign-resume', + title: 'Foreign interrupted work', + cwd: '/repo', + updatedAtMs: Date.now(), + transcriptPath: '/home/u/.claude/projects/-repo/foreign-resume.jsonl', + }; + let listSessionsCalls = 0; + let readDigestCalls = 0; + const foreignSessions = { + availableSources: async () => ['claude-code' as const], + listSessions: async () => { + listSessionsCalls += 1; + return [foreignSession]; + }, + readDigest: async () => { + readDigestCalls += 1; + throw new Error('foreign import must not run from /resume'); + }, + }; + const run = runMakaPiTui({ + title: 'Maka', + driver, + cwd: '/repo', + model: 'm', + connectionSlug: 'c', + permissionMode: 'bypass', + terminal, + foreignSessions, + }); + + terminal.input('/resume'); + terminal.input('\r'); + await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); + const output = plainTerminalOutput(terminal.output()); + assert.doesNotMatch(output, /Foreign interrupted work/); + assert.equal(listSessionsCalls, 0); + + terminal.input('\x1b'); + terminal.input('/exit'); + terminal.input('\r'); + await run; + assert.equal(readDigestCalls, 0); + assert.equal(driver.startNewSessionCalls, 0); + assert.equal(driver.prompts.length, 0); + }); + test('/session keeps attachable rows when resume discovery fails for another session', async () => { const terminal = new FakeTerminal(); const attachable = fakeSessionSummary('attachable', '/repo'); diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 53913e243f..6709927b71 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -2313,7 +2313,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { // Session and hands off a turn, which cannot detach from the running // one (#3380). Skip the scan mid-turn instead of offering rows whose // selection would silently no-op on importForeignSession's busy guard. - input.foreignSessions && !turnRunning + !options.onlyResumable && input.foreignSessions && !turnRunning ? input.foreignSessions.listSessions({ cwd }).then( (summaries) => ({ summaries }), (error: unknown) => ({ error }), @@ -2365,14 +2365,16 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { : `${shortSessionId(session.id)}${statusDetail}${location}${childDetail} ${session.llmConnectionSlug} ${session.model}`, }; }); - // Foreign sessions are cwd-scoped; show them in both scope views (they - // belong to this project) so a Tab toggle never makes them vanish. - for (const [value, summary] of foreignByValue) { - items.push({ - value, - label: summary.title, - description: `↩ resume from ${foreignSourceLabel(summary.source)}`, - }); + if (!options.onlyResumable) { + // Foreign sessions are cwd-scoped; show them in both scope views (they + // belong to this project) so a Tab toggle never makes them vanish. + for (const [value, summary] of foreignByValue) { + items.push({ + value, + label: summary.title, + description: `↩ resume from ${foreignSourceLabel(summary.source)}`, + }); + } } const list = new SelectList(items, 10, selectListTheme(), { minPrimaryColumnWidth: 20, From 5904e27bf916243cecb5a1438e3a4fd959f991d2 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Mon, 31 Aug 2026 05:36:34 +0800 Subject: [PATCH 10/10] fix(cli): guard resumable session selection --- .../cli/src/__tests__/pi-tui-runner.test.ts | 81 +++++++++++++++++++ packages/cli/src/pi-tui-runner.ts | 73 +++++++++++------ 2 files changed, 131 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index f3c659691d..e177c0ec1d 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -4006,6 +4006,61 @@ describe('Maka Pi TUI runner', () => { await run; }); + test('/resume does not resume after a stale selection fails to switch', async () => { + const terminal = new FakeTerminal(); + const session = fakeSessionSummary('stale', '/repo'); + const driver = new RejectingSwitchSessionDriver([session]); + (driver as unknown as { sessionId: string | null }).sessionId = null; + const run = runMakaPiTui({ + title: 'Maka', + driver, + cwd: '/repo', + model: 'm', + connectionSlug: 'c', + permissionMode: 'bypass', + terminal, + }); + + terminal.input('/resume'); + terminal.input('\r'); + await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); + terminal.input('\r'); + await waitFor(() => driver.switchCalls === 1); + await delay(0); + assert.equal(driver.resumeCalls, 0); + + exitMaka(terminal); + await run; + }); + + test('/resume bounds concurrent resumability checks for large session catalogs', async () => { + const terminal = new FakeTerminal(); + const sessions = Array.from({ length: 24 }, (_, index) => + fakeSessionSummary(`session-${index}`, '/repo'), + ); + const driver = new BoundedResumeAvailabilityDriver(sessions); + (driver as unknown as { sessionId: string | null }).sessionId = null; + const run = runMakaPiTui({ + title: 'Maka', + driver, + cwd: '/repo', + model: 'm', + connectionSlug: 'c', + permissionMode: 'bypass', + terminal, + }); + + terminal.input('/resume'); + terminal.input('\r'); + await waitFor(() => plainTerminalOutput(terminal.output()).includes('Resume Session Current')); + assert.ok(driver.availabilityCalls >= sessions.length); + assert.ok(driver.maxActiveCalls <= 8); + + terminal.input('\x1b'); + exitMaka(terminal); + await run; + }); + test('/resume excludes foreign sessions when none is attached', async () => { const terminal = new FakeTerminal(); const driver = new SlashCommandDriver([]); @@ -8538,6 +8593,32 @@ class SlashCommandDriver extends FakeSessionDriver { } } +class RejectingSwitchSessionDriver extends SlashCommandDriver { + switchCalls = 0; + + override async switchSession(_sessionId: string): Promise { + this.switchCalls += 1; + throw new Error('session became unavailable'); + } +} + +class BoundedResumeAvailabilityDriver extends SlashCommandDriver { + availabilityCalls = 0; + activeCalls = 0; + maxActiveCalls = 0; + + async getSessionResumeCandidateAvailability( + _session: SessionSummary, + ): Promise { + this.availabilityCalls += 1; + this.activeCalls += 1; + this.maxActiveCalls = Math.max(this.maxActiveCalls, this.activeCalls); + await delay(1); + this.activeCalls -= 1; + return { available: true }; + } +} + class UserCommandDriver extends SlashCommandDriver { readonly commands: string[] = []; diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index f0b77ad7d2..9d6fe40428 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -354,6 +354,8 @@ function sessionConnectionIdentityNotice( return undefined; } +const SESSION_RESUME_AVAILABILITY_CONCURRENCY = 8; + export async function runMakaPiTui(input: MakaPiTuiInput): Promise { const locale = input.locale ?? 'en'; const primaryGuidance = getTuiPrimaryGuidance(locale); @@ -1798,7 +1800,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { // serial lock like any control action; mid-turn that lock is held by the // running Turn, so the switch goes through the detach path instead of // silently no-oping on the busy gate. - const goToSession = async (sessionId: string): Promise => { + const goToSession = async (sessionId: string): Promise => { const pair = sideConversation; if ( pair && @@ -1806,24 +1808,31 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { (sessionId === pair.parentSessionId || sessionId === pair.sideSessionId) ) { await toggleSideConversation(); - return; + return true; } const leavesPair = pair !== undefined && sessionId !== pair.parentSessionId && sessionId !== pair.sideSessionId; if (!turnRunning) { + let switched = false; await runControl(async () => { await switchSession(sessionId); + switched = true; if (leavesPair) await discardCurrentSidePair(); }); - return; + return switched; } // One detach at a time (#3380): a second mid-turn switch while the first // is still handing the view over would clear `detaching` early, reopen // the interrupt window, and double-apply the adoption. - if (detaching) return; - await switchAwayMidTurn(sessionId) - .then(() => (leavesPair ? discardCurrentSidePair() : undefined)) - .catch(reportError); + if (detaching) return false; + try { + await switchAwayMidTurn(sessionId); + if (leavesPair) await discardCurrentSidePair(); + return true; + } catch (error) { + reportError(error); + return false; + } }; const openSideConversation = async (prompt: string): Promise => { @@ -2408,6 +2417,20 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { }; let sessionListPromise: Promise | undefined; + let activeResumeAvailabilityChecks = 0; + const queuedResumeAvailabilityChecks: Array<() => void> = []; + const runResumeAvailabilityCheck = async (task: () => Promise): Promise => { + if (activeResumeAvailabilityChecks >= SESSION_RESUME_AVAILABILITY_CONCURRENCY) { + await new Promise((resolve) => queuedResumeAvailabilityChecks.push(resolve)); + } + activeResumeAvailabilityChecks += 1; + try { + return await task(); + } finally { + activeResumeAvailabilityChecks -= 1; + queuedResumeAvailabilityChecks.shift()?.(); + } + }; const listSessions = (): Promise => { if (!sessionListPromise) { sessionListPromise = input.driver.listSessions().finally(() => { @@ -2472,19 +2495,21 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { Promise.all( sessions.map(async (session) => { try { - if (!session.cwd) { - return [ - session.id, - { available: false, reason: 'Missing working directory' }, - ] as const; - } - const availability = options.onlyResumable - ? ((await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? - (await input.driver.getSessionResumeAvailability?.(session)) ?? - (await inspectSessionResumeAvailability(session))) - : ((await input.driver.getSessionResumeAvailability?.(session)) ?? - (await inspectSessionResumeAvailability(session))); - return [session.id, availability] as const; + return await runResumeAvailabilityCheck(async () => { + if (!session.cwd) { + return [ + session.id, + { available: false, reason: 'Missing working directory' }, + ] as const; + } + const availability = options.onlyResumable + ? ((await input.driver.getSessionResumeCandidateAvailability?.(session)) ?? + (await input.driver.getSessionResumeAvailability?.(session)) ?? + (await inspectSessionResumeAvailability(session))) + : ((await input.driver.getSessionResumeAvailability?.(session)) ?? + (await inspectSessionResumeAvailability(session))); + return [session.id, availability] as const; + }); } catch (error) { const detail = error instanceof Error ? error.message : String(error); return [session.id, { available: false, reason: detail }] as const; @@ -2583,8 +2608,8 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { } closeOverlay(); void (async () => { - await goToSession(item.value); - if (options.onlyResumable) await runControl(resumeSession); + const switched = await goToSession(item.value); + if (switched && options.onlyResumable) await runControl(resumeSession); })().catch(reportError); }; list.onCancel = () => closeOverlay(); @@ -2616,7 +2641,9 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { sessions.find((candidate) => candidate.id === sessionId) ?? sessions.find((candidate) => candidate.cwd === cwd); if (!session) return; - const availability = await input.driver.getSessionResumeCandidateAvailability(session); + const availability = await runResumeAvailabilityCheck(() => + input.driver.getSessionResumeCandidateAvailability!(session), + ); if (availability.available) { state.entries.push({ kind: 'notice',