@lifenrir/dsh-external-task-context-patch 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/run.ts ADDED
@@ -0,0 +1,650 @@
1
+ /**
2
+ * One-shot Claude Code lifecycle: invoke the official Agent SDK, place its
3
+ * real CLI process under the shared subprocess owner, map only strict SDK
4
+ * success to completion, record a best-effort per-run log, and dispose to
5
+ * whole-range quiescence.
6
+ *
7
+ * @module @deepseek-ai/dsh-subagent-claude-code/run
8
+ */
9
+
10
+ import { randomUUID } from 'node:crypto'
11
+ import {
12
+ query as officialQuery,
13
+ type Options,
14
+ type Query,
15
+ type SDKMessage,
16
+ type SDKResultMessage,
17
+ type SpawnOptions,
18
+ } from '@anthropic-ai/claude-agent-sdk'
19
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
20
+ import { brandString } from '@deepseek-ai/dsh-brand'
21
+ import type { SessionId } from '@deepseek-ai/dsh-session'
22
+ import {
23
+ settleRunResult,
24
+ subprocessRunHandle,
25
+ type SubagentResult,
26
+ type SubagentRun,
27
+ type SubagentStartRequest,
28
+ type SubagentStopReason,
29
+ } from '@deepseek-ai/dsh-subagent'
30
+ import {
31
+ scrubbedParentEnv,
32
+ type SubprocessHandle,
33
+ type SubprocessOutcome,
34
+ type SubprocessSpawnSpec,
35
+ } from '@deepseek-ai/dsh-subprocess'
36
+ import {
37
+ cleanupExpiredRunLogs,
38
+ createRunLogWriter,
39
+ DEFAULT_LOG_RETENTION_DAYS,
40
+ defaultRunLogDirectory,
41
+ } from './log.ts'
42
+ import {
43
+ claudeSpawnSpec,
44
+ ManagedClaudeCodeProcess,
45
+ } from './process.ts'
46
+
47
+ /** Default POSIX grace between subprocess termination tiers. */
48
+ export const DEFAULT_DISPOSE_GRACE_MS = 3_000
49
+
50
+ /** Claude Code permission modes that cannot wait for a human response. */
51
+ export const CLAUDE_CODE_PERMISSION_MODES = [
52
+ 'dontAsk',
53
+ 'acceptEdits',
54
+ 'auto',
55
+ 'plan',
56
+ 'bypassPermissions',
57
+ ] as const satisfies readonly NonNullable<Options['permissionMode']>[]
58
+
59
+ /** Profile-selectable non-interactive Claude Code permission mode. */
60
+ export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[number]
61
+
62
+ /** Safe default for unattended Claude Code runs. */
63
+ export const DEFAULT_CLAUDE_CODE_PERMISSION_MODE: ClaudeCodePermissionMode = 'dontAsk'
64
+
65
+ const SUPPORTED_UNATTENDED_DIALOG_KINDS = [
66
+ 'refusal_fallback_prompt',
67
+ ] satisfies NonNullable<Options['supportedDialogKinds']>
68
+
69
+ type ClaudeCodeFailureStage =
70
+ | 'query-start'
71
+ | 'query-run'
72
+ | 'process'
73
+ | 'teardown'
74
+
75
+ type ClaudeCodeFailureCategory =
76
+ | 'limit'
77
+ | 'product-error'
78
+ | 'invalid-result'
79
+ | 'process'
80
+ | 'unknown'
81
+
82
+ interface ClaudeCodeFailureFacts {
83
+ readonly stage: ClaudeCodeFailureStage
84
+ readonly category: ClaudeCodeFailureCategory
85
+ readonly outcome?: SubprocessOutcome | undefined
86
+ }
87
+
88
+ function failureDiagnostic(facts: ClaudeCodeFailureFacts): string {
89
+ const fields = [
90
+ 'product: Claude Code',
91
+ `stage: ${facts.stage}`,
92
+ `category: ${facts.category}`,
93
+ ]
94
+ const exitCode = facts.outcome?.exitCode
95
+ if (exitCode !== null && exitCode !== undefined) {
96
+ fields.push(`exit code: ${exitCode}`)
97
+ }
98
+ const signal = facts.outcome?.signal
99
+ if (signal !== null && signal !== undefined) {
100
+ fields.push(`signal: ${signal}`)
101
+ }
102
+ return `Product subagent failure (${fields.join('; ')})`
103
+ }
104
+
105
+ class ClaudeCodeFailure extends Error {
106
+ constructor(
107
+ readonly facts: ClaudeCodeFailureFacts,
108
+ cause?: unknown,
109
+ ) {
110
+ super(
111
+ `subagent-claude-code: ${failureDiagnostic(facts)}`,
112
+ cause === undefined ? undefined : { cause },
113
+ )
114
+ this.name = 'ClaudeCodeFailure'
115
+ }
116
+ }
117
+
118
+ function sdkFailureCategory(
119
+ subtype: string,
120
+ ): ClaudeCodeFailureCategory {
121
+ switch (subtype) {
122
+ case 'error_max_turns':
123
+ case 'error_max_budget_usd':
124
+ case 'error_max_structured_output_retries':
125
+ return 'limit'
126
+ case 'error_during_execution':
127
+ return 'product-error'
128
+ default:
129
+ return 'unknown'
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Hide an unpublished product startup failure behind fixed safe facts.
135
+ * @param cause - original host-side failure retained only on the Error cause chain.
136
+ * @returns a rejection safe to expose through the subagent start boundary.
137
+ */
138
+ export function claudeCodeStartupFailure(cause: unknown): Error {
139
+ return new ClaudeCodeFailure({
140
+ stage: 'query-start',
141
+ category: 'unknown',
142
+ }, cause)
143
+ }
144
+
145
+ function unattendedDiagnostic(
146
+ mode: ClaudeCodePermissionMode,
147
+ request: 'tool permission' | 'MCP elicitation' | 'user dialog',
148
+ decision: 'denied' | 'declined' | 'cancelled',
149
+ reason: string,
150
+ ): string {
151
+ return `Claude Code unattended decision (mode: ${mode}; request: ${request}; decision: ${decision}): ${reason}`
152
+ }
153
+
154
+ /* jscpd:ignore-start -- sibling providers intentionally keep product-private
155
+ * run inputs and error normalization instead of adding a shared lifecycle owner. */
156
+ /** Fully resolved inputs for one official Claude Agent SDK query. */
157
+ export interface ClaudeCodeRunSpec {
158
+ /** Selected child working directory supplied to the SDK and real CLI. */
159
+ readonly cwd: string
160
+ /** Profile-selected native model; omitted to preserve Claude settings. */
161
+ readonly model?: string
162
+ /** Profile-selected native non-interactive permission mode. */
163
+ readonly permissionMode: ClaudeCodePermissionMode
164
+ /** Explicit deployment/test environment layered after shared scrubbing. */
165
+ readonly env: Record<string, string>
166
+ /** Subprocess termination grace passed to the shared managed-range owner. */
167
+ readonly disposeGraceMs: number
168
+ /**
169
+ * Directory for per-run JSONL and metadata logs; defaults to
170
+ * `<DSH_HOME>/logs/subagent-claude-code` at run time.
171
+ */
172
+ readonly logDirectory?: string
173
+ /** Days to retain finished run logs; defaults to seven. */
174
+ readonly logRetentionDays?: number
175
+ /** Whether Claude Code persists its native session transcript; defaults to true. */
176
+ readonly persistNativeTranscript?: boolean
177
+ /** Whether streamed partial assistant messages join the SDK stream; defaults to true. */
178
+ readonly includePartialMessages?: boolean
179
+ /** Shared subprocess service spawn operation. */
180
+ readonly spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle
181
+ /** Host diagnostic sink for a product failure kept outside model-visible text. */
182
+ readonly onError?: (error: Error, stopReason: SubagentStopReason) => void
183
+ /** Host diagnostic sink for a swallowed run-log failure. */
184
+ readonly onLogError?: (error: Error) => void
185
+ }
186
+
187
+ function thrown(value: unknown): Error {
188
+ /* v8 ignore next -- typed SDK and subprocess failures reject with Error. */
189
+ return value instanceof Error ? value : new Error(String(value))
190
+ }
191
+
192
+ /** Read live request cancellation across awaited startup cleanup. */
193
+ function isAborted(signal: AbortSignal): boolean {
194
+ return signal.aborted
195
+ }
196
+
197
+ /* jscpd:ignore-end */
198
+
199
+ /**
200
+ * Validate and preserve the one-shot task before crossing the SDK boundary.
201
+ * @param prompt - task content accepted from the shared subagent service.
202
+ * @returns the exact text sequence as one SDK prompt.
203
+ */
204
+ export function textTask(prompt: readonly ContentBlock[]): string {
205
+ if (prompt.length === 0) {
206
+ throw new Error('subagent-claude-code: the one-shot task must contain only text blocks')
207
+ }
208
+ const texts: string[] = []
209
+ for (const block of prompt) {
210
+ if (block.type !== 'text') {
211
+ throw new Error('subagent-claude-code: the one-shot task must contain only text blocks')
212
+ }
213
+ texts.push(block.text)
214
+ }
215
+ if (texts.every(text => text.trim().length === 0)) {
216
+ throw new Error('subagent-claude-code: the one-shot task must not be empty')
217
+ }
218
+ return texts.join('')
219
+ }
220
+
221
+ /**
222
+ * Strictly derive the only SDK result that can complete a shared run.
223
+ * @param message - an official discriminated result union.
224
+ * @returns exact final text for a successful, non-error result.
225
+ */
226
+ export function successfulResult(message: SDKResultMessage): string {
227
+ if (message.subtype !== 'success') {
228
+ const category = sdkFailureCategory(message.subtype)
229
+ const detail = category === 'unknown'
230
+ ? undefined
231
+ : message.errors.join('; ')
232
+ throw new ClaudeCodeFailure(
233
+ { stage: 'query-run', category },
234
+ detail === undefined || detail.length === 0
235
+ ? undefined
236
+ : new Error(detail),
237
+ )
238
+ }
239
+ if (message.is_error || message.result.trim().length === 0) {
240
+ throw new ClaudeCodeFailure({
241
+ stage: 'query-run',
242
+ category: 'invalid-result',
243
+ })
244
+ }
245
+ return message.result
246
+ }
247
+
248
+ /**
249
+ * Consume the complete SDK stream and require one strict success plus normal
250
+ * iterator completion.
251
+ * @param query - published official SDK query.
252
+ * @param onPermissionDenied - records a safe fact when the SDK reports native denial.
253
+ * @param onResult - records that the SDK supplied a terminal result message.
254
+ * @returns the completed shared result.
255
+ */
256
+ export async function consumeClaudeQuery(
257
+ query: AsyncIterable<SDKMessage>,
258
+ onPermissionDenied?: () => void,
259
+ onResult?: () => void,
260
+ ): Promise<SubagentResult> {
261
+ let answer: string | undefined
262
+ for await (const message of query) {
263
+ if (message.type === 'system' && message.subtype === 'permission_denied') {
264
+ onPermissionDenied?.()
265
+ continue
266
+ }
267
+ if (message.type !== 'result') continue
268
+ onResult?.()
269
+ answer = successfulResult(message)
270
+ }
271
+ if (answer === undefined) {
272
+ throw new ClaudeCodeFailure({
273
+ stage: 'query-run',
274
+ category: 'invalid-result',
275
+ })
276
+ }
277
+ return {
278
+ output: [{ type: 'text', text: answer }],
279
+ stopReason: 'completed',
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Close the official query, terminate the managed range, and wait for the
285
+ * subprocess owner to prove it is quiescent.
286
+ * @param query - official SDK query, when creation reached that point.
287
+ * @param child - shared-service handle that owns the CLI managed range, including
288
+ * a published handle whose direct result later rejects.
289
+ */
290
+ export async function disposeClaudeCodeChild(
291
+ query: Pick<Query, 'close'> | undefined,
292
+ child: SubprocessHandle,
293
+ ): Promise<void> {
294
+ const failures: Error[] = []
295
+ let outcome: SubprocessOutcome | undefined
296
+ void child.done.then(
297
+ (value) => { outcome = value },
298
+ () => {},
299
+ )
300
+ try {
301
+ query?.close()
302
+ } catch (error: unknown) {
303
+ failures.push(thrown(error))
304
+ }
305
+
306
+ child.terminate()
307
+ try {
308
+ await child.waitForExit()
309
+ } catch (error: unknown) {
310
+ failures.push(thrown(error))
311
+ }
312
+
313
+ const firstFailure = failures[0]
314
+ if (firstFailure !== undefined) {
315
+ const facts = {
316
+ stage: 'teardown',
317
+ category: 'unknown',
318
+ outcome,
319
+ } as const
320
+ const cause = failures.length === 1
321
+ ? firstFailure
322
+ : new AggregateError(failures, 'Claude Code teardown failures')
323
+ throw new ClaudeCodeFailure(facts, cause)
324
+ }
325
+ await child.done.catch(() => {})
326
+ }
327
+
328
+ /**
329
+ * Build the fixed official SDK options for one one-shot provider run.
330
+ * @param spec - Workspace, environment, process service, and disposal policy.
331
+ * @param controller - per-run cancellation owner.
332
+ * @param capture - receives the shared child and SDK-facing process synchronously.
333
+ * @param captureDiagnostic - receives safe facts from unattended interaction callbacks.
334
+ * @returns options that inherit native settings while never waiting for user questions.
335
+ */
336
+ export function claudeQueryOptions(
337
+ spec: ClaudeCodeRunSpec,
338
+ controller: AbortController,
339
+ capture: (
340
+ child: SubprocessHandle,
341
+ process: ManagedClaudeCodeProcess,
342
+ ) => void,
343
+ captureDiagnostic: (diagnostic: string) => void,
344
+ ): Options {
345
+ return {
346
+ abortController: controller,
347
+ cwd: spec.cwd,
348
+ ...spec.model === undefined ? {} : { model: spec.model },
349
+ env: { ...scrubbedParentEnv(), ...spec.env },
350
+ persistSession: spec.persistNativeTranscript ?? true,
351
+ includePartialMessages: spec.includePartialMessages ?? true,
352
+ disallowedTools: spec.permissionMode === 'plan'
353
+ ? ['AskUserQuestion', 'ExitPlanMode']
354
+ : ['AskUserQuestion'],
355
+ permissionMode: spec.permissionMode,
356
+ ...spec.permissionMode === 'bypassPermissions'
357
+ ? { allowDangerouslySkipPermissions: true }
358
+ : {
359
+ canUseTool: () => {
360
+ captureDiagnostic(unattendedDiagnostic(
361
+ spec.permissionMode,
362
+ 'tool permission',
363
+ 'denied',
364
+ 'the provider does not request human approval',
365
+ ))
366
+ return Promise.resolve({
367
+ behavior: 'deny' as const,
368
+ message: 'This unattended Claude Code subagent cannot request human approval.',
369
+ })
370
+ },
371
+ },
372
+ onElicitation: () => {
373
+ captureDiagnostic(unattendedDiagnostic(
374
+ spec.permissionMode,
375
+ 'MCP elicitation',
376
+ 'declined',
377
+ 'the provider does not collect interactive MCP input',
378
+ ))
379
+ return Promise.resolve({ action: 'decline' })
380
+ },
381
+ onUserDialog: () => {
382
+ captureDiagnostic(unattendedDiagnostic(
383
+ spec.permissionMode,
384
+ 'user dialog',
385
+ 'cancelled',
386
+ 'the provider does not render blocking dialogs',
387
+ ))
388
+ return Promise.resolve({ behavior: 'cancelled' as const })
389
+ },
390
+ supportedDialogKinds: SUPPORTED_UNATTENDED_DIALOG_KINDS,
391
+ spawnClaudeCodeProcess: (options: SpawnOptions) => {
392
+ const child = spec.spawn(claudeSpawnSpec(options, spec.disposeGraceMs))
393
+ const process = new ManagedClaudeCodeProcess(child)
394
+ capture(child, process)
395
+ return process
396
+ },
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Start one official Claude Agent SDK query and publish its one-shot run.
402
+ * @param request - resolved shared subagent request.
403
+ * @param spec - Workspace, environment, process service, run-log, and diagnostic policy.
404
+ * @returns the published run after both Query and the real CLI handle exist.
405
+ */
406
+ export async function startClaudeCodeRun(
407
+ request: SubagentStartRequest,
408
+ spec: ClaudeCodeRunSpec,
409
+ ): Promise<SubagentRun> {
410
+ const prompt = textTask(request.prompt)
411
+ if (request.signal.aborted) {
412
+ throw new Error('subagent-claude-code: request was aborted before SDK startup')
413
+ }
414
+
415
+ const controller = new AbortController()
416
+ const requestCancel = (): void => {
417
+ if (!controller.signal.aborted) {
418
+ controller.abort(new Error('subagent-claude-code: run cancelled locally'))
419
+ }
420
+ }
421
+ const onAbort = (): void => { requestCancel() }
422
+ request.signal.addEventListener('abort', onAbort, { once: true })
423
+ const reportFailure = (error: Error): void => {
424
+ try {
425
+ spec.onError?.(error, 'error')
426
+ } catch {
427
+ // Host diagnostic logging cannot replace the product failure.
428
+ }
429
+ }
430
+
431
+ const runId = randomUUID()
432
+ const logDirectory = spec.logDirectory ?? defaultRunLogDirectory()
433
+ const writer = createRunLogWriter({
434
+ directory: logDirectory,
435
+ runId,
436
+ cwd: spec.cwd,
437
+ model: spec.model ?? null,
438
+ ...spec.onLogError === undefined ? {} : { onError: spec.onLogError },
439
+ })
440
+ cleanupExpiredRunLogs(
441
+ logDirectory,
442
+ spec.logRetentionDays ?? DEFAULT_LOG_RETENTION_DAYS,
443
+ )
444
+ const finishStartupLog = (): void => {
445
+ writer.finish(isAborted(request.signal) ? 'cancelled' : 'failed')
446
+ writer.close()
447
+ }
448
+
449
+ let child: SubprocessHandle | undefined
450
+ let childFailure: Error | undefined
451
+ let childProcessFailure: Promise<never> | undefined
452
+ let query: Query | undefined
453
+ let managedProcess: ManagedClaudeCodeProcess | undefined
454
+ let diagnostic: string | undefined
455
+ const capturePermissionDiagnostic = (value: string): void => {
456
+ diagnostic = value
457
+ }
458
+ const prependFailureDiagnostic = (facts: ClaudeCodeFailureFacts): void => {
459
+ const failure = failureDiagnostic(facts)
460
+ diagnostic = diagnostic === undefined
461
+ ? failure
462
+ : `${failure}\n${diagnostic}`
463
+ }
464
+ const captureChild = (
465
+ captured: SubprocessHandle,
466
+ process: ManagedClaudeCodeProcess,
467
+ ): void => {
468
+ child = captured
469
+ managedProcess = process
470
+ childProcessFailure = captured.done.then(
471
+ () => new Promise<never>(() => {}),
472
+ (error: unknown) => {
473
+ childFailure = thrown(error)
474
+ throw childFailure
475
+ },
476
+ )
477
+ void childProcessFailure.catch(() => {})
478
+ }
479
+ try {
480
+ query = officialQuery({
481
+ prompt,
482
+ options: claudeQueryOptions(
483
+ spec,
484
+ controller,
485
+ captureChild,
486
+ capturePermissionDiagnostic,
487
+ ),
488
+ })
489
+ if (child === undefined || childProcessFailure === undefined) {
490
+ throw new Error(
491
+ 'subagent-claude-code: official SDK did not publish a controllable Claude Code process',
492
+ )
493
+ }
494
+ if (isAborted(controller.signal)) {
495
+ throw new Error('subagent-claude-code: request was aborted before SDK startup')
496
+ }
497
+ } catch (error: unknown) {
498
+ request.signal.removeEventListener('abort', onAbort)
499
+ const cancelledBeforeCleanup = controller.signal.aborted
500
+ // Let child.done publish a concurrently observed exit before classification.
501
+ await Promise.resolve()
502
+ const startupOutcome = managedProcess?.outcome
503
+ const startupFacts = {
504
+ stage: 'query-start',
505
+ category: 'unknown',
506
+ outcome: startupOutcome,
507
+ } as const
508
+ const startupFailure = (cause: unknown = childFailure ?? error): ClaudeCodeFailure => new ClaudeCodeFailure(
509
+ startupFacts,
510
+ thrown(cause),
511
+ )
512
+ requestCancel()
513
+ if (child !== undefined) {
514
+ try {
515
+ await disposeClaudeCodeChild(query, child)
516
+ } catch (disposeError: unknown) {
517
+ const failure = startupFailure()
518
+ const cleanupFailure = thrown(disposeError)
519
+ const aggregate = new AggregateError(
520
+ [failure, cleanupFailure],
521
+ `${failure.message}; ${cleanupFailure.message}`,
522
+ )
523
+ reportFailure(aggregate)
524
+ finishStartupLog()
525
+ throw aggregate
526
+ }
527
+ if (cancelledBeforeCleanup || isAborted(request.signal)) {
528
+ finishStartupLog()
529
+ throw new Error('subagent-claude-code: request was aborted before SDK startup')
530
+ }
531
+ const failure = startupFailure()
532
+ reportFailure(failure)
533
+ finishStartupLog()
534
+ throw failure
535
+ } else if (query !== undefined) {
536
+ try {
537
+ query.close()
538
+ } catch (disposeError: unknown) {
539
+ const failure = startupFailure()
540
+ const cleanupFailure = new ClaudeCodeFailure({
541
+ stage: 'teardown',
542
+ category: 'unknown',
543
+ }, thrown(disposeError))
544
+ const aggregate = new AggregateError(
545
+ [failure, cleanupFailure],
546
+ `${failure.message}; ${cleanupFailure.message}`,
547
+ )
548
+ reportFailure(aggregate)
549
+ finishStartupLog()
550
+ throw aggregate
551
+ }
552
+ }
553
+ if (cancelledBeforeCleanup || isAborted(request.signal)) {
554
+ finishStartupLog()
555
+ throw new Error('subagent-claude-code: request was aborted before SDK startup')
556
+ }
557
+ const failure = startupFailure()
558
+ reportFailure(failure)
559
+ finishStartupLog()
560
+ throw failure
561
+ }
562
+
563
+ const publishedQuery = query
564
+ const publishedChild = child
565
+ const publishedProcessFailure = childProcessFailure
566
+ async function* loggedQuery(): AsyncGenerator<SDKMessage, void> {
567
+ for await (const message of publishedQuery) {
568
+ writer.message(message)
569
+ yield message
570
+ }
571
+ }
572
+ let receivedResult = false
573
+ const result = settleRunResult({
574
+ attempt: async () => {
575
+ try {
576
+ const completed = await Promise.race([
577
+ consumeClaudeQuery(loggedQuery(), () => {
578
+ capturePermissionDiagnostic(unattendedDiagnostic(
579
+ spec.permissionMode,
580
+ 'tool permission',
581
+ 'denied',
582
+ 'Claude Code denied the request before an interactive prompt',
583
+ ))
584
+ }, () => {
585
+ receivedResult = true
586
+ }),
587
+ publishedProcessFailure,
588
+ ])
589
+ if (isAborted(controller.signal)) {
590
+ writer.finish('cancelled')
591
+ } else {
592
+ const answer = completed.output.find(
593
+ (block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text',
594
+ )
595
+ writer.finish('completed', answer?.text ?? null)
596
+ }
597
+ return completed
598
+ } catch (error: unknown) {
599
+ writer.finish(isAborted(controller.signal) ? 'cancelled' : 'failed')
600
+ const processOutcome = managedProcess?.outcome
601
+ let facts: ClaudeCodeFailureFacts
602
+ if (error instanceof ClaudeCodeFailure) {
603
+ facts = { ...error.facts, outcome: processOutcome }
604
+ } else if (processOutcome !== undefined && !receivedResult) {
605
+ facts = {
606
+ stage: 'process',
607
+ category: 'process',
608
+ outcome: processOutcome,
609
+ }
610
+ } else {
611
+ facts = {
612
+ stage: 'query-run',
613
+ category: 'unknown',
614
+ outcome: processOutcome,
615
+ }
616
+ }
617
+ prependFailureDiagnostic(facts)
618
+ // Keep the SDK category and cause; the diagnostic adds later process facts.
619
+ throw error instanceof ClaudeCodeFailure
620
+ ? error
621
+ : new ClaudeCodeFailure(facts, thrown(error))
622
+ } finally {
623
+ writer.close()
624
+ }
625
+ },
626
+ collectOutput: () => [],
627
+ collectDiagnostic: () => diagnostic,
628
+ cancelled: () => controller.signal.aborted,
629
+ onError: spec.onError,
630
+ signal: request.signal,
631
+ onAbort,
632
+ })
633
+
634
+ return subprocessRunHandle({
635
+ id: brandString<SessionId>(runId),
636
+ result,
637
+ signal: request.signal,
638
+ onAbort,
639
+ requestCancel,
640
+ teardown: async () => {
641
+ try {
642
+ await disposeClaudeCodeChild(publishedQuery, publishedChild)
643
+ } catch (error: unknown) {
644
+ const failure = thrown(error)
645
+ reportFailure(failure)
646
+ throw failure
647
+ }
648
+ },
649
+ })
650
+ }