@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2653-next.71811ca

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 (109) hide show
  1. package/README.md +129 -39
  2. package/README.zh-CN.md +128 -39
  3. package/dist/activation-ledger-topology.js +1 -1
  4. package/dist/archivist-record-entry.js +47 -26
  5. package/dist/auditor-dossier-tool.js +3 -1
  6. package/dist/compliance-transport.js +17 -26
  7. package/dist/doctor-contracts.js +1 -2
  8. package/dist/dossier-resolution.js +4 -8
  9. package/dist/engine-detour-tool.js +33 -8
  10. package/dist/engine-detour.js +1 -0
  11. package/dist/evidence-child-executor.js +277 -424
  12. package/dist/host-contracts.js +22 -0
  13. package/dist/institutional-resolution.js +88 -0
  14. package/dist/merger-contracts.js +1 -2
  15. package/dist/navigator-attendance.js +16 -0
  16. package/dist/notary-contracts.js +2 -3
  17. package/dist/package-contracts/fixer-output.js +1 -2
  18. package/dist/packaged-role-registry.js +102 -9
  19. package/dist/pi/in-process-session.js +671 -0
  20. package/dist/public-cli/config.js +7 -5
  21. package/dist/public-cli/main.js +3767 -4113
  22. package/dist/reviewer-child-executor.js +6 -1
  23. package/dist/session-opening-materials.js +10 -38
  24. package/dist/sitian-appender.js +146 -0
  25. package/dist/sitian-contracts.js +21 -0
  26. package/dist/sitian-facade.js +15 -0
  27. package/dist/sitian-reader.js +52 -0
  28. package/extensions/role-runtime.ts +9 -7
  29. package/package.json +5 -5
  30. package/resources/engines/cursor.md +12 -26
  31. package/resources/engines/hermes.md +2 -23
  32. package/resources/engines/opus.md +1 -16
  33. package/resources/navigator-route-playbook.md +2 -2
  34. package/scripts/build-package.mjs +0 -1
  35. package/souls/quality-law.md +1 -1
  36. package/src/activation-ledger-topology.ts +1 -1
  37. package/src/archivist-record-entry.ts +57 -45
  38. package/src/auditor-dossier-tool.ts +4 -2
  39. package/src/canonical-skill-binding.ts +37 -27
  40. package/src/collector-role.ts +11 -20
  41. package/src/collector-tool-schemas.ts +1 -4
  42. package/src/compliance-transport.ts +19 -44
  43. package/src/doctor-auditor.ts +1 -5
  44. package/src/doctor-contracts.ts +1 -4
  45. package/src/doctor-role.ts +31 -8
  46. package/src/dossier-resolution.ts +8 -8
  47. package/src/engine-detour-tool.ts +48 -19
  48. package/src/engine-detour.ts +3 -0
  49. package/src/evidence-child-executor.ts +277 -485
  50. package/src/factory-board.ts +3 -3
  51. package/src/gatekeeper-role.ts +21 -10
  52. package/src/host-contracts.ts +386 -0
  53. package/src/in-process-session.ts +1 -115
  54. package/src/institutional-resolution.ts +135 -0
  55. package/src/judge-auditor.ts +1 -5
  56. package/src/judge-role.ts +37 -51
  57. package/src/merger-contracts.ts +1 -4
  58. package/src/merger-role.ts +7 -11
  59. package/src/navigator-attendance.ts +20 -1
  60. package/src/notary-contracts.ts +8 -11
  61. package/src/notary-role.ts +7 -23
  62. package/src/notary-source-run.ts +4 -3
  63. package/src/package-contracts/fixer-output.ts +1 -4
  64. package/src/package-resources/method-skill-binding.ts +10 -34
  65. package/src/packaged-role-registry.ts +122 -12
  66. package/src/pi/adapter.ts +192 -0
  67. package/src/pi/durable-principal.ts +98 -0
  68. package/src/pi/in-process-session.ts +819 -0
  69. package/src/pi/known-failure.ts +77 -0
  70. package/src/pi/pi-normalization.ts +284 -0
  71. package/src/pi/role-turn-host.ts +459 -0
  72. package/src/public-cli/auto-resume.ts +42 -44
  73. package/src/public-cli/cli.ts +171 -257
  74. package/src/public-cli/coder-run.ts +115 -433
  75. package/src/public-cli/collector-run.ts +62 -314
  76. package/src/public-cli/config.ts +7 -5
  77. package/src/public-cli/doctor-run.ts +41 -52
  78. package/src/public-cli/fixer-run.ts +122 -427
  79. package/src/public-cli/invocation.ts +287 -96
  80. package/src/public-cli/judge-run.ts +92 -422
  81. package/src/public-cli/merger-run.ts +167 -429
  82. package/src/public-cli/notary-run.ts +41 -47
  83. package/src/public-cli/option-definitions.ts +136 -3
  84. package/src/public-cli/post-admission.ts +497 -0
  85. package/src/public-cli/public-run-credentials.ts +4 -4
  86. package/src/public-cli/reviewer-dispatch-rejection.ts +106 -0
  87. package/src/public-cli/reviewer-run.ts +132 -449
  88. package/src/public-cli/run-lifecycle.ts +339 -327
  89. package/src/public-cli/settlement.ts +251 -134
  90. package/src/public-cli/terminal.ts +3 -9
  91. package/src/public-cli/turn-request.ts +87 -0
  92. package/src/reviewer-child-executor.ts +9 -1
  93. package/src/reviewer-role.ts +11 -20
  94. package/src/role-runtime.ts +129 -78
  95. package/src/session-opening-materials.ts +25 -39
  96. package/src/sitian-appender.ts +171 -0
  97. package/src/sitian-contracts.ts +106 -0
  98. package/src/sitian-facade.ts +18 -0
  99. package/src/sitian-reader.ts +62 -0
  100. package/src/worker-role.ts +42 -40
  101. package/src/worker-submission-gates.ts +33 -0
  102. package/dist/archivist-role-run-coordinates.js +0 -28
  103. package/dist/package-contracts/terminating-infrastructure.js +0 -90
  104. package/resources/engines/sonnet.md +0 -27
  105. package/resources/engines/zcode.md +0 -63
  106. package/src/archivist-role-run-coordinates.ts +0 -45
  107. package/src/package-contracts/terminating-infrastructure.ts +0 -132
  108. package/src/public-cli/explicit-internal.ts +0 -427
  109. package/src/public-cli/one-shot-dispatch.ts +0 -300
@@ -7,28 +7,25 @@ import { mkdtemp, rm } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import {
10
- createAssistantMessageEventStream,
11
- InMemoryCredentialStore,
12
10
  type Api,
13
11
  type AssistantMessage,
14
12
  type Context,
15
13
  type Model,
16
- type Provider,
17
14
  type ProviderStreamOptions,
18
15
  type Usage,
19
16
  } from "@earendil-works/pi-ai";
20
17
  import type {
21
18
  ExtensionContext,
22
- ModelRuntime,
23
19
  SessionManager,
24
20
  } from "@earendil-works/pi-coding-agent";
25
21
 
26
22
  import {
27
23
  AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE,
28
24
  AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
29
- prepareComplianceDispatch,
30
25
  type AuditorParentAttemptBinding,
31
26
  } from "./compliance-transport.ts";
27
+ import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
28
+ import { sitianReport } from "./sitian-facade.ts";
32
29
  import { createEngineDetourToolDefinition } from "./engine-detour-tool.ts";
33
30
  import { engineNameFromEnv } from "./engine-detour.ts";
34
31
  import {
@@ -38,15 +35,16 @@ import {
38
35
  } from "./package-resources/engine-material.ts";
39
36
  import { readPackageMaterial } from "./session-opening-materials.ts";
40
37
  import {
41
- formatModelSpec,
42
- loadPublicCliConfig,
43
- resolveGateOfficerModelSelection,
44
38
  type GateOfficerSeat,
45
39
  } from "./public-cli/config.ts";
46
- import type { PublicThinkingLevel } from "./public-cli/registry.ts";
40
+ import { readInstitutionalSeatSelection } from "./institutional-resolution.ts";
41
+ import type {
42
+ OpenPiInstitutionalSessionOptions,
43
+ OpenPiInstitutionalSessionResult,
44
+ } from "./pi/in-process-session.ts";
45
+ import type { HostAssistantTurnResult, HostContext } from "./host-contracts.ts";
47
46
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
48
47
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
49
- import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
50
48
  import {
51
49
  hasUpstreamErrorTestimony,
52
50
  isNonSuccessHttpStatus,
@@ -95,11 +93,6 @@ export class AuditorTurnLimitError extends Error {
95
93
  this.name = "AuditorTurnLimitError";
96
94
  }
97
95
  }
98
- export type AuditorCompletion = (
99
- model: Model<Api>,
100
- context: Context,
101
- options: ProviderStreamOptions,
102
- ) => Promise<AssistantMessage>;
103
96
  export type AuditorDecisionTool = {
104
97
  name: string;
105
98
  description: string;
@@ -136,288 +129,40 @@ export async function withInProcessScratch<T>(
136
129
  }
137
130
  }
138
131
 
139
- export type InheritedRuntimeOptions = {
140
- readonly context: ExtensionContext;
141
- readonly label: string;
142
- readonly runCompletion?: AuditorCompletion;
143
- readonly injectedSystemPrompt?: string;
144
- readonly signal?: AbortSignal;
145
- /** When true, wrap provider streams with ADR 0059 idle-only retry. */
146
- readonly idleRetry?: boolean;
147
- /**
148
- * Explicit child model. When set, auth/availability failures do not fall back
149
- * to the parent session model (#453 gate overrides).
150
- */
151
- readonly model?: Model<Api>;
152
- };
153
-
154
- export type InheritedRuntime = {
155
- readonly runtime: ModelRuntime;
156
- readonly model: Model<Api>;
157
- readonly dispatch: Awaited<ReturnType<typeof prepareComplianceDispatch>>;
158
- /** Present when a stream failed under idle-retry instrumentation. */
159
- streamFailure: unknown;
160
- };
161
-
162
132
  /**
163
- * Build an inherited ModelRuntime + provider. Optional idle-only retry is the
164
- * ADR 0059 provider-stream seam (not package-owned-tool idle).
133
+ * Run every child cleanup, aggregating failures so one throwing cleanup never
134
+ * skips the rest (e.g. handle.close must still run when unsubscribe throws).
135
+ * If a primary failure is supplied and cleanup also failed, the two are combined
136
+ * into an AggregateError; otherwise only the failing branch is surfaced.
165
137
  */
166
- export async function createInheritedRuntime(options: InheritedRuntimeOptions): Promise<InheritedRuntime> {
167
- const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
168
- // Explicit override wins; never silent-fallback to parent when override is set (#453).
169
- const activeModel = options.model ?? options.context.model;
170
- if (activeModel === undefined) throw new Error(`${options.label} requires an active model`);
171
- const dispatch = await prepareComplianceDispatch(activeModel, options.context, options.label);
172
- const parentProvider = options.runCompletion === undefined
173
- ? options.context.modelRegistry.getProvider(activeModel.provider)
174
- : undefined;
175
- if (parentProvider === undefined && options.runCompletion === undefined) {
176
- throw new Error(`${options.label} provider not found: ${activeModel.provider}`);
177
- }
178
- const runtime = await ModelRuntime.create({
179
- credentials: new InMemoryCredentialStore(),
180
- modelsPath: null,
181
- });
182
- // Injected completions historically accepted the minimal model exposed by an
183
- // ExtensionContext. AgentSession crosses ModelRuntime first, so complete the
184
- // model metadata required by that runtime without changing provider identity.
185
- const inheritedModel: Model<Api> = options.runCompletion === undefined
186
- ? dispatch.model
187
- : {
188
- ...dispatch.model,
189
- name: dispatch.model.name ?? dispatch.model.id,
190
- baseUrl: dispatch.model.baseUrl ?? "",
191
- reasoning: dispatch.model.reasoning ?? false,
192
- input: dispatch.model.input ?? ["text"],
193
- cost: dispatch.model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
194
- contextWindow: dispatch.model.contextWindow ?? 1,
195
- maxTokens: dispatch.model.maxTokens ?? 1,
196
- };
197
- const state: InheritedRuntime = {
198
- runtime,
199
- model: inheritedModel,
200
- dispatch,
201
- streamFailure: undefined,
202
- };
203
-
204
- const abortReason = (signal: AbortSignal): unknown =>
205
- signal.reason ?? new Error(`${options.label} provider stream aborted`);
206
-
207
- async function waitForStream<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
208
- if (signal.aborted) throw abortReason(signal);
209
- let onAbort: (() => void) | undefined;
138
+ async function runChildCleanup(
139
+ cleanups: ReadonlyArray<() => void | Promise<void>>,
140
+ primaryFailure: unknown,
141
+ label: string,
142
+ ): Promise<void> {
143
+ let cleanupFailure: unknown;
144
+ for (const cleanup of cleanups) {
210
145
  try {
211
- return await Promise.race([
212
- promise,
213
- new Promise<never>((_resolve, reject) => {
214
- onAbort = () => reject(abortReason(signal));
215
- signal.addEventListener("abort", onAbort, { once: true });
216
- }),
217
- ]);
218
- } finally {
219
- if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
146
+ await cleanup();
147
+ } catch (failure) {
148
+ cleanupFailure = cleanupFailure === undefined
149
+ ? failure
150
+ : new AggregateError([cleanupFailure, failure], `${label} cleanup failed`, {
151
+ cause: cleanupFailure,
152
+ });
220
153
  }
221
154
  }
222
-
223
- const createRetriedStream = (
224
- simple: boolean,
225
- model: Model<Api>,
226
- context: Context,
227
- request?: ProviderStreamOptions,
228
- ): ReturnType<Provider["stream"]> => {
229
- const wrapped = createAssistantMessageEventStream();
230
- void (async () => {
231
- for (let attempt = 0; ; attempt += 1) {
232
- const idle = createStreamIdleGuard(
233
- options.signal === undefined ? {} : { parentSignal: options.signal },
234
- );
235
- // Typed HTTP status observed via onResponse — never inferred from prose.
236
- let observedHttpStatus: number | undefined;
237
- try {
238
- const requestSignal = request?.signal;
239
- const streamSignal = requestSignal === undefined
240
- ? idle.signal
241
- : AbortSignal.any([idle.signal, requestSignal]);
242
- const priorOnResponse = request?.onResponse;
243
- const inheritedRequest = {
244
- ...(request ?? {}),
245
- ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
246
- signal: streamSignal,
247
- onResponse: async (
248
- response: { status: number; headers: Record<string, string> },
249
- model: Model<Api>,
250
- ) => {
251
- if (typeof response?.status === "number") observedHttpStatus = response.status;
252
- await priorOnResponse?.(response, model);
253
- },
254
- } as ProviderStreamOptions;
255
- if (options.runCompletion !== undefined) {
256
- await new Promise<void>((resolve) => setImmediate(resolve));
257
- if (streamSignal.aborted) throw abortReason(streamSignal);
258
- const completed = await waitForStream(
259
- options.runCompletion(
260
- model,
261
- options.injectedSystemPrompt === undefined
262
- ? context
263
- : { ...context, systemPrompt: options.injectedSystemPrompt },
264
- inheritedRequest,
265
- ),
266
- streamSignal,
267
- );
268
- const response: AssistantMessage = attachObservedHttpStatus({
269
- ...completed,
270
- api: model.api,
271
- provider: model.provider,
272
- model: model.id,
273
- }, observedHttpStatus);
274
- if (response.stopReason === "error" || response.stopReason === "aborted") {
275
- wrapped.push({ type: "error", reason: response.stopReason, error: response });
276
- } else {
277
- wrapped.end(response);
278
- }
279
- return;
280
- }
281
- const source = simple
282
- ? parentProvider!.streamSimple(model, context, inheritedRequest as any)
283
- : parentProvider!.stream(model, context, inheritedRequest as any);
284
- let sawEvent = false;
285
- const iterator = source[Symbol.asyncIterator]();
286
- while (true) {
287
- const next = await waitForStream(iterator.next(), idle.signal);
288
- if (next.done) break;
289
- sawEvent = true;
290
- idle.poke();
291
- wrapped.push(enrichStreamEvent(next.value, observedHttpStatus) as any);
292
- }
293
- const response = attachObservedHttpStatus(
294
- await waitForStream(source.result(), idle.signal),
295
- observedHttpStatus,
296
- );
297
- if (!sawEvent) wrapped.end(response);
298
- return;
299
- } catch (error) {
300
- if (request?.signal?.aborted) {
301
- const response: AssistantMessage = {
302
- role: "assistant",
303
- content: [],
304
- api: model.api,
305
- provider: model.provider,
306
- model: model.id,
307
- usage: emptyUsage(),
308
- stopReason: "aborted",
309
- errorMessage: "Auditor session aborted",
310
- timestamp: Date.now(),
311
- };
312
- wrapped.push({ type: "error", reason: "aborted", error: response });
313
- wrapped.end(response);
314
- return;
315
- }
316
- const failure = isStreamIdleTimeoutError(idle.signal.reason) ? idle.signal.reason : error;
317
- if (
318
- options.idleRetry === true
319
- && isStreamIdleTimeoutError(failure)
320
- && attempt < DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES
321
- && options.signal?.aborted !== true
322
- ) {
323
- continue;
324
- }
325
- state.streamFailure = failure;
326
- const projected = projectStructuredRemote(failure);
327
- // Prefer structured fields on the thrown failure; fall back to onResponse observation.
328
- const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
329
- const response = {
330
- role: "assistant" as const,
331
- content: [] as [],
332
- api: model.api,
333
- provider: model.provider,
334
- model: model.id,
335
- usage: emptyUsage(),
336
- stopReason: "error" as const,
337
- // Preserve the held failure message bytes — do not rewrite.
338
- errorMessage: failure instanceof Error ? failure.message : String(failure),
339
- timestamp: Date.now(),
340
- ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
341
- ...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
342
- ...(projected.body === undefined ? {} : { body: projected.body }),
343
- ...(projected.code === undefined ? {} : { code: projected.code }),
344
- ...(projected.errno === undefined ? {} : { errno: projected.errno }),
345
- } as unknown as AssistantMessage;
346
- wrapped.push({ type: "error", reason: "error", error: response });
347
- wrapped.end(response);
348
- return;
349
- } finally {
350
- idle.dispose();
351
- }
352
- }
353
- })();
354
- return wrapped as ReturnType<Provider["stream"]>;
355
- };
356
-
357
- // Provider id must match activeModel.provider so ModelRuntime auth lookup
358
- // finds this registration when a gate override changes provider (#453).
359
- const provider: Provider = options.idleRetry === true || options.runCompletion !== undefined
360
- ? {
361
- id: activeModel.provider,
362
- name: parentProvider?.name ?? options.label,
363
- auth: {
364
- apiKey: {
365
- name: `Inherited ${options.label} authentication`,
366
- async resolve() {
367
- const { env, ...auth } = dispatch.auth;
368
- return {
369
- auth: {
370
- ...auth,
371
- ...(dispatch.model.baseUrl === undefined ? {} : { baseUrl: dispatch.model.baseUrl }),
372
- },
373
- ...(env === undefined ? {} : { env }),
374
- };
375
- },
376
- },
377
- },
378
- getModels() { return [inheritedModel]; },
379
- stream(model, context, request) {
380
- return createRetriedStream(false, model, context, request as ProviderStreamOptions | undefined);
381
- },
382
- streamSimple(model, context, request) {
383
- return createRetriedStream(true, model, context, request as ProviderStreamOptions | undefined);
384
- },
385
- }
386
- : {
387
- id: parentProvider!.id,
388
- name: parentProvider!.name,
389
- ...(parentProvider!.baseUrl === undefined ? {} : { baseUrl: parentProvider!.baseUrl }),
390
- ...(parentProvider!.headers === undefined ? {} : { headers: parentProvider!.headers }),
391
- auth: {
392
- apiKey: {
393
- name: `Inherited ${options.label} authentication`,
394
- async resolve() {
395
- return {
396
- auth: {
397
- ...(dispatch.auth.apiKey === undefined ? {} : { apiKey: dispatch.auth.apiKey }),
398
- ...(dispatch.auth.headers === undefined ? {} : { headers: dispatch.auth.headers }),
399
- ...(dispatch.model.baseUrl === undefined ? {} : { baseUrl: dispatch.model.baseUrl }),
400
- },
401
- ...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
402
- };
403
- },
404
- },
405
- },
406
- getModels() { return [inheritedModel]; },
407
- stream(model, childContext, streamOptions) {
408
- return parentProvider!.stream(model, childContext, streamOptions);
409
- },
410
- streamSimple(model, childContext, streamOptions) {
411
- return parentProvider!.streamSimple(model, childContext, streamOptions);
412
- },
413
- };
414
-
415
- runtime.registerNativeProvider(provider);
416
- // registerNativeProvider fires a background refresh; await it so child
417
- // AgentSession.prompt sees configured auth without racing void refresh
418
- // (mock timers / slow CI otherwise stall before the compliance stream).
419
- await runtime.refresh({ allowNetwork: false });
420
- return state;
155
+ if (cleanupFailure === undefined) return;
156
+ if (primaryFailure !== undefined) {
157
+ throw new AggregateError(
158
+ [primaryFailure, cleanupFailure],
159
+ `${label} execution and cleanup failed`,
160
+ { cause: primaryFailure },
161
+ );
162
+ }
163
+ throw new AggregateError([cleanupFailure], `${label} cleanup failed`, {
164
+ cause: cleanupFailure,
165
+ });
421
166
  }
422
167
 
423
168
  type EvidenceChildFailureClassification = "provider" | "child" | "unknown";
@@ -542,6 +287,22 @@ function classifiedError(error: unknown, evidenceChildFailure: EvidenceChildFail
542
287
  return Object.assign(wrapped, { evidenceChildFailure: classification });
543
288
  }
544
289
 
290
+ /** Extract the first text diagnostic from a flattened toolResult error `details`. */
291
+ function extractToolResultText(details: unknown): string | undefined {
292
+ if (typeof details !== "object" || details === null) return undefined;
293
+ const record = details as Record<string, unknown>;
294
+ const content = record.content;
295
+ if (Array.isArray(content)) {
296
+ for (const part of content) {
297
+ if (typeof part === "object" && part !== null) {
298
+ const text = (part as Record<string, unknown>).text;
299
+ if (typeof text === "string" && text.trim() !== "") return text;
300
+ }
301
+ }
302
+ }
303
+ return undefined;
304
+ }
305
+
545
306
  function emptyUsage(): Usage {
546
307
  return {
547
308
  input: 0,
@@ -568,10 +329,14 @@ function addUsage(total: Usage, next: Usage): void {
568
329
 
569
330
  // ── evidence child (reviewer legs) ────────────────────────────────────────
570
331
 
332
+ /** Evidence child execution options. */
571
333
  export type EvidenceChildExecuteOptions = Readonly<{
572
334
  signal?: AbortSignal;
573
335
  /** Parent directory for credential/config scratch. Defaults to os.tmpdir(). */
574
336
  credentialScratchParent?: string;
337
+ /** Run directory carrying the institutional resolution page (#518). Derives
338
+ * from context when absent. */
339
+ runDirectory?: string;
575
340
  /**
576
341
  * Package root for optional engine method-material resolution (#378).
577
342
  * Required only when AK_ROLE_ENGINE is set and packaged notes should attach.
@@ -586,6 +351,11 @@ export async function executeEvidenceChild(
586
351
  options: EvidenceChildExecuteOptions = {},
587
352
  ): Promise<{ report: string; usage: Usage; prompt: ReviewerPromptText }> {
588
353
  const signal = options.signal;
354
+ const runDirectory = options.runDirectory ?? auditorRunDirectory(context);
355
+ if (runDirectory === undefined) {
356
+ throw new Error("Evidence child requires a run directory carrying the institutional resolution page");
357
+ }
358
+ const selection = await readInstitutionalSeatSelection(runDirectory, "evidenceChild");
589
359
  return withInProcessScratch(
590
360
  {
591
361
  prefix: "ak-evidence-child-",
@@ -594,17 +364,8 @@ export async function executeEvidenceChild(
594
364
  : { parentDirectory: options.credentialScratchParent }),
595
365
  },
596
366
  async (childConfigDir) => {
597
- const { openInProcessAgentSession } = await import("./in-process-session.ts");
367
+ const { openPiInstitutionalSession } = await import("./pi/in-process-session.ts");
598
368
  const { createRecordSession } = await import("./archivist-record-entry.ts");
599
- let inherited: InheritedRuntime;
600
- try {
601
- inherited = await createInheritedRuntime({
602
- context,
603
- label: "Evidence child",
604
- });
605
- } catch (error) {
606
- throw classifiedError(error, "provider");
607
- }
608
369
  // #378: when labor engine is configured, legs get the same detour tool + material
609
370
  // dual-path as the parent seat (ADR 0069 detour-rejoins-main-road).
610
371
  const engineName = engineNameFromEnv();
@@ -632,37 +393,44 @@ export async function executeEvidenceChild(
632
393
  },
633
394
  });
634
395
  // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
635
- // Single createAgentSession owner: in-process-session.ts.
636
- const { session, dispose } = await openInProcessAgentSession({
637
- cwd: workspace,
638
- agentDir: childConfigDir,
639
- model: inherited.model,
640
- thinkingLevel: context.thinkingLevel ?? "off",
641
- modelRuntime: inherited.runtime,
642
- systemPrompt: await buildEvidenceChildSystemPrompt(engineMaterial),
643
- ...(engineDetourTool === undefined
644
- ? {}
645
- : { customTools: [engineDetourTool] }),
646
- sessionManager: createRecordSession({
396
+ // Single open seam owner: pi/in-process-session.ts.
397
+ let opened: Awaited<ReturnType<typeof openPiInstitutionalSession>>;
398
+ try {
399
+ opened = await openPiInstitutionalSession({
647
400
  cwd: workspace,
648
- kind: "evidence-children",
649
- ...(context.sessionManager === undefined ? {} : { parent: context.sessionManager }),
650
- }),
651
- });
401
+ agentDir: childConfigDir,
402
+ selection,
403
+ systemPrompt: await buildEvidenceChildSystemPrompt(engineMaterial),
404
+ ...(engineDetourTool === undefined
405
+ ? {}
406
+ : { customTools: [engineDetourTool] }),
407
+ sessionManager: createRecordSession({
408
+ cwd: workspace,
409
+ kind: "evidence-children",
410
+ ...(context.sessionManager === undefined ? {} : { parent: context.sessionManager }),
411
+ }),
412
+ ...(signal === undefined ? {} : { signal }),
413
+ label: "Evidence child",
414
+ });
415
+ } catch (error) {
416
+ throw classifiedError(error, "provider");
417
+ }
418
+ const { handle } = opened;
652
419
  const usage = emptyUsage();
653
- const unsubscribe = session.subscribe((event) => {
654
- if (event.type === "message_end" && event.message.role === "assistant") {
655
- addUsage(usage, event.message.usage);
420
+ const unsubscribe = handle.subscribe((event) => {
421
+ if (event.type === "message_end" && event.role === "assistant") {
422
+ if (event.usage) addUsage(usage, event.usage);
656
423
  }
657
424
  });
658
- const abortChild = () => { void session.abort(); };
425
+ const abortChild = () => { handle.abort(); };
659
426
  if (signal?.aborted) abortChild();
660
427
  else signal?.addEventListener("abort", abortChild, { once: true });
661
428
  let primaryFailure: unknown;
662
429
  try {
663
430
  const delivered = prompt;
431
+ let turnResult: HostAssistantTurnResult;
664
432
  try {
665
- await session.prompt(delivered);
433
+ turnResult = await handle.prompt(delivered);
666
434
  } catch (error) {
667
435
  if (engineDetourFailure !== undefined) {
668
436
  throw classifiedError(engineDetourFailure, "child");
@@ -673,30 +441,31 @@ export async function executeEvidenceChild(
673
441
  throw classifiedError(engineDetourFailure, "child");
674
442
  }
675
443
  if (signal?.aborted) throw new Error("Evidence child was cancelled");
676
- const lastAssistant = [...session.messages]
677
- .reverse()
678
- .find((message) => message.role === "assistant");
444
+ const lastAssistant = turnResult.messages !== undefined
445
+ ? [...turnResult.messages].reverse().find((message: any) => message?.role === "assistant") as AssistantMessage | undefined
446
+ : undefined;
679
447
  // error|aborted assistant stops share the upstream-testimony rule: provider only
680
448
  // with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
681
449
  // for real local child/report failures (no assistant / blank report / cleanup).
682
450
  if (
683
- lastAssistant?.role === "assistant"
684
- && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")
451
+ turnResult.stopReason === "error" || turnResult.stopReason === "aborted"
452
+ || (lastAssistant?.role === "assistant" && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted"))
685
453
  ) {
454
+ const errMsg = turnResult.errorMessage ?? lastAssistant?.errorMessage ?? "";
686
455
  throw classifiedError(
687
- new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }),
688
- projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown",
456
+ new Error(errMsg, { cause: lastAssistant }),
457
+ lastAssistant && projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown",
689
458
  );
690
459
  }
691
- if (lastAssistant?.role !== "assistant") {
460
+ if (lastAssistant !== undefined && lastAssistant.role !== "assistant") {
692
461
  throw classifiedError(
693
462
  new Error("Evidence child child terminated without a report", {
694
- cause: lastAssistant ?? session.messages,
463
+ cause: lastAssistant ?? turnResult.messages,
695
464
  }),
696
465
  "child",
697
466
  );
698
467
  }
699
- const report = session.getLastAssistantText() ?? "";
468
+ const report = turnResult.text;
700
469
  if (report.trim().length === 0) {
701
470
  throw new Error("Evidence child returned a blank child report");
702
471
  }
@@ -706,30 +475,7 @@ export async function executeEvidenceChild(
706
475
  throw primaryFailure;
707
476
  } finally {
708
477
  signal?.removeEventListener("abort", abortChild);
709
- let cleanupFailure: unknown;
710
- for (const cleanup of [() => unsubscribe(), () => dispose()]) {
711
- try {
712
- cleanup();
713
- } catch (failure) {
714
- cleanupFailure = cleanupFailure === undefined
715
- ? failure
716
- : new AggregateError([cleanupFailure, failure], "Reviewer child cleanup failed", {
717
- cause: cleanupFailure,
718
- });
719
- }
720
- }
721
- if (cleanupFailure !== undefined) {
722
- if (primaryFailure !== undefined) {
723
- throw new AggregateError(
724
- [primaryFailure, cleanupFailure],
725
- "Reviewer child execution and cleanup failed",
726
- { cause: primaryFailure },
727
- );
728
- }
729
- throw new AggregateError([cleanupFailure], "Reviewer child cleanup failed", {
730
- cause: cleanupFailure,
731
- });
732
- }
478
+ await runChildCleanup([() => unsubscribe(), () => handle.close()], primaryFailure, "Reviewer child");
733
479
  }
734
480
  },
735
481
  );
@@ -743,45 +489,25 @@ export type AuditorRoleOptions = {
743
489
  tool: AuditorDecisionTool;
744
490
  dossierTool: AuditorDecisionTool;
745
491
  roleLabel: string;
746
- context: ExtensionContext;
492
+ context: ExtensionContext | HostContext;
747
493
  signal?: AbortSignal;
748
- runCompletion?: AuditorCompletion;
749
494
  retainResponse?(response: AssistantMessage): void;
750
- /**
751
- * Province seat identity only — shared executor owns config load, registry
752
- * resolve, and loud auth/availability failure (#453 / ADR 0018).
753
- */
495
+ /** Province seat identity only — shared executor owns seat resolution, and
496
+ * loud auth/availability failure (#453 / ADR 0018). */
754
497
  gateSeat?: GateOfficerSeat;
498
+ /** Run directory carrying the institutional resolution page (#518). Derives
499
+ * from context when absent. */
500
+ runDirectory?: string;
755
501
  };
756
502
 
757
- /**
758
- * Resolve province child model from public-cli persistent config (#453).
759
- * Own override > gatekeeper override > unset (inherit parent). Availability
760
- * failures throw — createInheritedRuntime must not silent-fallback to parent.
761
- */
762
- async function resolveGateSeatModelOptions(
763
- context: ExtensionContext,
764
- seat: GateOfficerSeat,
765
- roleLabel: string,
766
- ): Promise<{ model?: Model<Api>; thinkingLevel?: PublicThinkingLevel }> {
767
- const selection = resolveGateOfficerModelSelection(await loadPublicCliConfig(), seat);
768
- if (selection === undefined) return {};
769
- const find = context.modelRegistry.find?.bind(context.modelRegistry);
770
- if (typeof find !== "function") {
771
- throw new Error(`${roleLabel} model registry cannot resolve models`);
772
- }
773
- const model = find(selection.provider, selection.model);
774
- if (model === undefined) {
775
- throw new Error(`${roleLabel} model is unavailable: ${formatModelSpec(selection)}`);
776
- }
777
- return {
778
- model,
779
- ...(selection.thinking === undefined ? {} : { thinkingLevel: selection.thinking }),
780
- };
503
+ /** Resolution-page seat key for an auditor invocation: province gate seats map
504
+ * to their own page seats; doctor/judge compliance audits use the auditor seat. */
505
+ function auditorSeatKey(gateSeat: GateOfficerSeat | undefined): string {
506
+ return gateSeat ?? "auditor";
781
507
  }
782
508
 
783
509
  /**
784
- * Auditor lifecycle via the shared in-process helper.
510
+ * Auditor lifecycle via the shared institutional sub-session adapter.
785
511
  * Adapter keeps role label / soul / decision tool / result projection only.
786
512
  * No tools allowlist (ADR 0064). Provider-stream idle-only retry (ADR 0059).
787
513
  * Durable child session via ADR 0065 archivist entry.
@@ -790,27 +516,14 @@ export async function executeAuditorChild(
790
516
  options: AuditorRoleOptions,
791
517
  ): Promise<{ decision: unknown; response: AssistantMessage; noReceiptLifecycle?: NoReceiptLifecycleFacts }> {
792
518
  const { createRecordSession } = await import("./archivist-record-entry.ts");
519
+ const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
520
+ if (runDirectory === undefined) {
521
+ throw new Error(`${options.roleLabel} requires a run directory carrying the institutional resolution page`);
522
+ }
523
+ const seat = auditorSeatKey(options.gateSeat);
524
+ const selection = await readInstitutionalSeatSelection(runDirectory, seat);
793
525
 
794
526
  return withInProcessScratch({ prefix: "ak-auditor-role-" }, async (scratch) => {
795
- const gate =
796
- options.gateSeat === undefined
797
- ? {}
798
- : await resolveGateSeatModelOptions(
799
- options.context,
800
- options.gateSeat,
801
- options.roleLabel,
802
- );
803
- const inherited = await createInheritedRuntime({
804
- context: options.context,
805
- label: options.roleLabel,
806
- idleRetry: true,
807
- ...(gate.model === undefined ? {} : { model: gate.model }),
808
- ...(options.runCompletion === undefined
809
- ? {}
810
- : { runCompletion: options.runCompletion, injectedSystemPrompt: options.systemPrompt }),
811
- ...(options.signal === undefined ? {} : { signal: options.signal }),
812
- });
813
-
814
527
  const cwd = options.context.cwd ?? process.cwd();
815
528
 
816
529
  let decision: unknown;
@@ -833,12 +546,15 @@ export async function executeAuditorChild(
833
546
  try {
834
547
  const result = await options.tool.execute(...args);
835
548
  delivery.recordAccepted();
836
- decision = args[1];
549
+ const rawDecision = args[1];
550
+ const isMissingArgs = rawDecision === undefined
551
+ || (typeof rawDecision === "object" && rawDecision !== null && !Array.isArray(rawDecision) && Object.keys(rawDecision).length === 0);
552
+ decision = isMissingArgs ? undefined : rawDecision;
837
553
  decisionCallId = args[0];
838
554
  decisionToolFailure = undefined;
839
555
  decisionToolFailures.delete(args[0]);
840
556
  decisionSubmitted = true;
841
- return result;
557
+ return { ...result, terminate: true };
842
558
  } catch (error) {
843
559
  decisionToolFailure = error;
844
560
  decisionToolFailures.set(args[0], error);
@@ -857,18 +573,34 @@ export async function executeAuditorChild(
857
573
  ...(parentSessionManager === undefined ? {} : { parent: parentSessionManager }),
858
574
  });
859
575
 
860
- // Shared session open — no tools allowlist (ADR 0064).
861
- const { openInProcessAgentSession: openSession } = await import("./in-process-session.ts");
862
- const { session, dispose } = await openSession({
576
+ // Shared session open — no tools allowlist (ADR 0064). Auth resolved
577
+ // child-locally from the explicit seat selection; adapter owns runtime/provider.
578
+ const { openPiInstitutionalSession } = await import("./pi/in-process-session.ts");
579
+ const evidenceToolFailures = new Map<string, unknown>();
580
+ const wrappedDossierTool = {
581
+ ...options.dossierTool,
582
+ label: options.roleLabel,
583
+ async execute(...args: any[]) {
584
+ try {
585
+ return await options.dossierTool.execute(...args);
586
+ } catch (error) {
587
+ evidenceToolFailures.set(args[0], error);
588
+ throw error;
589
+ }
590
+ },
591
+ };
592
+ const opened = await openPiInstitutionalSession({
863
593
  cwd,
864
594
  agentDir: scratch,
865
- model: inherited.model,
866
- thinkingLevel: gate.thinkingLevel ?? options.context.thinkingLevel ?? "off",
867
- modelRuntime: inherited.runtime,
595
+ selection,
868
596
  systemPrompt: options.systemPrompt,
869
- customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
597
+ customTools: [wrappedDossierTool, tool],
870
598
  sessionManager: auditorSessionManager,
599
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
600
+ idleRetry: true,
601
+ label: options.roleLabel,
871
602
  });
603
+ const { handle } = opened;
872
604
 
873
605
  const binding: AuditorParentAttemptBinding = {
874
606
  version: 1,
@@ -883,6 +615,16 @@ export async function executeAuditorChild(
883
615
  // Durable binding is a prerequisite: never observe the provider when its
884
616
  // response could not later be tied to the current parent attempt.
885
617
  auditorSessionManager.appendCustomEntry(AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, binding);
618
+ try {
619
+ sitianReport({
620
+ level: "event",
621
+ kind: "auditor",
622
+ cwd,
623
+ sessionParent: parentSessionFile,
624
+ payload: { type: AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, ...binding },
625
+ source: "evidence-child-executor",
626
+ });
627
+ } catch {}
886
628
 
887
629
  let turns = 0;
888
630
  const sessionUsage = emptyUsage();
@@ -892,31 +634,13 @@ export async function executeAuditorChild(
892
634
  let rejectedDecisionResponse: AssistantMessage | undefined;
893
635
  let promptNeighboringFailure: unknown;
894
636
  let promptDecisionFailures: unknown[] = [];
895
- const registeredToolNames = new Set(session.getAllTools().map((entry) => entry.name));
896
- const evidenceToolFailures = new Map<string, unknown>();
897
- for (const name of registeredToolNames) {
898
- if (name === tool.name) continue;
899
- const definition = session.getToolDefinition(name);
900
- if (definition === undefined) continue;
901
- const execute = definition.execute.bind(definition);
902
- definition.execute = async (...args: any[]) => {
903
- try {
904
- return await (execute as (...executeArgs: any[]) => Promise<any>)(...args);
905
- } catch (error) {
906
- evidenceToolFailures.set(args[0], error);
907
- throw error;
908
- }
909
- };
910
- }
911
637
  const findToolFailure = (response: AssistantMessage): unknown => {
912
638
  const callIds = response.content.flatMap((part) =>
913
- part.type === "toolCall" && part.name !== tool.name && registeredToolNames.has(part.name) ? [part.id] : []);
639
+ part.type === "toolCall" && part.name !== tool.name ? [part.id] : []);
914
640
  for (const callId of callIds) {
915
641
  if (evidenceToolFailures.has(callId)) return evidenceToolFailures.get(callId);
916
642
  }
917
- const callIdSet = new Set(callIds);
918
- return [...session.messages].reverse().find((message) =>
919
- message.role === "toolResult" && callIdSet.has(message.toolCallId) && message.isError);
643
+ return undefined;
920
644
  };
921
645
  const drainRejectedDecisionFailures = (response: AssistantMessage) => {
922
646
  for (const part of response.content) {
@@ -926,29 +650,74 @@ export async function executeAuditorChild(
926
650
  decisionToolFailures.delete(part.id);
927
651
  }
928
652
  };
929
- const unsubscribe = session.subscribe((event) => {
930
- if (event.type === "message_end" && event.message.role === "assistant" && boundaryResponse === undefined) {
653
+ const retainedAssistants: AssistantMessage[] = [];
654
+ const unsubscribe = handle.subscribe((event) => {
655
+ // Capture evidence-tool (non-decision) failures from the handle's real
656
+ // tool_result events. The old runCompletion path wrapped every child tool's
657
+ // execute to observe failures; through the real handle the adapter forwards
658
+ // tool_execution_end isError. Record the errored tool call id so an adjacent
659
+ // evidence failure can outrank a settled/correctable decision feedback.
660
+ if (event.type === "tool_result" && event.isError === true && event.toolName !== tool.name) {
661
+ // Through the real handle the child flattens a throwing evidence tool
662
+ // into a toolResult error whose content text carries the diagnostic
663
+ // (e.g. "ENOENT: no such file or directory..."). Recover that text so
664
+ // the adjacent evidence failure outranks a settled decision feedback
665
+ // with its original diagnostic, mirroring the pre-migration wrapped
666
+ // execute capture (which held the raw error object).
667
+ const detailText = extractToolResultText(event.details);
668
+ // A native unknown-tool receipt ("Tool <name> not found") is not an
669
+ // evidence-tool failure: the child session has no such tool registered,
670
+ // so its errored toolResult must not short-circuit the auditor (it keeps
671
+ // prompting to the turn limit, mirroring the pre-migration
672
+ // registeredToolNames exclusion in findToolFailure).
673
+ if (detailText !== undefined && /^Tool\s+.+ not found$/.test(detailText.trim())) return;
674
+ const failure = detailText === undefined
675
+ ? new Error(event.toolName ?? "evidence tool failed")
676
+ : new Error(detailText);
677
+ // Recover the errno code from the flattened diagnostic so an evidence
678
+ // failure's identity (e.g. code "ENOENT") is preserved across the real
679
+ // HTTP round-trip, mirroring the pre-migration raw error object.
680
+ const errno = /^([A-Z_]+):/.exec(detailText ?? "");
681
+ if (errno !== null && errno[1] !== undefined) (failure as NodeJS.ErrnoException).code = errno[1];
682
+ evidenceToolFailures.set(event.toolCallId, failure);
683
+ }
684
+ if (event.type === "message_end" && event.role === "assistant" && boundaryResponse === undefined) {
931
685
  turns += 1;
932
- addUsage(sessionUsage, event.message.usage);
933
- retainedResponse = event.message;
934
- try { options.retainResponse?.(event.message); } catch (error) { retentionFailure = error; }
935
- // A tool call in assistant output is only an observation. Preserve its
936
- // candidate for typed malformed-decision settlement, but the wrapped
937
- // execute path above is the sole owner of accepted-receipt state; a
938
- // rejected execution must remain retryable in this same session.
939
- for (const part of event.message.content) {
940
- if (part.type === "toolCall" && part.name === tool.name) {
941
- rejectedDecisionResponse = event.message;
942
- if (decision === undefined) {
943
- decision = part.arguments;
944
- decisionCallId = part.id;
945
- // Pi can reject malformed root arguments before invoking execute;
946
- // that remains the existing unreadable-candidate failure path.
947
- if (part.arguments === undefined) decisionSubmitted = true;
686
+ if (event.usage) addUsage(sessionUsage, event.usage);
687
+ const msg = event.message as AssistantMessage | undefined;
688
+ retainedResponse = msg;
689
+ if (msg) {
690
+ retainedAssistants.push(msg);
691
+ try { options.retainResponse?.(msg); } catch (error) { retentionFailure = error; }
692
+ // A tool call in assistant output is only an observation. Preserve its
693
+ // candidate for typed malformed-decision settlement, but the wrapped
694
+ // execute path above is the sole owner of accepted-receipt state; a
695
+ // rejected execution must remain retryable in this same session.
696
+ for (const part of msg.content) {
697
+ if (part.type === "toolCall" && part.name === tool.name) {
698
+ rejectedDecisionResponse = msg;
699
+ if (decision === undefined) {
700
+ decision = (part.arguments === undefined
701
+ || (typeof part.arguments === "object" && part.arguments !== null
702
+ && !Array.isArray(part.arguments) && Object.keys(part.arguments).length === 0))
703
+ ? undefined
704
+ : part.arguments;
705
+ decisionCallId = part.id;
706
+ // Pi can reject malformed root arguments before invoking execute;
707
+ // that remains the existing unreadable-candidate failure path.
708
+ // A missing root argument reaches the real provider adapter as an
709
+ // empty object after serialization — treat it as missing too so a
710
+ // one-shot typed missing-args settlement does not solicit another turn.
711
+ if (part.arguments === undefined
712
+ || (typeof part.arguments === "object" && part.arguments !== null
713
+ && !Array.isArray(part.arguments) && Object.keys(part.arguments).length === 0)) {
714
+ decisionSubmitted = true;
715
+ }
716
+ }
948
717
  }
949
718
  }
719
+ if (turns >= AUDITOR_TURN_LIMIT || msg.stopReason === "error") boundaryResponse = msg;
950
720
  }
951
- if (turns >= AUDITOR_TURN_LIMIT) boundaryResponse = event.message;
952
721
  }
953
722
  if (event.type === "turn_end") {
954
723
  if (rejectedDecisionResponse !== undefined) {
@@ -958,14 +727,15 @@ export async function executeAuditorChild(
958
727
  if (decisionSubmitted || promptNeighboringFailure !== undefined
959
728
  || (boundaryResponse !== undefined && rejectedDecisionResponse === undefined)
960
729
  || retentionFailure !== undefined) {
961
- void session.abort();
730
+ handle.abort();
962
731
  }
963
732
  }
964
733
  });
965
- const abort = () => { void session.abort(); };
734
+ const abort = () => { handle.abort(); };
966
735
  if (options.signal?.aborted) abort();
967
736
  else options.signal?.addEventListener("abort", abort, { once: true });
968
737
 
738
+ let auditorFailure: unknown;
969
739
  try {
970
740
  try {
971
741
  const promptAllowingRejectedDecision = async (prompt: string) => {
@@ -975,7 +745,7 @@ export async function executeAuditorChild(
975
745
  promptDecisionFailures = [];
976
746
  let promptFailure: unknown;
977
747
  try {
978
- await session.prompt(prompt);
748
+ await handle.prompt(prompt);
979
749
  } catch (error) {
980
750
  promptFailure = error;
981
751
  }
@@ -997,6 +767,8 @@ export async function executeAuditorChild(
997
767
  return;
998
768
  }
999
769
  if (decisionToolFailure !== undefined) return;
770
+ if (retentionFailure !== undefined) return;
771
+ if (opened.streamFailure !== undefined) throw opened.streamFailure;
1000
772
  if (promptFailure !== undefined) throw promptFailure;
1001
773
  };
1002
774
  const chargeAndClearRejectedDecisionFailures = (failures: unknown[]) => {
@@ -1007,8 +779,8 @@ export async function executeAuditorChild(
1007
779
  promptDecisionFailures = [];
1008
780
  };
1009
781
  await promptAllowingRejectedDecision(options.prompt);
1010
- while (!decisionSubmitted && (boundaryResponse === undefined || decisionToolFailure !== undefined)
1011
- && inherited.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
782
+ while (!decisionSubmitted && retentionFailure === undefined && (boundaryResponse === undefined || decisionToolFailure !== undefined)
783
+ && opened.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
1012
784
  if (decisionToolFailure !== undefined) {
1013
785
  const failures = promptDecisionFailures.length === 0
1014
786
  ? [decisionToolFailure]
@@ -1028,7 +800,7 @@ export async function executeAuditorChild(
1028
800
  await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
1029
801
  }
1030
802
  }
1031
- if (!decisionSubmitted && inherited.streamFailure === undefined
803
+ if (!decisionSubmitted && retentionFailure === undefined && opened.streamFailure === undefined
1032
804
  && delivery.nextAction() === "no-receipt") {
1033
805
  const runPointer = options.context.sessionManager.getSessionFile() ?? options.context.cwd ?? process.cwd();
1034
806
  const attemptPointer = binding.parent.attemptEntryId ?? binding.parent.sessionId ?? `current:${runPointer}`;
@@ -1038,28 +810,44 @@ export async function executeAuditorChild(
1038
810
  // charged this prompt to the exhausted shared budget.
1039
811
  decisionToolFailure = undefined;
1040
812
  auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
813
+ try {
814
+ sitianReport({
815
+ level: "event",
816
+ kind: "auditor",
817
+ cwd,
818
+ sessionParent: parentSessionFile,
819
+ payload: { type: NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, ...facts },
820
+ source: "evidence-child-executor",
821
+ });
822
+ } catch {}
1041
823
  // Provenance is granted only after the lifecycle owner persisted the
1042
824
  // current child record; accepted model arguments can never set it.
1043
825
  noReceiptLifecycle = facts;
1044
826
  }
1045
827
  } catch (error) {
1046
828
  if (options.signal?.aborted) throw options.signal.reason;
1047
- if (inherited.streamFailure !== undefined) throw inherited.streamFailure;
1048
- throw error;
829
+ if (retentionFailure === undefined && opened.streamFailure !== undefined) throw opened.streamFailure;
830
+ if (retentionFailure === undefined) throw error;
1049
831
  }
1050
832
  if (options.signal?.aborted) throw options.signal.reason;
1051
- if (inherited.streamFailure !== undefined) throw inherited.streamFailure;
833
+ if (retentionFailure === undefined && opened.streamFailure !== undefined) throw opened.streamFailure;
1052
834
  if (!decisionSubmitted && decisionToolFailure !== undefined) throw decisionToolFailure;
1053
835
  const relevantResponse = !decisionSubmitted
1054
836
  ? boundaryResponse
1055
- : [...session.messages].reverse().find((message): message is AssistantMessage =>
1056
- message.role === "assistant" && message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
837
+ : (retainedResponse && retainedResponse.role === "assistant" && retainedResponse.content.some((part) => part.type === "toolCall" && part.name === tool.name)
838
+ ? retainedResponse
839
+ : undefined);
1057
840
  if (relevantResponse !== undefined) {
1058
841
  const toolFailure = findToolFailure(relevantResponse);
1059
842
  if (toolFailure !== undefined) throw toolFailure;
1060
843
  }
1061
- if (retentionFailure !== undefined && retainedResponse?.stopReason !== "error") throw retentionFailure;
1062
- if (boundaryResponse !== undefined && !decisionSubmitted && noReceiptLifecycle === undefined) {
844
+ const assistants = [...retainedAssistants].reverse();
845
+ const response = !decisionSubmitted
846
+ ? assistants[0]
847
+ : assistants.find((message) =>
848
+ message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
849
+
850
+ if (boundaryResponse !== undefined && boundaryResponse.stopReason !== "error" && !decisionSubmitted && noReceiptLifecycle === undefined) {
1063
851
  const toolNames = boundaryResponse.content.flatMap((part) => part.type === "toolCall" ? [part.name] : []);
1064
852
  throw new AuditorTurnLimitError(AUDITOR_TURN_LIMIT, turns, {
1065
853
  stopReason: boundaryResponse.stopReason,
@@ -1067,18 +855,10 @@ export async function executeAuditorChild(
1067
855
  });
1068
856
  }
1069
857
 
1070
- const assistants = [...session.messages]
1071
- .reverse()
1072
- .filter((message): message is AssistantMessage => message.role === "assistant");
1073
- const response = !decisionSubmitted
1074
- ? assistants[0]
1075
- : assistants.find((message) =>
1076
- message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
1077
-
1078
858
  if (response !== undefined) {
1079
859
  try {
860
+ if (retentionFailure !== undefined) throw retentionFailure;
1080
861
  if (retainedResponse === undefined) options.retainResponse?.(response);
1081
- else if (retentionFailure !== undefined) throw retentionFailure;
1082
862
  } catch (retentionFailure) {
1083
863
  if (response.stopReason !== "error") throw retentionFailure;
1084
864
  // Do not trim/rewrite the held errorMessage bytes.
@@ -1133,8 +913,8 @@ export async function executeAuditorChild(
1133
913
  }),
1134
914
  },
1135
915
  };
1136
- auditorSessionManager.appendCustomEntry(AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, {
1137
- version: 1,
916
+ const failureData = {
917
+ version: 1 as const,
1138
918
  parent: binding.parent,
1139
919
  failure: {
1140
920
  cause: failure.knownCause,
@@ -1142,7 +922,18 @@ export async function executeAuditorChild(
1142
922
  ...(failure.message === "" ? {} : { diagnostic: failure.message }),
1143
923
  details: failure.details,
1144
924
  },
1145
- });
925
+ };
926
+ auditorSessionManager.appendCustomEntry(AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, failureData);
927
+ try {
928
+ sitianReport({
929
+ level: "event",
930
+ kind: "auditor",
931
+ cwd,
932
+ sessionParent: parentSessionFile,
933
+ payload: { type: AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, ...failureData },
934
+ source: "evidence-child-executor",
935
+ });
936
+ } catch {}
1146
937
  throw failure;
1147
938
  }
1148
939
  }
@@ -1150,20 +941,21 @@ export async function executeAuditorChild(
1150
941
  if (
1151
942
  response === undefined
1152
943
  || response.stopReason === "error"
1153
- || response.stopReason === "aborted"
1154
- || (!decisionSubmitted && decision === undefined)
944
+ || (!decisionSubmitted && (response.stopReason === "aborted" || decision === undefined))
1155
945
  ) {
1156
- throw new Error(`${options.roleLabel} exited without a readable decision receipt`);
946
+ throw new Error(response?.errorMessage ?? `${options.roleLabel} exited without a readable decision receipt`);
1157
947
  }
1158
948
  return {
1159
949
  decision,
1160
950
  response: { ...response, usage: sessionUsage },
1161
951
  ...(noReceiptLifecycle === undefined ? {} : { noReceiptLifecycle }),
1162
952
  };
953
+ } catch (error) {
954
+ auditorFailure = error;
955
+ throw error;
1163
956
  } finally {
1164
957
  options.signal?.removeEventListener("abort", abort);
1165
- unsubscribe();
1166
- dispose();
958
+ await runChildCleanup([() => unsubscribe(), () => handle.close()], auditorFailure, options.roleLabel);
1167
959
  }
1168
960
  });
1169
961
  }