ai-foreman 1.7.29 → 2.0.1

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.
Files changed (156) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +231 -26
  4. package/dist/adapters/codex.d.ts +19 -7
  5. package/dist/adapters/codex.js +202 -19
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +85 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/git.js +3 -8
  18. package/dist/branch/planner.js +1 -4
  19. package/dist/branch/runner.d.ts +2 -1
  20. package/dist/branch/runner.js +10 -7
  21. package/dist/buildAdmission.js +4 -0
  22. package/dist/buildApproval.d.ts +15 -0
  23. package/dist/buildApproval.js +28 -9
  24. package/dist/buildAssignment.d.ts +1 -0
  25. package/dist/buildAssignment.js +82 -23
  26. package/dist/buildInterventions.d.ts +2 -2
  27. package/dist/buildInterventions.js +11 -4
  28. package/dist/buildLaunchGate.js +3 -2
  29. package/dist/buildRuns.d.ts +1 -0
  30. package/dist/buildRuns.js +6 -1
  31. package/dist/builderGuidanceFollowup.js +17 -6
  32. package/dist/cli/doctor.js +3 -1
  33. package/dist/cli/manager.js +22 -20
  34. package/dist/cli/start.js +85 -39
  35. package/dist/cli/tickets.d.ts +0 -8
  36. package/dist/cli/tickets.js +50 -60
  37. package/dist/continuity.d.ts +10 -16
  38. package/dist/continuity.js +223 -208
  39. package/dist/continuityOrigin.d.ts +13 -0
  40. package/dist/continuityOrigin.js +91 -0
  41. package/dist/dispatchNotSent.d.ts +4 -0
  42. package/dist/dispatchNotSent.js +10 -0
  43. package/dist/foreman.d.ts +15 -16
  44. package/dist/foreman.js +70 -278
  45. package/dist/gitExclude.d.ts +16 -0
  46. package/dist/gitExclude.js +46 -0
  47. package/dist/graph/boundary.d.ts +1 -1
  48. package/dist/graph/boundary.js +1 -1
  49. package/dist/graph/host.js +8 -19
  50. package/dist/graph/manager.d.ts +1 -1
  51. package/dist/graph/manager.js +1 -1
  52. package/dist/graph/preparation.d.ts +38 -0
  53. package/dist/graph/preparation.js +7 -6
  54. package/dist/graph/semantic.d.ts +1 -0
  55. package/dist/graph/semantic.js +4 -1
  56. package/dist/graph/session.js +7 -2
  57. package/dist/graph/turn.d.ts +3 -4
  58. package/dist/graph/turn.js +29 -41
  59. package/dist/handoffProtocol.js +2 -2
  60. package/dist/handoffs.d.ts +5 -3
  61. package/dist/handoffs.js +92 -88
  62. package/dist/hostGraphApplicability.d.ts +2 -1
  63. package/dist/hostGraphApplicability.js +17 -19
  64. package/dist/hostQaAugmentation.js +21 -4
  65. package/dist/hostQaComparison.d.ts +21 -2
  66. package/dist/hostQaComparison.js +43 -29
  67. package/dist/hostSourcePreservation.d.ts +38 -0
  68. package/dist/hostSourcePreservation.js +72 -0
  69. package/dist/humanDecision.d.ts +7 -0
  70. package/dist/humanDecision.js +11 -0
  71. package/dist/interviews.d.ts +7 -0
  72. package/dist/interviews.js +18 -1
  73. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  74. package/dist/legacyCommunicationDisplay.js +2 -0
  75. package/dist/managerPacket.js +3 -3
  76. package/dist/markers.d.ts +4 -3
  77. package/dist/markers.js +4 -20
  78. package/dist/observability.js +3 -8
  79. package/dist/probeIpc.d.ts +14 -0
  80. package/dist/probeIpc.js +29 -0
  81. package/dist/qaBuildGate.d.ts +21 -0
  82. package/dist/qaBuildGate.js +60 -13
  83. package/dist/qaBuilderCoverage.js +22 -13
  84. package/dist/qaBuilderEvidence.d.ts +16 -1
  85. package/dist/qaBuilderEvidence.js +48 -8
  86. package/dist/qaContractCoverage.d.ts +2 -2
  87. package/dist/qaContractCoverage.js +4 -9
  88. package/dist/qaContractDelivery.js +34 -20
  89. package/dist/qaCoverageAssembly.d.ts +22 -0
  90. package/dist/qaCoverageAssembly.js +16 -0
  91. package/dist/qaDeliveryJournal.d.ts +2 -0
  92. package/dist/qaDeliveryJournal.js +19 -0
  93. package/dist/qaFailureDelivery.js +97 -85
  94. package/dist/qaHostTransformation.d.ts +26 -0
  95. package/dist/qaHostTransformation.js +100 -0
  96. package/dist/qaPreparation.d.ts +8 -20
  97. package/dist/qaPreparation.js +153 -254
  98. package/dist/qaPreparationInputs.js +19 -0
  99. package/dist/qaPreparationStore.d.ts +11 -0
  100. package/dist/qaPreparationStore.js +44 -4
  101. package/dist/qaPrerequisites.d.ts +2 -1
  102. package/dist/qaPrerequisites.js +21 -1
  103. package/dist/qaProtocolV2.d.ts +15 -0
  104. package/dist/qaRecovery.d.ts +0 -3
  105. package/dist/qaRecovery.js +21 -27
  106. package/dist/qaReview.js +241 -406
  107. package/dist/qaReviewEvidence.d.ts +7 -3
  108. package/dist/qaReviewEvidence.js +15 -4
  109. package/dist/qaRuntime.js +1 -1
  110. package/dist/qaSnapshot.js +76 -21
  111. package/dist/qaStructuredResponse.d.ts +18 -0
  112. package/dist/qaStructuredResponse.js +81 -0
  113. package/dist/qaTimeline.js +2 -2
  114. package/dist/qaVerificationContract.d.ts +1 -2
  115. package/dist/qaVerificationContract.js +24 -35
  116. package/dist/readinessGate.js +5 -4
  117. package/dist/recoveryOwners.d.ts +32 -0
  118. package/dist/recoveryOwners.js +87 -0
  119. package/dist/runtimePreflight.d.ts +11 -0
  120. package/dist/runtimePreflight.js +77 -0
  121. package/dist/runtimeReadiness.js +23 -15
  122. package/dist/sources/sourceRegistry.d.ts +0 -3
  123. package/dist/sources/sourceRegistry.js +0 -42
  124. package/dist/statusReporter.d.ts +2 -0
  125. package/dist/statusReporter.js +2 -0
  126. package/dist/structuredCutover.d.ts +31 -0
  127. package/dist/structuredCutover.js +510 -0
  128. package/dist/structuredCutoverFiles.d.ts +10 -0
  129. package/dist/structuredCutoverFiles.js +181 -0
  130. package/dist/structuredDispatchAuthority.d.ts +22 -0
  131. package/dist/structuredDispatchAuthority.js +110 -0
  132. package/dist/structuredDispatchRecord.d.ts +29 -0
  133. package/dist/structuredDispatchRecord.js +36 -0
  134. package/dist/structuredLiveBudget.d.ts +2 -0
  135. package/dist/structuredLiveBudget.js +23 -0
  136. package/dist/structuredOperation.d.ts +18 -0
  137. package/dist/structuredOperation.js +279 -0
  138. package/dist/supervisedStart.js +35 -21
  139. package/dist/ticketPlanning.d.ts +1 -376
  140. package/dist/ticketPlanning.js +2 -49
  141. package/dist/ticketPopulation.d.ts +0 -3
  142. package/dist/ticketPopulation.js +4 -21
  143. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  144. package/dist/ticketPopulationPageAuthority.js +30 -0
  145. package/dist/ticketPopulationPaging.d.ts +27 -0
  146. package/dist/ticketPopulationPaging.js +127 -0
  147. package/dist/tickets/renderMarkdown.d.ts +14 -0
  148. package/dist/tickets/renderMarkdown.js +39 -5
  149. package/dist/tickets/ticketLoader.js +10 -0
  150. package/dist/tickets/ticketSchema.d.ts +107 -0
  151. package/dist/tickets/ticketSchema.js +4 -1
  152. package/dist/workflowDb.d.ts +116 -3
  153. package/dist/workflowDb.js +852 -61
  154. package/dist/workflowReader.d.ts +1 -0
  155. package/dist/workflowReader.js +9 -0
  156. package/package.json +23 -7
package/README.md CHANGED
@@ -48,7 +48,7 @@ Helpful when:
48
48
 
49
49
  Requires:
50
50
 
51
- - Node.js 20 or newer.
51
+ - Node.js 24.
52
52
 
53
53
  ### Claude runtime and enterprise authentication
54
54
 
@@ -1,4 +1,4 @@
1
- import { ProviderPhaseBarrier, type ProviderTurnPurpose } from "../providerPhase.js";
1
+ import { ProviderPhaseBarrier } from "../providerPhase.js";
2
2
  import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
3
3
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
4
4
  export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
@@ -9,7 +9,7 @@ import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
9
9
  * Extracted so tests can assert on the shape without making a live SDK call.
10
10
  * `canUseTool` is omitted here — it's a closure that the constructor adds.
11
11
  */
12
- export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">): Record<string, unknown>;
12
+ export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">, scratchDirectory?: string): Record<string, unknown>;
13
13
  export declare function probeClaudeSession(ref: ProviderSessionRefV1, input: {
14
14
  cwd: string;
15
15
  configRoot?: string;
@@ -43,6 +43,7 @@ export declare function claudeApiRetryEvent(message: {
43
43
  */
44
44
  export declare class ClaudeAdapter implements BuilderAdapter {
45
45
  private readonly opts;
46
+ private readonly queryFactory;
46
47
  readonly agent: "claude";
47
48
  graphRuntimeSettings(): {
48
49
  model: string | undefined;
@@ -50,11 +51,16 @@ export declare class ClaudeAdapter implements BuilderAdapter {
50
51
  fast: boolean | undefined;
51
52
  runtimeExecutable: string | undefined;
52
53
  };
53
- private readonly inbox;
54
+ private inbox;
54
55
  private readonly eventQueue;
55
- private readonly query;
56
- private readonly abort;
57
- private readonly pumpDone;
56
+ private query;
57
+ private abort;
58
+ private pumpDone;
59
+ private rotatingQuery;
60
+ private configuringTurn;
61
+ private output?;
62
+ private outputWireContract?;
63
+ private responseOnlyViolation;
58
64
  private _sessionId?;
59
65
  private _sessionRef?;
60
66
  private readonly sessionIdentityReady;
@@ -76,8 +82,10 @@ export declare class ClaudeAdapter implements BuilderAdapter {
76
82
  private cumulativeUsage;
77
83
  private priorTurnUsage;
78
84
  private activeProviderTurnId?;
85
+ private activeTurnPolicy?;
79
86
  private activeProviderTurnSpanId?;
80
87
  private readonly toolSpans;
88
+ private readonly structuredSubmissionIds;
81
89
  private readonly toolCalls;
82
90
  private readonly completedToolCalls;
83
91
  private readonly unknownToolResults;
@@ -96,6 +104,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
96
104
  private streamEnded;
97
105
  private closed;
98
106
  private closePromise?;
107
+ private readonly scratchDirectory?;
99
108
  private launchScopeObserved;
100
109
  private providerIdentityObserved;
101
110
  private toolCwd?;
@@ -109,16 +118,18 @@ export declare class ClaudeAdapter implements BuilderAdapter {
109
118
  private disableFastMode;
110
119
  static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
111
120
  private constructor();
121
+ private queryBudget?;
122
+ private queryBudgetAbort?;
123
+ private finishQueryBudget;
124
+ private startQuery;
125
+ private isResultSubmission;
126
+ private resultSubmissionError;
127
+ private configureOutput;
112
128
  /** Background loop: consume the SDK message stream until it ends. */
113
129
  private providerInputs;
114
130
  private pump;
115
131
  private handle;
116
- sendTurn(text: string, policy?: {
117
- handback?: boolean;
118
- responseOnly?: boolean;
119
- logicalActionId?: string;
120
- purpose?: ProviderTurnPurpose;
121
- }): Promise<TurnResult>;
132
+ sendTurn(text: string, policy?: import("./types.js").ProviderTurnPolicy): Promise<TurnResult>;
122
133
  private sendTurnInternal;
123
134
  private sendTurnAttempt;
124
135
  private captureStderr;
@@ -1,7 +1,11 @@
1
+ import { inheritedStructuredLiveBudget } from "../structuredLiveBudget.js";
1
2
  import { ProviderPhaseBarrier } from "../providerPhase.js";
2
3
  import { OperationDeadline } from "../util/deadline.js";
4
+ import { assertActiveProviderDispatch } from "../structuredDispatchAuthority.js";
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { dirname, join, relative, isAbsolute } from "node:path";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
5
9
  /** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
6
10
  export async function requireClaudeSDK() {
7
11
  try {
@@ -22,19 +26,21 @@ import { createProviderSessionRef, validateProviderSessionScope, canonicalSessio
22
26
  import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
23
27
  import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
24
28
  import { handoffSystemInstructions } from "../handoffProtocol.js";
29
+ import { providerControlOperationV2, providerWireContract, renderProviderWireInstruction, validateProviderWireValue } from "rafi-spec";
30
+ import { structuredReceipt } from "./structured.js";
25
31
  const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
26
32
  /**
27
33
  * Pure function: build the `options` object passed to `query()`.
28
34
  * Extracted so tests can assert on the shape without making a live SDK call.
29
35
  * `canUseTool` is omitted here — it's a closure that the constructor adds.
30
36
  */
31
- export function buildClaudeQueryOptions(opts) {
37
+ export function buildClaudeQueryOptions(opts, scratchDirectory = join(dirname(opts.cwd), "scratch")) {
32
38
  const qaReadOnly = opts.sessionRole === "qa" || opts.sandboxMode === "read-only";
33
39
  const confinedBuilder = opts.sessionRole === "builder";
34
40
  const base = {
35
41
  cwd: opts.cwd,
36
42
  pathToClaudeCodeExecutable: opts.runtimeExecutable,
37
- env: qaReadOnly ? qaEnvironment(process.env, opts.cwd) : { ...process.env },
43
+ env: qaReadOnly ? qaEnvironment(process.env, scratchDirectory) : { ...process.env },
38
44
  model: opts.model,
39
45
  resume: opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId,
40
46
  permissionMode: qaReadOnly ? "default" : "acceptEdits",
@@ -57,14 +63,15 @@ export function buildClaudeQueryOptions(opts) {
57
63
  failIfUnavailable: true,
58
64
  autoAllowBashIfSandboxed: true,
59
65
  allowUnsandboxedCommands: false,
60
- network: { allowedDomains: [], deniedDomains: ["*"] },
61
- filesystem: { allowWrite: [join(dirname(opts.cwd), "scratch")], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
66
+ // QA stays offline unless the operator granted --qa-network for this build.
67
+ network: opts.networkAccess === true ? { allowedDomains: ["*"] } : { allowedDomains: [], deniedDomains: ["*"] },
68
+ filesystem: { allowWrite: [scratchDirectory], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
62
69
  } } : {}),
63
70
  };
64
71
  const exactSkills = opts.preloadedSkillContent?.length
65
72
  ? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
66
73
  : undefined;
67
- const systemAppend = [handoffSystemInstructions(opts.sessionRole, opts.systemPromptAppend), opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
74
+ const systemAppend = [handoffSystemInstructions(opts.sessionRole, opts.systemPromptAppend), opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, opts.sessionRole === "qa" && opts.networkAccess === true ? "QA runtime contract: shell network explicitly approved for this review (--qa-network); you may run live checks against operator-provided local or remote services. Workspace stays read-only." : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
68
75
  if (systemAppend) {
69
76
  base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
70
77
  }
@@ -73,9 +80,9 @@ export function buildClaudeQueryOptions(opts) {
73
80
  }
74
81
  return base;
75
82
  }
76
- function qaEnvironment(source, cwd) {
83
+ function qaEnvironment(source, scratchDirectory) {
77
84
  const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"];
78
- return { ...Object.fromEntries(allowed.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])), TMPDIR: join(dirname(cwd), "scratch") };
85
+ return { ...Object.fromEntries(allowed.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])), TMPDIR: scratchDirectory };
79
86
  }
80
87
  export async function probeClaudeSession(ref, input) {
81
88
  const now = input.now ?? new Date();
@@ -176,6 +183,7 @@ export function claudeApiRetryEvent(message) {
176
183
  */
177
184
  export class ClaudeAdapter {
178
185
  opts;
186
+ queryFactory;
179
187
  agent = "claude";
180
188
  graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
181
189
  inbox = new AsyncQueue();
@@ -183,6 +191,11 @@ export class ClaudeAdapter {
183
191
  query;
184
192
  abort = new AbortController();
185
193
  pumpDone;
194
+ rotatingQuery = false;
195
+ configuringTurn = false;
196
+ output;
197
+ outputWireContract;
198
+ responseOnlyViolation = false;
186
199
  _sessionId;
187
200
  _sessionRef;
188
201
  sessionIdentityReady;
@@ -204,8 +217,10 @@ export class ClaudeAdapter {
204
217
  cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
205
218
  priorTurnUsage = {};
206
219
  activeProviderTurnId;
220
+ activeTurnPolicy;
207
221
  activeProviderTurnSpanId;
208
222
  toolSpans = new Map();
223
+ structuredSubmissionIds = new Set();
209
224
  toolCalls = new Map();
210
225
  completedToolCalls = new Set();
211
226
  unknownToolResults = new Map();
@@ -221,6 +236,7 @@ export class ClaudeAdapter {
221
236
  streamEnded = false;
222
237
  closed = false;
223
238
  closePromise;
239
+ scratchDirectory;
224
240
  launchScopeObserved = false;
225
241
  providerIdentityObserved = false;
226
242
  toolCwd;
@@ -251,6 +267,7 @@ export class ClaudeAdapter {
251
267
  return;
252
268
  }
253
269
  try {
270
+ providerControlOperationV2("claude", "applyFlagSettings");
254
271
  await new OperationDeadline("Claude fast mode", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: true }));
255
272
  this.fastModeEnabled = true;
256
273
  }
@@ -269,10 +286,13 @@ export class ClaudeAdapter {
269
286
  this.fastModeEnabled = false;
270
287
  }
271
288
  async disableFastMode() {
289
+ providerControlOperationV2("claude", "applyFlagSettings");
272
290
  await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
273
291
  this.disableFastModeNotice();
274
292
  }
275
293
  static async create(opts) {
294
+ const inheritedBudget = inheritedStructuredLiveBudget("claude");
295
+ opts = { ...opts, queryBudget: opts.queryBudget ?? inheritedBudget };
276
296
  try {
277
297
  const runtimeExecutable = opts.runtimeExecutable ?? resolveExecutablePath("claude");
278
298
  if (!runtimeExecutable) {
@@ -310,8 +330,9 @@ export class ClaudeAdapter {
310
330
  }
311
331
  }
312
332
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
313
- constructor(opts, query) {
333
+ constructor(opts, queryFactory) {
314
334
  this.opts = opts;
335
+ this.queryFactory = queryFactory;
315
336
  this.phaseBarrier.begin("initialization");
316
337
  this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
317
338
  this._sessionRef = opts.resumeSessionRef;
@@ -324,15 +345,61 @@ export class ClaudeAdapter {
324
345
  start: (attemptId) => opts.observer.store.startSpan(this.observationContext(), { kind: "provider_wait", name: "Waiting for Claude after question answer", attributes: { attemptId } }),
325
346
  finish: (spanId, outcome) => opts.observer.store.finishSpan(spanId, { outcome }),
326
347
  } : undefined);
327
- this.query = query({
348
+ // Planning and interpretation also use read-only sessions, without a QA
349
+ // snapshot's sibling scratch directory. Own a real, private directory for
350
+ // the complete query lifecycle, including exact-session schema rotations.
351
+ // macOS's default TMPDIR is too long for the SDK's Unix socket paths.
352
+ if (opts.sessionRole === "qa" || opts.sandboxMode === "read-only")
353
+ this.scratchDirectory = mkdtempSync(join(process.platform === "darwin" ? "/tmp" : tmpdir(), "rf-c-"));
354
+ try {
355
+ this.startQuery();
356
+ }
357
+ catch (error) {
358
+ this.finishQueryBudget();
359
+ if (this.scratchDirectory)
360
+ rmSync(this.scratchDirectory, { recursive: true, force: true });
361
+ throw error;
362
+ }
363
+ }
364
+ queryBudget;
365
+ queryBudgetAbort;
366
+ finishQueryBudget() {
367
+ if (this.queryBudgetAbort)
368
+ this.queryBudget?.signal.removeEventListener("abort", this.queryBudgetAbort);
369
+ this.queryBudgetAbort = undefined;
370
+ this.queryBudget?.close();
371
+ this.queryBudget = undefined;
372
+ }
373
+ startQuery() {
374
+ const opts = this.opts;
375
+ this.queryBudget = opts.queryBudget?.reserveQuery("claude");
376
+ if (this.queryBudget) {
377
+ const lease = this.queryBudget;
378
+ this.queryBudgetAbort = () => { this.abort.abort(); void this.close(); };
379
+ lease.signal.addEventListener("abort", this.queryBudgetAbort, { once: true });
380
+ if (lease.signal.aborted) {
381
+ this.finishQueryBudget();
382
+ throw new Error("Live query deadline expired before launch");
383
+ }
384
+ }
385
+ this.query = this.queryFactory({
328
386
  prompt: this.providerInputs(),
329
387
  options: {
330
- ...buildClaudeQueryOptions(opts),
388
+ ...buildClaudeQueryOptions(opts, this.scratchDirectory),
389
+ ...(this.queryBudget ? { maxBudgetUsd: this.queryBudget.maximumSpendUsd } : {}),
390
+ ...(this._sessionId ? { resume: this._sessionId } : {}),
391
+ ...(this.outputWireContract ? { outputFormat: { type: "json_schema", schema: this.outputWireContract.wireSchema } } : {}),
331
392
  abortController: this.abort,
332
393
  hooks: {
333
394
  PreToolUse: [{ hooks: [async (input) => {
334
395
  if (input.hook_event_name !== "PreToolUse")
335
396
  return {};
397
+ if (this.isResultSubmission(input.tool_name, input.tool_input)) {
398
+ const reason = this.resultSubmissionError(input.tool_input);
399
+ return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
400
+ }
401
+ if (this.output && this.responseOnlyTurn)
402
+ this.responseOnlyViolation = true;
336
403
  this.toolCwd = input.cwd;
337
404
  this.toolCwds.set(input.tool_use_id, input.cwd);
338
405
  if (this.toolCwds.size > 1000)
@@ -356,6 +423,12 @@ export class ClaudeAdapter {
356
423
  },
357
424
  stderr: (data) => this.captureStderr(data),
358
425
  canUseTool: async (toolName, input, requestOptions = {}) => {
426
+ if (this.isResultSubmission(toolName, input)) {
427
+ const reason = this.resultSubmissionError(input);
428
+ return reason ? { behavior: "deny", message: reason } : { behavior: "allow", updatedInput: input };
429
+ }
430
+ if (this.output && this.responseOnlyTurn)
431
+ this.responseOnlyViolation = true;
359
432
  if (this.responseOnlyTurn)
360
433
  return { behavior: "deny", message: "QA response correction forbids all tools, including read-only tools", interrupt: true };
361
434
  const isProviderQuestion = toolName === "AskUserQuestion";
@@ -394,6 +467,60 @@ export class ClaudeAdapter {
394
467
  });
395
468
  this.pumpDone = this.pump();
396
469
  }
470
+ isResultSubmission(name, input) {
471
+ // Only the SDK's built-in submission in a host-configured structured query.
472
+ // An MCP name or arbitrary tool cannot acquire this exception.
473
+ return Boolean(this.pending && this.output && name === "StructuredOutput");
474
+ }
475
+ resultSubmissionError(input) {
476
+ try {
477
+ // This SDK hook sees provider wire data, not the canonical artifact.
478
+ // Canonical validation is deliberately deferred until the selected
479
+ // normalizer runs in structuredReceipt.
480
+ validateProviderWireValue(this.outputWireContract, input);
481
+ return undefined;
482
+ }
483
+ catch (error) {
484
+ // The SDK submission is data, including when malformed. Give its bounded
485
+ // native retry the actual wire-contract violations, never a mutation-policy
486
+ // diagnosis. Only a valid wire payload may invoke the SDK submission tool.
487
+ return `Native structured result violates selected ${this.outputWireContract.profile} wire contract for ${this.output.operation}: ${String(error).slice(0, 2400)}. Return every required field with the declared type.`;
488
+ }
489
+ }
490
+ async configureOutput(output, wireContract) {
491
+ const selected = output ? wireContract ?? providerWireContract(output, "claude", "default", { requireRegistered: true }) : undefined;
492
+ if (selected && (selected.provider !== "claude" || selected.canonical.schemaDigest !== output.schemaDigest))
493
+ throw new Error("Claude received a mismatched provider wire contract");
494
+ if (output?.schemaDigest === this.output?.schemaDigest && output?.operation === this.output?.operation && selected?.descriptorDigest === this.outputWireContract?.descriptorDigest)
495
+ return;
496
+ if (this.pending || this.closed || this.streamEnded)
497
+ throw new Error("Cannot change output schema on an active or unavailable Claude query");
498
+ this.rotatingQuery = true;
499
+ try {
500
+ this.inbox.close();
501
+ this.abort.abort();
502
+ this.query.close?.();
503
+ await new OperationDeadline("Claude schema transition", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone);
504
+ this.finishQueryBudget();
505
+ }
506
+ catch (error) {
507
+ this.streamEnded = true;
508
+ throw error;
509
+ }
510
+ finally {
511
+ this.rotatingQuery = false;
512
+ }
513
+ this.inbox = new AsyncQueue();
514
+ this.abort = new AbortController();
515
+ this.output = output;
516
+ this.outputWireContract = selected;
517
+ this.fastModePrepared = undefined;
518
+ this.fastModeEnabled = false;
519
+ this.providerIdentityObserved = false;
520
+ this.startQuery();
521
+ if (this._sessionId)
522
+ this.eventQueue.push({ kind: "session-transition", transition: "resumed", detail: "Exact session resumed for output schema change" });
523
+ }
397
524
  /** Background loop: consume the SDK message stream until it ends. */
398
525
  async *providerInputs() {
399
526
  for await (const message of this.inbox) {
@@ -413,6 +540,8 @@ export class ClaudeAdapter {
413
540
  }
414
541
  }
415
542
  catch (err) {
543
+ if (this.rotatingQuery)
544
+ return;
416
545
  this.questionTrace?.finish(this.closed ? "closed" : "stream-error");
417
546
  // Suppress the AbortError that fires when close() aborts the stream.
418
547
  const isShutdownAbort = this.closed &&
@@ -429,6 +558,8 @@ export class ClaudeAdapter {
429
558
  }
430
559
  }
431
560
  finally {
561
+ if (this.rotatingQuery)
562
+ return;
432
563
  this.questionTrace?.finish(this.closed ? "closed" : "stream-ended");
433
564
  this.streamEnded = true;
434
565
  if (!this.closed && this.pending) {
@@ -464,7 +595,7 @@ export class ClaudeAdapter {
464
595
  this.turnSignals.push(`assistant error: ${msg.error}`);
465
596
  }
466
597
  for (const block of msg.message.content) {
467
- this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: String(block.type ?? "unknown"), payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
598
+ this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: block.type === "tool_use" && this.isResultSubmission(block.name, block.input) ? "structuredOutput" : String(block.type ?? "unknown"), payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
468
599
  if (block.type === "text" && block.text) {
469
600
  if (msg.error && this.fastModeEnabled && isFastModeUnavailable(block.text) && !this.turnSawWork)
470
601
  continue;
@@ -473,6 +604,16 @@ export class ClaudeAdapter {
473
604
  this.opts.observer?.signal(true);
474
605
  }
475
606
  else if (block.type === "tool_use") {
607
+ // The SDK result submission is transport, not a domain tool. Retain its
608
+ // provider-item evidence without falsely reporting an acceptance violation.
609
+ if (this.isResultSubmission(block.name, block.input)) {
610
+ this.structuredSubmissionIds.add(block.id);
611
+ if (this.structuredSubmissionIds.size > 1000)
612
+ this.structuredSubmissionIds.delete(this.structuredSubmissionIds.values().next().value);
613
+ continue;
614
+ }
615
+ if (this.output && this.responseOnlyTurn && !this.isResultSubmission(block.name, block.input))
616
+ this.responseOnlyViolation = true;
476
617
  this.turnSawWork = true;
477
618
  const callId = block.id;
478
619
  this.toolCalls.set(callId, { name: block.name, input: block.input });
@@ -537,6 +678,8 @@ export class ClaudeAdapter {
537
678
  this.eventQueue.push({ kind: "activity", provider: "claude", state: "running tool", detail: `${msg.tool_name} (${Math.floor(msg.elapsed_time_seconds)}s)`, transient: true });
538
679
  const progress = msg;
539
680
  const callId = typeof progress.tool_use_id === "string" ? progress.tool_use_id : typeof progress.toolUseID === "string" ? progress.toolUseID : undefined;
681
+ if (callId && this.structuredSubmissionIds.has(callId))
682
+ return;
540
683
  this.eventQueue.push({ kind: "tool", name: msg.tool_name, input: {}, lifecycle: "progress", callId, durationMs: Math.max(0, msg.elapsed_time_seconds * 1000), completionKnown: false, eventId: randomUUID(), observedAt: new Date().toISOString() });
541
684
  this.opts.observer?.signal(false);
542
685
  }
@@ -586,7 +729,8 @@ export class ClaudeAdapter {
586
729
  cwd: this.opts.cwd,
587
730
  diagnostics: rawDiagnostics,
588
731
  } : undefined;
589
- const result = {
732
+ this.queryBudget?.record(msg.total_cost_usd);
733
+ let result = {
590
734
  text: failure ? formatClaudeFailure(failure) : text,
591
735
  isError: msg.is_error,
592
736
  numTurns: msg.num_turns,
@@ -599,8 +743,21 @@ export class ClaudeAdapter {
599
743
  rawResponse: text,
600
744
  cleanedResponse: failure ? formatClaudeFailure(failure) : text,
601
745
  providerMetadata: { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef },
746
+ ...(this.activeTurnPolicy?.selectedDispatch ? { selectedDispatch: this.activeTurnPolicy.selectedDispatch } : {}),
602
747
  };
603
748
  const rawResult = msg;
749
+ if (this.output) {
750
+ const selectedDispatch = this.activeTurnPolicy?.selectedDispatch;
751
+ if (!selectedDispatch || !this._sessionId)
752
+ throw new Error("Structured terminal lost its sealed dispatch tuple or session");
753
+ result = { ...result, selectedDispatch, structured: msg.subtype !== "success" || msg.is_error || this.responseOnlyViolation
754
+ ? { state: "rejected", reason: this.responseOnlyViolation ? "tool-policy-violation" : "execution-not-completed", detail: `Claude terminal ${msg.subtype}`, dispatchId: result.turnId }
755
+ : !this.outputWireContract || !this.activeTurnPolicy?.selectedDispatch
756
+ ? { state: "rejected", reason: "invalid-contract", detail: "Claude structured turn has no sealed descriptor or tuple", dispatchId: result.turnId }
757
+ : structuredReceipt(this.output, "claude", rawResult.structured_output, { dispatchId: result.turnId, sessionId: this._sessionId, nativeTurnId: typeof rawResult.uuid === "string" ? rawResult.uuid : undefined }, "decoded-json", this.outputWireContract, this.activeTurnPolicy.selectedDispatch) };
758
+ if (result.structured.state === "rejected")
759
+ result.isError = true;
760
+ }
604
761
  const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
605
762
  result.inputTokens = mergedUsage.inputTokens;
606
763
  result.outputTokens = mergedUsage.outputTokens;
@@ -616,11 +773,30 @@ export class ClaudeAdapter {
616
773
  }
617
774
  }
618
775
  async sendTurn(text, policy) {
619
- if (this.pending)
776
+ if (this.pending || this.configuringTurn)
620
777
  throw new Error("a turn is already in progress");
778
+ if (policy?.output)
779
+ assertActiveProviderDispatch(policy, text, "claude", this.sessionRef());
780
+ this.configuringTurn = true;
781
+ try {
782
+ const selected = policy?.output ? policy.wireContract ?? providerWireContract(policy.output, "claude", "default", { requireRegistered: true }) : undefined;
783
+ if (selected && policy?.renderedWireContractDigest && policy.renderedWireContractDigest !== selected.descriptorDigest)
784
+ throw new Error("Claude received mismatched rendered wire guidance");
785
+ if (selected) {
786
+ text = renderProviderWireInstruction(text, selected);
787
+ policy = { ...policy, wireContract: selected, renderedWireContractDigest: selected.descriptorDigest };
788
+ }
789
+ if (policy?.output?.schemaDigest !== this.output?.schemaDigest || policy?.output?.operation !== this.output?.operation || selected?.descriptorDigest !== this.outputWireContract?.descriptorDigest)
790
+ await this.configureOutput(policy?.output, selected);
791
+ }
792
+ finally {
793
+ this.configuringTurn = false;
794
+ }
795
+ this.responseOnlyViolation = false;
796
+ this.activeTurnPolicy = policy;
621
797
  this.responseOnlyTurn = Boolean(policy?.responseOnly);
622
798
  this.phaseBarrier.begin(policy?.purpose ?? (policy?.responseOnly ? "response-repair" : "implementation"));
623
- const turnId = randomUUID();
799
+ const turnId = policy?.dispatchId ?? randomUUID();
624
800
  this.activeProviderTurnId = turnId;
625
801
  const observer = this.opts.observer;
626
802
  if (!observer) {
@@ -663,7 +839,7 @@ export class ClaudeAdapter {
663
839
  if (this.opts.fast)
664
840
  await this.prepareFastMode();
665
841
  const result = await this.sendTurnAttempt(text);
666
- if (result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(result.rawResponse ?? result.text) && !this.streamEnded) {
842
+ if (!this.output && result.isError && this.fastModeEnabled && !this.turnSawWork && isFastModeUnavailable(result.rawResponse ?? result.text) && !this.streamEnded) {
667
843
  await this.disableFastMode();
668
844
  return this.sendTurnAttempt(text);
669
845
  }
@@ -712,9 +888,9 @@ export class ClaudeAdapter {
712
888
  for (const block of body.content) {
713
889
  if (block.type !== "tool_result")
714
890
  continue;
715
- this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: "tool_result", payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
891
+ this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: typeof block.tool_use_id === "string" && this.structuredSubmissionIds.has(block.tool_use_id) ? "structuredOutput" : "tool_result", payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
716
892
  const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
717
- if (!callId)
893
+ if (!callId || this.structuredSubmissionIds.has(callId))
718
894
  continue;
719
895
  if (this.completedToolCalls.has(callId))
720
896
  continue;
@@ -735,11 +911,15 @@ export class ClaudeAdapter {
735
911
  : failed && /cancel(?:led|ed)/i.test(text) ? "cancelled"
736
912
  : failed && /permission denied/i.test(text) ? "permission_denied"
737
913
  : failed && /not found/i.test(text) ? "not_found"
738
- : failed && /\b(?:exit(?:ed)?|status)\s+[1-9]\d*\b/i.test(text) ? "nonzero_exit"
739
- : failed && /\b(?:failed|error|exception)\b/i.test(text) ? "explicit_error"
740
- : failed ? "unexplained_error"
741
- : "completed";
742
- const status = resultReason === "host_policy_denied" ? "denied" : resultReason === "timeout" ? "timed_out" : resultReason === "cancelled" ? "cancelled" : ["permission_denied", "not_found", "nonzero_exit", "explicit_error"].includes(resultReason) ? "failed" : resultReason === "completed" ? "completed" : "unknown";
914
+ // Claude's Read tool explicitly reports its size guard this
915
+ // way. It is a terminal, observed rejection, not a
916
+ // provisional transport error that might later complete.
917
+ : failed && /\b(?:content|file)\b[\s\S]{0,160}\bexceeds\s+(?:the\s+)?maximum\s+allowed\s+tokens\b/i.test(text) ? "output_limit"
918
+ : failed && /\b(?:exit(?:ed)?|status)\s+[1-9]\d*\b/i.test(text) ? "nonzero_exit"
919
+ : failed && /\b(?:failed|error|exception)\b/i.test(text) ? "explicit_error"
920
+ : failed ? "unexplained_error"
921
+ : "completed";
922
+ const status = resultReason === "host_policy_denied" ? "denied" : resultReason === "timeout" ? "timed_out" : resultReason === "cancelled" ? "cancelled" : ["permission_denied", "not_found", "output_limit", "nonzero_exit", "explicit_error"].includes(resultReason) ? "failed" : resultReason === "completed" ? "completed" : "unknown";
743
923
  if (status !== "unknown") {
744
924
  this.completedToolCalls.add(callId);
745
925
  if (this.completedToolCalls.size > 1000)
@@ -795,6 +975,7 @@ export class ClaudeAdapter {
795
975
  // Require an advertised built-in local command; never substitute a
796
976
  // model-generated warm-up for QA's pre-dispatch identity validation.
797
977
  if (commands.some(command => command.name === "context" && "builtin" in command && command.builtin === true)) {
978
+ providerControlOperationV2("claude", "/context");
798
979
  this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-start", at: new Date().toISOString() });
799
980
  const result = await this.sendTurn("/context", { responseOnly: true });
800
981
  this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-complete", at: new Date().toISOString() });
@@ -807,7 +988,11 @@ export class ClaudeAdapter {
807
988
  }
808
989
  return this.sessionIdentityReady;
809
990
  };
810
- const ref = await Promise.race([prepare(), interrupted]);
991
+ const ready = await Promise.race([prepare(), interrupted]);
992
+ // The identity promise resolves once. A host may later adopt a new scope
993
+ // for the same conversation (for example a handoff generation), so the
994
+ // prepared session is the adapter's current reference, as for Codex.
995
+ const ref = this._sessionRef?.sessionId === ready.sessionId ? this._sessionRef : ready;
811
996
  if (!ref.validatedAt)
812
997
  throw new Error("Claude session identity was not provider-validated during initialization");
813
998
  return ref;
@@ -857,6 +1042,7 @@ export class ClaudeAdapter {
857
1042
  }
858
1043
  }
859
1044
  async compactInternal() {
1045
+ providerControlOperationV2("claude", "/compact");
860
1046
  this.compactResult = undefined;
861
1047
  this.manualCompactionInFlight = true;
862
1048
  const normalMs = Math.min(180_000, Math.max(1, this.opts.compactionTimeoutMs ?? 120_000));
@@ -898,7 +1084,9 @@ export class ClaudeAdapter {
898
1084
  // queue, including a later local command. Establish identity first.
899
1085
  if (!this.providerIdentityObserved && typeof this.query.supportedCommands === "function")
900
1086
  await this.prepareSession();
1087
+ providerControlOperationV2("claude", "applyFlagSettings");
901
1088
  await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true }));
1089
+ providerControlOperationV2("claude", "getContextUsage");
902
1090
  const baseline = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
903
1091
  if (!Number.isFinite(baseline.maxTokens) || baseline.maxTokens <= 0
904
1092
  || !Number.isFinite(baseline.autoCompactThreshold) || baseline.autoCompactThreshold === undefined
@@ -912,7 +1100,9 @@ export class ClaudeAdapter {
912
1100
  if (reserve < 0)
913
1101
  throw new Error("Claude reported an invalid automatic-compaction reserve");
914
1102
  const requestedWindow = tokenLimit(baseline.maxTokens, threshold) + reserve;
1103
+ providerControlOperationV2("claude", "applyFlagSettings");
915
1104
  await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow }));
1105
+ providerControlOperationV2("claude", "getContextUsage");
916
1106
  const installed = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
917
1107
  if (!installed.isAutoCompactEnabled || !Number.isFinite(installed.maxTokens) || installed.maxTokens <= 0
918
1108
  || !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
@@ -942,6 +1132,7 @@ export class ClaudeAdapter {
942
1132
  restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
943
1133
  async contextUsage() {
944
1134
  try {
1135
+ providerControlOperationV2("claude", "getContextUsage");
945
1136
  const usage = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
946
1137
  const result = { used: usage.totalTokens, maximum: usage.maxTokens, percentage: usage.percentage, observedAt: new Date().toISOString(), source: "provider-query" };
947
1138
  this.eventQueue.push({ kind: "context-usage", ...result });
@@ -959,14 +1150,21 @@ export class ClaudeAdapter {
959
1150
  return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
960
1151
  if (settings.model === this.opts.model)
961
1152
  return { ok: true };
1153
+ providerControlOperationV2("claude", "/model");
1154
+ if (settings.model && /[\r\n]/.test(settings.model))
1155
+ return { ok: false, error: "Model control requires one model identifier" };
962
1156
  if (this.opts.fast)
963
1157
  await this.prepareFastMode();
1158
+ if (this.fastModeEnabled)
1159
+ providerControlOperationV2("claude", "applyFlagSettings");
964
1160
  if (this.fastModeEnabled)
965
1161
  await new OperationDeadline("Claude standard speed", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ fastMode: false }));
966
1162
  this.fastModeEnabled = false;
967
1163
  const result = await new OperationDeadline("Claude model settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.sendTurn(`/model ${settings.model ?? "default"}`), () => this.abort.abort());
968
1164
  if (result.isError)
969
1165
  return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
1166
+ if (result.numTurns !== 0 || result.structured)
1167
+ return { ok: false, error: "Model control did not complete as a non-authoritative provider acknowledgement" };
970
1168
  this.opts.model = settings.model;
971
1169
  this.fastModePrepared = undefined;
972
1170
  this.fastModeUnavailable = false;
@@ -995,8 +1193,15 @@ export class ClaudeAdapter {
995
1193
  catch {
996
1194
  // interrupt is best-effort — ignore if no turn is active
997
1195
  }
998
- this.query.close?.();
999
- await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
1196
+ try {
1197
+ this.query.close?.();
1198
+ await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
1199
+ }
1200
+ finally {
1201
+ this.finishQueryBudget();
1202
+ if (this.scratchDirectory)
1203
+ rmSync(this.scratchDirectory, { recursive: true, force: true });
1204
+ }
1000
1205
  }
1001
1206
  failSessionIdentity(error) {
1002
1207
  this.rejectSessionIdentity(error);
@@ -1137,7 +1342,7 @@ export class ClaudeAdapter {
1137
1342
  pending.idleTimer.unref();
1138
1343
  }
1139
1344
  providerIdleTimeoutMs() {
1140
- const configured = this.opts.providerIdleTimeoutMs;
1345
+ const configured = this.opts.providerIdleTimeoutMs ?? Number(process.env.RAFI_PROVIDER_IDLE_TIMEOUT_MS);
1141
1346
  return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
1142
1347
  }
1143
1348
  streamFailureResult(message, sessionUnavailable) {