From f0e438b53d629d51b10c2ff84a00b244ada73b0d Mon Sep 17 00:00:00 2001 From: User Date: Wed, 20 May 2026 14:13:20 -0700 Subject: [PATCH] refactor!: remove cwd from ResumeSessionOptions (CLI-72) cwd on resumeSession was misleading: it only controlled subprocess startup and was never sent to droid.load_session, so it could not change the resumed session's working directory. Remove it from ResumeSessionOptions to make the contract explicit: createSession uses the caller's cwd; resumeSession uses the persisted session's cwd. BREAKING CHANGE: resumeSession() no longer accepts cwd. To run in a different directory, create a new session or fork the existing one. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- README.md | 10 +++---- docs/typescript-sdk-reference.md | 5 +++- examples/fork-session.ts | 2 +- examples/init-metadata.ts | 2 +- src/session.ts | 15 ++++++++-- tests/session.test.ts | 49 ++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 41a7a26..d10738c 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,7 @@ Use `session.sessionId` to persist the session ID, then resume it later: ```ts import { resumeSession } from '@factory/droid-sdk'; -const session = await resumeSession(savedSessionId, { - cwd: '/my/project', -}); +const session = await resumeSession(savedSessionId); for await (const msg of session.stream('Continue where we left off')) { // Handle streamed DroidMessage events. } @@ -180,7 +178,7 @@ const session = await createSession({ cwd: '/my/project' }); console.log(session.sessionId); console.log(session.initResult.settings.modelId); -const resumed = await resumeSession(session.sessionId, { cwd: '/my/project' }); +const resumed = await resumeSession(session.sessionId); console.log(resumed.initResult.cwd); await resumed.close(); @@ -271,7 +269,7 @@ for await (const _msg of session.stream( } const { newSessionId } = await session.forkSession(); -const fork = await resumeSession(newSessionId, { cwd: '/my/project' }); +const fork = await resumeSession(newSessionId); for await (const msg of fork.stream('What phrase did I ask you to remember?')) { if (msg.type === DroidMessageType.AssistantTextDelta) { @@ -458,7 +456,7 @@ Session creation options used by `run()` and `createSession()` include: - **`askUserHandler`** — callback for interactive questions - **`abortSignal`** — standard `AbortSignal` for cancellation -`resumeSession()` accepts the process, transport, handler, `cwd`, `mcpServers`, and `abortSignal` options needed to reconnect to an existing session, but does not accept new-session-only options such as `modelId` or `interactionMode`. +`resumeSession()` accepts the process, transport, handler, `mcpServers`, and `abortSignal` options needed to reconnect to an existing session, but does not accept new-session-only options such as `modelId` or `interactionMode`. `cwd` is intentionally not accepted on resume: the persisted session's working directory is always used. To run in a different directory, create a new session or fork the existing one. Message APIs (`run()` and `session.stream()`) also accept: diff --git a/docs/typescript-sdk-reference.md b/docs/typescript-sdk-reference.md index a9c3b3e..4f5b755 100644 --- a/docs/typescript-sdk-reference.md +++ b/docs/typescript-sdk-reference.md @@ -77,7 +77,10 @@ function createSession(options?: CreateSessionOptions): Promise; ### `resumeSession()` -Reconnects to an existing session by ID. +Reconnects to an existing session by ID. The resumed session always runs in the +working directory persisted with the session; `ResumeSessionOptions` does not +accept `cwd`. To run in a different directory, create a new session or fork the +existing one. ```ts function resumeSession( diff --git a/examples/fork-session.ts b/examples/fork-session.ts index c35524f..cf5459b 100644 --- a/examples/fork-session.ts +++ b/examples/fork-session.ts @@ -40,7 +40,7 @@ async function main(): Promise { const { newSessionId } = await session.forkSession(); console.log(`Forked session: ${newSessionId}\n`); - fork = await resumeSession(newSessionId, { cwd: process.cwd() }); + fork = await resumeSession(newSessionId); const result = await streamText( fork, diff --git a/examples/init-metadata.ts b/examples/init-metadata.ts index 4861910..709411b 100644 --- a/examples/init-metadata.ts +++ b/examples/init-metadata.ts @@ -8,7 +8,7 @@ import { createSession, resumeSession } from '@factory/droid-sdk'; const session = await createSession({ cwd: process.cwd() }); -const resumed = await resumeSession(session.sessionId, { cwd: process.cwd() }); +const resumed = await resumeSession(session.sessionId); console.log(`created session: ${session.sessionId}`); console.log(`resumed session: ${resumed.sessionId}`); diff --git a/src/session.ts b/src/session.ts index 0614055..90407af 100644 --- a/src/session.ts +++ b/src/session.ts @@ -67,7 +67,6 @@ export interface ResumeSessionOptions extends Pick< CreateSessionOptions, | 'execPath' | 'execArgs' - | 'cwd' | 'env' | 'permissionHandler' | 'askUserHandler' @@ -410,7 +409,19 @@ export async function createSession( } } -/** @throws {SessionNotFoundError} If the session ID does not exist. */ +/** + * Resumes an existing Droid session. + * + * The resumed session always runs in the working directory that was persisted + * with the session at creation time. `resumeSession()` intentionally does not + * accept a `cwd` option: the persisted session cwd is authoritative. The + * underlying subprocess is launched in the host process's `process.cwd()`, but + * this does not affect the session's working directory as far as the Droid is + * concerned. To run in a different directory, create a new session or fork the + * existing one. + * + * @throws {SessionNotFoundError} If the session ID does not exist. + */ export async function resumeSession( sessionId: string, options: ResumeSessionOptions = {} diff --git a/tests/session.test.ts b/tests/session.test.ts index 64cbca4..6dd559d 100644 --- a/tests/session.test.ts +++ b/tests/session.test.ts @@ -333,6 +333,55 @@ describe('resumeSession()', () => { expect(transport.isConnected).toBe(false); }); }); + + describe('CLI-72: cwd behavior on resume', () => { + it('does not send cwd to loadSession when omitted', async () => { + const transport = new InMemoryTransport(); + await transport.connect(); + + setupLoadResponder(transport, 'sess-resume-cwd-001'); + + const session = await resumeSession('sess-resume-cwd-001', { + transport, + }); + + const loadMsg = transport.sentMessages.find( + (m) => + (m as Record)['method'] === + DroidServerMethod.LOAD_SESSION + ) as Record; + const params = loadMsg['params'] as Record; + + expect(params).not.toHaveProperty('cwd'); + expect(params['sessionId']).toBe('sess-resume-cwd-001'); + + await session.close(); + }); + + it('rejects cwd as an option at the type level', async () => { + const transport = new InMemoryTransport(); + await transport.connect(); + + setupLoadResponder(transport, 'sess-resume-cwd-002'); + + const session = await resumeSession('sess-resume-cwd-002', { + transport, + // @ts-expect-error - cwd is not a valid ResumeSessionOptions field + cwd: '/tmp/should-not-be-allowed', + }); + + const loadMsg = transport.sentMessages.find( + (m) => + (m as Record)['method'] === + DroidServerMethod.LOAD_SESSION + ) as Record; + const params = loadMsg['params'] as Record; + + expect(params).not.toHaveProperty('cwd'); + + await session.close(); + }); + }); }); describe('DroidSession', () => {