Skip to content

Commit c1021c0

Browse files
committed
sdk: fail fast spawn requests on spawn manager errors
1 parent 2544879 commit c1021c0

File tree

2 files changed

+53
-0
lines changed

2 files changed

+53
-0
lines changed

packages/sdk/src/client.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,39 @@ describe('RelayClient', () => {
188188
// Should not throw
189189
expect(() => (client as any).processFrame(errorEnvelope)).not.toThrow();
190190
});
191+
192+
it('rejects pending spawn when SpawnManager is disabled', async () => {
193+
const client = new RelayClient({ reconnect: false, quiet: true });
194+
(client as any)._state = 'READY';
195+
const sendMock = vi.fn().mockReturnValue(true);
196+
(client as any).send = sendMock;
197+
198+
const spawnPromise = client.spawn(
199+
{
200+
name: 'Worker',
201+
cli: 'codex',
202+
task: 'Do work',
203+
},
204+
10000
205+
);
206+
207+
const errorEnvelope: Envelope<ErrorPayload> = {
208+
v: 1,
209+
type: 'ERROR',
210+
id: 'err-spawn-disabled',
211+
ts: Date.now(),
212+
payload: {
213+
code: 'INTERNAL' as any,
214+
message: 'SpawnManager not enabled. Configure spawnManager: true in daemon config.',
215+
fatal: false,
216+
},
217+
};
218+
219+
(client as any).processFrame(errorEnvelope);
220+
221+
await expect(spawnPromise).rejects.toThrow('SpawnManager not enabled');
222+
expect((client as any).pendingSpawns.size).toBe(0);
223+
});
191224
});
192225

193226
describe('sendMessage', () => {

packages/sdk/src/client.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,6 +1849,21 @@ export class RelayClient {
18491849
console.error('[sdk] Server error:', envelope.payload);
18501850
}
18511851

1852+
const errorMessage = envelope.payload.message || 'Server error';
1853+
const isSpawnManagerDisabled =
1854+
envelope.payload.code === 'INTERNAL' &&
1855+
/spawnmanager not enabled/i.test(errorMessage);
1856+
1857+
// Fail fast for spawn-related operations when daemon cannot service them.
1858+
// Without this, spawn/release/input/list-workers calls sit until timeout.
1859+
if (isSpawnManagerDisabled) {
1860+
const err = new Error(errorMessage);
1861+
this.rejectPendingSpawns(err);
1862+
this.rejectPendingReleases(err);
1863+
this.rejectPendingSendInputs(err);
1864+
this.rejectPendingListWorkers(err);
1865+
}
1866+
18521867
if (envelope.payload.code === 'RESUME_TOO_OLD') {
18531868
this.resumeToken = undefined;
18541869
this.sessionId = undefined;
@@ -1861,6 +1876,11 @@ export class RelayClient {
18611876
}
18621877
this._destroyed = true;
18631878
}
1879+
1880+
// Surface server-side ERROR frames to SDK consumers.
1881+
if (this.onError) {
1882+
this.onError(new Error(errorMessage));
1883+
}
18641884
}
18651885

18661886
private handleDisconnect(): void {

0 commit comments

Comments
 (0)