Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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:

Expand Down
5 changes: 4 additions & 1 deletion docs/typescript-sdk-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ function createSession(options?: CreateSessionOptions): Promise<DroidSession>;

### `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(
Expand Down
2 changes: 1 addition & 1 deletion examples/fork-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async function main(): Promise<void> {
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,
Expand Down
2 changes: 1 addition & 1 deletion examples/init-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
15 changes: 13 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export interface ResumeSessionOptions extends Pick<
CreateSessionOptions,
| 'execPath'
| 'execArgs'
| 'cwd'
| 'env'
| 'permissionHandler'
| 'askUserHandler'
Expand Down Expand Up @@ -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 = {}
Expand Down
49 changes: 49 additions & 0 deletions tests/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)['method'] ===
DroidServerMethod.LOAD_SESSION
) as Record<string, unknown>;
const params = loadMsg['params'] as Record<string, unknown>;

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<string, unknown>)['method'] ===
DroidServerMethod.LOAD_SESSION
) as Record<string, unknown>;
const params = loadMsg['params'] as Record<string, unknown>;

expect(params).not.toHaveProperty('cwd');

await session.close();
});
});
});

describe('DroidSession', () => {
Expand Down
Loading