From 34b6cb91eda44b9febd928eaba192aecec63b909 Mon Sep 17 00:00:00 2001 From: pku-xht Date: Wed, 5 Aug 2026 00:26:39 +0800 Subject: [PATCH] Keep provider adapters private --- .../subagent-claude-code/src/process.ts | 7 ++----- .../subagent/subagent-claude-code/src/run.ts | 21 ++++++++++++------- .../tests/subagent-claude-code.spec.ts | 11 +--------- packages/subagent/subagent-codex/src/run.ts | 12 +++++++---- .../subagent/subagent/src/out-of-process.ts | 10 +++------ 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/subagent/subagent-claude-code/src/process.ts b/packages/subagent/subagent-claude-code/src/process.ts index b8b09216fa..e27e0a6649 100644 --- a/packages/subagent/subagent-claude-code/src/process.ts +++ b/packages/subagent/subagent-claude-code/src/process.ts @@ -76,11 +76,8 @@ export class ManagedClaudeCodeProcess implements SpawnedProcess { * @param child - shared handle that remains the process-tree authority. */ constructor(private readonly child: SubprocessHandle) { - if (child.stdin === undefined || child.stdout === undefined) { - throw new Error('subagent-claude-code: SDK child requires piped stdin and stdout') - } - this.stdin = child.stdin - this.stdout = child.stdout + this.stdin = child.stdin as NonNullable + this.stdout = child.stdout as NonNullable // EventEmitter gives `error` special throw semantics without a listener. // The SDK attaches its listener synchronously after custom spawn returns, // while this no-op also contains an already-rejected spawn handle. diff --git a/packages/subagent/subagent-claude-code/src/run.ts b/packages/subagent/subagent-claude-code/src/run.ts index 2ad305f155..d5f222b6c4 100644 --- a/packages/subagent/subagent-claude-code/src/run.ts +++ b/packages/subagent/subagent-claude-code/src/run.ts @@ -20,7 +20,6 @@ import { SessionId } from '@deepseek-ai/dsh-session' import { settleRunResult, subprocessRunHandle, - thrownError, type SubagentResult, type SubagentRun, type SubagentStartRequest, @@ -39,6 +38,8 @@ import { /** Default POSIX grace between subprocess termination tiers. */ export const DEFAULT_DISPOSE_GRACE_MS = 3_000 +/* jscpd:ignore-start -- sibling providers intentionally keep product-private + * run inputs and error normalization instead of adding a shared lifecycle owner. */ /** Fully resolved inputs for one official Claude Agent SDK query. */ export interface ClaudeCodeRunSpec { /** Parent Session workspace supplied to the SDK and real CLI. */ @@ -53,6 +54,12 @@ export interface ClaudeCodeRunSpec { readonly onError?: (error: Error, stopReason: SubagentStopReason) => void } +function thrown(value: unknown): Error { + /* v8 ignore next -- typed SDK and subprocess failures reject with Error. */ + return value instanceof Error ? value : new Error(String(value)) +} +/* jscpd:ignore-end */ + /** * Validate and preserve the one-shot task before crossing the SDK boundary. * @param prompt - task content accepted from the shared subagent service. @@ -131,7 +138,7 @@ export async function disposeClaudeCodeChild( try { query?.close() } catch (error: unknown) { - failures.push(thrownError(error)) + failures.push(thrown(error)) } if (child.pid > 0) { @@ -139,13 +146,13 @@ export async function disposeClaudeCodeChild( try { await child.waitForExit() } catch (error: unknown) { - failures.push(thrownError(error)) + failures.push(thrown(error)) } } try { await child.done } catch (error: unknown) { - failures.push(thrownError(error)) + failures.push(thrown(error)) } const firstFailure = failures[0] @@ -234,7 +241,7 @@ export async function startClaudeCodeRun( await disposeClaudeCodeChild(query, child) } catch (disposeError: unknown) { throw new AggregateError( - [thrownError(error), thrownError(disposeError)], + [thrown(error), thrown(disposeError)], 'subagent-claude-code: startup failed and CLI cleanup also failed', ) } @@ -243,7 +250,7 @@ export async function startClaudeCodeRun( query.close() } catch (disposeError: unknown) { throw new AggregateError( - [thrownError(error), thrownError(disposeError)], + [thrown(error), thrown(disposeError)], 'subagent-claude-code: startup failed and query cleanup also failed', ) } @@ -252,7 +259,7 @@ export async function startClaudeCodeRun( if (cancelledBeforeCleanup || request.signal.aborted) { throw new Error('subagent-claude-code: request was aborted before SDK startup') } - throw thrownError(error) + throw thrown(error) } const publishedQuery = query diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts index 8c8158fb28..0fa781c082 100644 --- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts +++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts @@ -445,7 +445,7 @@ describe('official spawn projection', () => { expect(process.kill('SIGTERM')).toBe(false) }) - it('emits spawn errors and rejects handles without the required pipes', async () => { + it('emits spawn errors', async () => { const child = fakeChild() const process = new ManagedClaudeCodeProcess(child.handle) const errorListener = vi.fn() @@ -459,15 +459,6 @@ describe('official spawn projection', () => { message: 'spawn boom', })) expect(removed).not.toHaveBeenCalled() - - const missingStdin = fakeChild({ stdin: undefined }) - Object.defineProperty(missingStdin.handle, 'stdin', { value: undefined }) - expect(() => new ManagedClaudeCodeProcess(missingStdin.handle)) - .toThrow('requires piped stdin and stdout') - const missingStdout = fakeChild({ stdout: undefined }) - Object.defineProperty(missingStdout.handle, 'stdout', { value: undefined }) - expect(() => new ManagedClaudeCodeProcess(missingStdout.handle)) - .toThrow('requires piped stdin and stdout') }) it('exposes a settled direct-child exit code', async () => { diff --git a/packages/subagent/subagent-codex/src/run.ts b/packages/subagent/subagent-codex/src/run.ts index 54a2bc1d79..9f52e18f12 100644 --- a/packages/subagent/subagent-codex/src/run.ts +++ b/packages/subagent/subagent-codex/src/run.ts @@ -13,7 +13,6 @@ import { SessionId } from '@deepseek-ai/dsh-session' import { settleRunResult, subprocessRunHandle, - thrownError, type SubagentResult, type SubagentRun, type SubagentStartRequest, @@ -39,6 +38,11 @@ export interface CodexRunSpec { readonly onError?: (error: Error, stopReason: SubagentStopReason) => void } +function thrown(value: unknown): Error { + /* v8 ignore next -- typed subprocess/wire failures reject with Error. */ + return value instanceof Error ? value : new Error(String(value)) +} + /** * Validate and preserve the one-shot task before crossing the process seam. * @param prompt - task content accepted from the shared subagent service. @@ -120,7 +124,7 @@ export async function startCodexRun( 'subagent-codex: app-server exited before the run settled ' + `(code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`, )), - (error: unknown) => Promise.reject(thrownError(error)), + (error: unknown) => Promise.reject(thrown(error)), ) // A normal post-result dispose also closes the process. Keep that expected // late rejection observed after the result race has already settled. @@ -145,14 +149,14 @@ export async function startCodexRun( await disposeProcess() } catch (disposeError: unknown) { throw new AggregateError( - [thrownError(error), thrownError(disposeError)], + [thrown(error), thrown(disposeError)], 'subagent-codex: startup failed and app-server cleanup also failed', ) } if (runAbort.signal.aborted) { throw new Error('subagent-codex: request was aborted before run publication') } - throw thrownError(error) + throw thrown(error) } const collectOutput = (): ContentBlock[] => wire.collectOutput() diff --git a/packages/subagent/subagent/src/out-of-process.ts b/packages/subagent/subagent/src/out-of-process.ts index eac2125897..d049dba2be 100644 --- a/packages/subagent/subagent/src/out-of-process.ts +++ b/packages/subagent/subagent/src/out-of-process.ts @@ -119,12 +119,8 @@ export function resolveChildCwd(prefix: string, configured: string | undefined, return assertUsableCwd(prefix, 'parent session cwd', parentCwd) } -/** - * Normalize an unknown thrown value to an Error. - * @param value - the unknown catch binding. - * @returns the original Error or a defensive Error wrapper. - */ -export function thrownError(value: unknown): Error { +/** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */ +function toError(value: unknown): Error { // The rejecting surfaces (wire clients, spawn failures) only throw // `Error`s; the `String(value)` arm is a defensive fallback for a non-Error // throw the typed surfaces cannot produce. @@ -168,7 +164,7 @@ export async function settleRunResult(parts: RunResultSettlement): Promise