pi-daddy 0.43.1 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +93 -31
  2. package/README.md +15 -0
  3. package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
  4. package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
  5. package/THIRD_PARTY_NOTICES.md +9 -0
  6. package/dist/cli.d.ts +2 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +10 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/executors/activity-session.js +116 -65
  11. package/dist/executors/activity-session.js.map +1 -1
  12. package/dist/executors/captured-execution.d.ts +13 -0
  13. package/dist/executors/captured-execution.d.ts.map +1 -0
  14. package/dist/executors/captured-execution.js +53 -0
  15. package/dist/executors/captured-execution.js.map +1 -0
  16. package/dist/executors/child-final.d.ts +31 -0
  17. package/dist/executors/child-final.d.ts.map +1 -0
  18. package/dist/executors/child-final.js +197 -0
  19. package/dist/executors/child-final.js.map +1 -0
  20. package/dist/executors/executor.d.ts +2 -1
  21. package/dist/executors/executor.d.ts.map +1 -1
  22. package/dist/executors/executor.js +12 -0
  23. package/dist/executors/executor.js.map +1 -1
  24. package/dist/executors/owned-worker.d.ts +18 -0
  25. package/dist/executors/owned-worker.d.ts.map +1 -0
  26. package/dist/executors/owned-worker.js +295 -0
  27. package/dist/executors/owned-worker.js.map +1 -0
  28. package/dist/executors/worker-artifact.d.ts +3 -0
  29. package/dist/executors/worker-artifact.d.ts.map +1 -0
  30. package/dist/executors/worker-artifact.js +3 -0
  31. package/dist/executors/worker-artifact.js.map +1 -0
  32. package/dist/governance/approval-prompt.d.ts +4 -3
  33. package/dist/governance/approval-prompt.d.ts.map +1 -1
  34. package/dist/governance/approval-prompt.js +13 -6
  35. package/dist/governance/approval-prompt.js.map +1 -1
  36. package/dist/governance/captured-worker-record.d.ts +9 -0
  37. package/dist/governance/captured-worker-record.d.ts.map +1 -0
  38. package/dist/governance/captured-worker-record.js +123 -0
  39. package/dist/governance/captured-worker-record.js.map +1 -0
  40. package/dist/governance/lease-record.d.ts +4 -0
  41. package/dist/governance/lease-record.d.ts.map +1 -1
  42. package/dist/governance/lease-record.js +19 -2
  43. package/dist/governance/lease-record.js.map +1 -1
  44. package/dist/governance/ledger-events.d.ts +5 -5
  45. package/dist/governance/ledger-events.d.ts.map +1 -1
  46. package/dist/governance/ledger-v3-validation.js +1 -1
  47. package/dist/governance/ledger-v3-validation.js.map +1 -1
  48. package/dist/governance/workspace-acceptance.d.ts +3 -1
  49. package/dist/governance/workspace-acceptance.d.ts.map +1 -1
  50. package/dist/governance/workspace-acceptance.js +21 -5
  51. package/dist/governance/workspace-acceptance.js.map +1 -1
  52. package/dist/governance/workspace-lease.d.ts.map +1 -1
  53. package/dist/governance/workspace-lease.js +44 -1
  54. package/dist/governance/workspace-lease.js.map +1 -1
  55. package/dist/kernel/authority-text.d.ts +3 -0
  56. package/dist/kernel/authority-text.d.ts.map +1 -0
  57. package/dist/kernel/authority-text.js +22 -0
  58. package/dist/kernel/authority-text.js.map +1 -0
  59. package/dist/kernel/bounded-read-failures.d.ts +9 -0
  60. package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
  61. package/dist/kernel/bounded-read-failures.js +33 -0
  62. package/dist/kernel/bounded-read-failures.js.map +1 -0
  63. package/dist/kernel/bounded-read-sync.d.ts +24 -0
  64. package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
  65. package/dist/kernel/bounded-read-sync.js +75 -0
  66. package/dist/kernel/bounded-read-sync.js.map +1 -0
  67. package/dist/kernel/bounded-read.d.ts +29 -2
  68. package/dist/kernel/bounded-read.d.ts.map +1 -1
  69. package/dist/kernel/bounded-read.js +98 -9
  70. package/dist/kernel/bounded-read.js.map +1 -1
  71. package/dist/kernel/capacity.d.ts +36 -0
  72. package/dist/kernel/capacity.d.ts.map +1 -0
  73. package/dist/kernel/capacity.js +94 -0
  74. package/dist/kernel/capacity.js.map +1 -0
  75. package/dist/kernel/captured-worker-contract.d.ts +38 -0
  76. package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
  77. package/dist/kernel/captured-worker-contract.js +2 -0
  78. package/dist/kernel/captured-worker-contract.js.map +1 -0
  79. package/dist/kernel/catalog.d.ts +12 -3
  80. package/dist/kernel/catalog.d.ts.map +1 -1
  81. package/dist/kernel/catalog.js +28 -8
  82. package/dist/kernel/catalog.js.map +1 -1
  83. package/dist/kernel/chain.d.ts +1 -0
  84. package/dist/kernel/chain.d.ts.map +1 -1
  85. package/dist/kernel/chain.js.map +1 -1
  86. package/dist/kernel/definitions.d.ts +11 -1
  87. package/dist/kernel/definitions.d.ts.map +1 -1
  88. package/dist/kernel/definitions.js +6 -3
  89. package/dist/kernel/definitions.js.map +1 -1
  90. package/dist/kernel/fanout.d.ts +5 -25
  91. package/dist/kernel/fanout.d.ts.map +1 -1
  92. package/dist/kernel/fanout.js +15 -30
  93. package/dist/kernel/fanout.js.map +1 -1
  94. package/dist/kernel/grant-env.d.ts +0 -2
  95. package/dist/kernel/grant-env.d.ts.map +1 -1
  96. package/dist/kernel/grant-env.js +0 -1
  97. package/dist/kernel/grant-env.js.map +1 -1
  98. package/dist/kernel/model-preflight.d.ts +8 -0
  99. package/dist/kernel/model-preflight.d.ts.map +1 -1
  100. package/dist/kernel/model-preflight.js +23 -0
  101. package/dist/kernel/model-preflight.js.map +1 -1
  102. package/dist/kernel/selected-definitions.d.ts +15 -0
  103. package/dist/kernel/selected-definitions.d.ts.map +1 -0
  104. package/dist/kernel/selected-definitions.js +146 -0
  105. package/dist/kernel/selected-definitions.js.map +1 -0
  106. package/dist/kernel/skill-packages.d.ts +4 -2
  107. package/dist/kernel/skill-packages.d.ts.map +1 -1
  108. package/dist/kernel/skill-packages.js +34 -41
  109. package/dist/kernel/skill-packages.js.map +1 -1
  110. package/dist/kernel/skill-resources.d.ts.map +1 -1
  111. package/dist/kernel/skill-resources.js +8 -11
  112. package/dist/kernel/skill-resources.js.map +1 -1
  113. package/dist/kernel/workspace.d.ts +6 -3
  114. package/dist/kernel/workspace.d.ts.map +1 -1
  115. package/dist/kernel/workspace.js +10 -6
  116. package/dist/kernel/workspace.js.map +1 -1
  117. package/dist/products/dashboard-cli.js +1 -1
  118. package/dist/products/dashboard-cli.js.map +1 -1
  119. package/dist/products/dashboard-render.d.ts +1 -2
  120. package/dist/products/dashboard-render.d.ts.map +1 -1
  121. package/dist/products/dashboard-render.js +3 -1
  122. package/dist/products/dashboard-render.js.map +1 -1
  123. package/dist/products/dashboard-session-client.d.ts +1 -2
  124. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  125. package/dist/products/dashboard-session-client.js.map +1 -1
  126. package/dist/products/episode-report.d.ts +12 -8
  127. package/dist/products/episode-report.d.ts.map +1 -1
  128. package/dist/products/episode-report.js +61 -12
  129. package/dist/products/episode-report.js.map +1 -1
  130. package/extensions/approvals.ts +1 -1
  131. package/extensions/chain-plan.ts +9 -3
  132. package/extensions/context-staging.ts +30 -35
  133. package/extensions/dashboard-session-server.ts +1 -2
  134. package/extensions/definition-describe.ts +44 -0
  135. package/extensions/definition-runtime.ts +89 -26
  136. package/extensions/delegate-chain.ts +273 -256
  137. package/extensions/delegation-activation.ts +1 -1
  138. package/extensions/delegation.ts +41 -53
  139. package/extensions/execute-child.ts +167 -134
  140. package/extensions/fanout-outcome.ts +1 -9
  141. package/extensions/grants-command.ts +2 -9
  142. package/extensions/grants.ts +106 -38
  143. package/extensions/init-command.ts +52 -12
  144. package/extensions/reload-environment.ts +21 -2
  145. package/extensions/retired-inputs.ts +7 -0
  146. package/extensions/run-delegation.ts +212 -265
  147. package/extensions/session-capacity.ts +53 -0
  148. package/extensions/session-environment.ts +5 -2
  149. package/extensions/session-model-prompt.ts +33 -32
  150. package/extensions/session-report.ts +28 -11
  151. package/extensions/session.ts +200 -157
  152. package/extensions/workspace-runtime.ts +2 -1
  153. package/native/README.md +25 -0
  154. package/native/linux-x64/worker +0 -0
  155. package/native/linux-x64/worker.sha256 +1 -0
  156. package/native/provenance.json +42 -0
  157. package/native/toolchain.json +18 -0
  158. package/native/worker.c +209 -0
  159. package/package.json +15 -7
  160. package/src/cli.ts +16 -6
  161. package/src/executors/activity-session.ts +120 -66
  162. package/src/executors/captured-execution.ts +61 -0
  163. package/src/executors/child-final.ts +190 -0
  164. package/src/executors/executor.ts +14 -1
  165. package/src/executors/owned-worker.ts +322 -0
  166. package/src/executors/worker-artifact.ts +2 -0
  167. package/src/governance/approval-prompt.ts +18 -8
  168. package/src/governance/captured-worker-record.ts +138 -0
  169. package/src/governance/lease-record.ts +21 -2
  170. package/src/governance/ledger-events.ts +5 -5
  171. package/src/governance/ledger-v3-validation.ts +1 -1
  172. package/src/governance/workspace-acceptance.ts +20 -4
  173. package/src/governance/workspace-lease.ts +59 -1
  174. package/src/kernel/authority-text.ts +17 -0
  175. package/src/kernel/bounded-read-failures.ts +42 -0
  176. package/src/kernel/bounded-read-sync.ts +87 -0
  177. package/src/kernel/bounded-read.ts +128 -11
  178. package/src/kernel/capacity.ts +123 -0
  179. package/src/kernel/captured-worker-contract.ts +29 -0
  180. package/src/kernel/catalog.ts +45 -16
  181. package/src/kernel/chain.ts +1 -0
  182. package/src/kernel/definitions.ts +11 -2
  183. package/src/kernel/fanout.ts +17 -32
  184. package/src/kernel/grant-env.ts +0 -3
  185. package/src/kernel/model-preflight.ts +29 -0
  186. package/src/kernel/selected-definitions.ts +156 -0
  187. package/src/kernel/skill-packages.ts +40 -38
  188. package/src/kernel/skill-resources.ts +8 -9
  189. package/src/kernel/workspace.ts +12 -5
  190. package/src/products/dashboard-cli.ts +1 -1
  191. package/src/products/dashboard-render.ts +6 -2
  192. package/src/products/dashboard-session-client.ts +1 -2
  193. package/src/products/episode-report.ts +80 -17
  194. package/dist/advisors/advisor.d.ts +0 -90
  195. package/dist/advisors/advisor.d.ts.map +0 -1
  196. package/dist/advisors/advisor.js +0 -154
  197. package/dist/advisors/advisor.js.map +0 -1
  198. package/dist/advisors/decider.d.ts +0 -88
  199. package/dist/advisors/decider.d.ts.map +0 -1
  200. package/dist/advisors/decider.js +0 -29
  201. package/dist/advisors/decider.js.map +0 -1
  202. package/dist/advisors/jev.d.ts +0 -41
  203. package/dist/advisors/jev.d.ts.map +0 -1
  204. package/dist/advisors/jev.js +0 -139
  205. package/dist/advisors/jev.js.map +0 -1
  206. package/dist/advisors/settings.d.ts +0 -49
  207. package/dist/advisors/settings.d.ts.map +0 -1
  208. package/dist/advisors/settings.js +0 -99
  209. package/dist/advisors/settings.js.map +0 -1
  210. package/dist/governance/episode-cost-gate.d.ts +0 -31
  211. package/dist/governance/episode-cost-gate.d.ts.map +0 -1
  212. package/dist/governance/episode-cost-gate.js +0 -112
  213. package/dist/governance/episode-cost-gate.js.map +0 -1
  214. package/extensions/advisor-session.ts +0 -67
  215. package/extensions/effort-advice.ts +0 -112
  216. package/extensions/pruning-advice.ts +0 -133
  217. package/src/advisors/advisor.ts +0 -246
  218. package/src/advisors/decider.ts +0 -74
  219. package/src/advisors/jev.ts +0 -163
  220. package/src/advisors/settings.ts +0 -115
  221. package/src/governance/episode-cost-gate.ts +0 -128
@@ -1,10 +1,17 @@
1
+ import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
2
+ import { retainDiscoveryCleanupFailure } from "./session.ts";
3
+ import type { CapacityReservation } from "../src/kernel/capacity.ts";
4
+ import { HERDR_UNQUALIFIED_REASON } from "../src/executors/executor.ts";
5
+ import { runCapturedExecution } from "../src/executors/captured-execution.ts";
6
+ import type { ChildFinal } from "../src/executors/child-final.ts";
7
+ import type { CapturedWorkerCleanup } from "../src/kernel/captured-worker-contract.ts";
1
8
  import type { Delegation } from "../src/kernel/delegate.ts";
2
9
  import {
3
10
  beginExecutionRetention,
4
11
  retentionConfigurationDigest,
5
12
  type RetentionStatus,
6
13
  } from "../src/governance/execution-retention.ts";
7
- import { appendLedgerEvent, buildChildLifecycleEvent, buildEpisodeCostGateEvent } from "../src/governance/ledger.ts";
14
+ import { appendLedgerEvent, buildChildLifecycleEvent } from "../src/governance/ledger.ts";
8
15
  import { hasFinalizerError } from "../src/governance/finalization.ts";
9
16
  import { mergeChildEnv } from "../src/kernel/propagation.ts";
10
17
  import type { Capability } from "../src/kernel/resolve.ts";
@@ -25,7 +32,6 @@ import { ENV_HERDR_KEEP_PANE, type GrantsSession } from "./session.ts";
25
32
  import { CHILD_ATTRIBUTION_ENV_KEYS, ENV_EPISODE_ID } from "../src/kernel/env-names.ts";
26
33
  import { releaseDelegationWorkspace, type PreparedWorkspace } from "./workspace-runtime.ts";
27
34
  import { ActivityTimelineRecorder, ENV_ACTIVITY_PARENT_TASK } from "../src/products/activity-timeline.ts";
28
- import { loadEpisodeCosts } from "../src/governance/episode-cost-gate.ts";
29
35
  import { resolvedModelOf, type ResolvedDefinitionRuntime } from "./definition-runtime.ts";
30
36
  export interface DelegationOutcome {
31
37
  ok: boolean;
@@ -43,6 +49,12 @@ export interface DelegationOutcome {
43
49
  retention?: RetentionStatus;
44
50
  /** A loud best-effort post-execution observation failure; execution ownership is still settled. */
45
51
  control?: "failed";
52
+ work?: "succeeded" | "failed" | "unknown";
53
+ final?: ChildFinal;
54
+ cleanup?: CapturedWorkerCleanup;
55
+ observation?: { state: "complete" | "incomplete"; reasons: string[] };
56
+ diagnostics?: string;
57
+ diagnosticsTruncated?: boolean;
46
58
  }
47
59
  /**
48
60
  * The upstream controller's token, honoured ONLY when the child otherwise exited cleanly non-zero.
@@ -54,14 +66,26 @@ export interface DelegationOutcome {
54
66
  * by anybody's gate, so the token cannot be taken at its word there.
55
67
  */
56
68
  export function isCriticalAssuranceBlock(
57
- outcome: Pick<DelegationOutcome, "ok" | "text" | "timedOut" | "aborted" | "truncated" | "spawnFailed">,
69
+ outcome: Pick<
70
+ DelegationOutcome,
71
+ "ok" | "text" | "exitCode" | "timedOut" | "aborted" | "truncated" | "spawnFailed" | "control" | "cleanup"
72
+ >,
58
73
  ): boolean {
59
74
  // `truncated` is deliberately NOT here. The process executor keeps the HEAD of the output
60
75
  // (`run-child.ts` slices to the cap and stops appending) and the token is matched at byte 0, so a
61
76
  // genuine veto with a rationale over the output cap is still a genuine veto — rejecting it broke the
62
77
  // pass-through ADR-0034 pins, in the fix that was supposed to protect it. What must be rejected is a
63
78
  // child that never finished speaking: killed, cancelled, or never started.
64
- if (outcome.ok || outcome.timedOut || outcome.aborted || outcome.spawnFailed) return false;
79
+ if (
80
+ outcome.ok ||
81
+ outcome.timedOut ||
82
+ outcome.aborted ||
83
+ outcome.spawnFailed ||
84
+ outcome.control === "failed" ||
85
+ outcome.cleanup?.state === "unknown" ||
86
+ !(outcome.exitCode !== null && outcome.exitCode > 0)
87
+ )
88
+ return false;
65
89
  return outcome.text.trimStart().startsWith("BLOCKED_CRITICAL_ASSURANCE");
66
90
  }
67
91
 
@@ -97,8 +121,9 @@ export interface ChildProgressUpdate {
97
121
  * has already run, so failing closed there prevents nothing and used to discard completed work while
98
122
  * blaming "ledger" (R-99). The failure is reported alongside the outcome instead of replacing it.
99
123
  */
100
- export async function executePlannedChild(input: {
124
+ export interface ChildExecutionInput {
101
125
  session: GrantsSession;
126
+ capacityReservation?: CapacityReservation;
102
127
  plan: Delegation;
103
128
  agent?: string;
104
129
  childId: string;
@@ -110,14 +135,56 @@ export async function executePlannedChild(input: {
110
135
  resolvedRuntime?: ResolvedDefinitionRuntime;
111
136
  signal?: AbortSignal;
112
137
  onProgress?: (update: ChildProgressUpdate) => void;
113
- costGateUI?: {
114
- hasUI: boolean;
115
- input(title: string, placeholder: string, signal?: AbortSignal): Promise<string | undefined>;
116
- notify(message: string): void;
117
- };
118
- }): Promise<DelegationOutcome> {
138
+ }
139
+ export async function executePlannedChild(input: ChildExecutionInput): Promise<DelegationOutcome> {
140
+ let executorEntered = false;
141
+ try {
142
+ return await executePreparedChild(input, () => {
143
+ executorEntered = true;
144
+ });
145
+ } catch (error) {
146
+ if (executorEntered) throw error;
147
+ // Every operation above the executor-entry marker is local setup: no helper or worker exists yet.
148
+ input.capacityReservation?.finalize({ state: "not-started", reason: "child setup failed before executor entry" });
149
+ const failures: unknown[] = [error];
150
+ try {
151
+ const released = await releaseDelegationWorkspace({
152
+ prepared: input.preparedWorkspace,
153
+ childId: input.childId,
154
+ episodeId: input.session.episodeId,
155
+ executionId: input.executionId,
156
+ parentExecutionId: input.parentExecutionId,
157
+ ledgerPath: input.session.ledgerPath,
158
+ reason: "setup-failed",
159
+ });
160
+ if (released === "lost" || released === "retained")
161
+ failures.push(new Error(`workspace setup release is ${released}`));
162
+ } catch (releaseError) {
163
+ failures.push(releaseError);
164
+ }
165
+ try {
166
+ input.plan.disposeHandoff?.();
167
+ } catch (disposeError) {
168
+ failures.push(disposeError);
169
+ }
170
+ if (failures.length > 1) throw new AggregateError(failures, failures.map(String).join("; "));
171
+ throw error;
172
+ }
173
+ }
174
+ async function executePreparedChild(
175
+ input: ChildExecutionInput,
176
+ onExecutorEntry: () => void,
177
+ ): Promise<DelegationOutcome> {
119
178
  const { session, plan, childId, executionId, parentExecutionId, preparedWorkspace, signal, onProgress } = input;
120
179
  const ledgerPath = session.ledgerPath;
180
+ let executorEntered = false;
181
+ const teardownFailures: string[] = [];
182
+ const controlFailures: string[] = [];
183
+ const lifecycle = session.reloadLifecycle;
184
+ const onReadCleanup = (error: BoundedReadCleanupError): void => {
185
+ if (lifecycle) retainDiscoveryCleanupFailure(session, error, lifecycle);
186
+ controlFailures.push(`session read cleanup unresolved: ${error.message}`);
187
+ };
121
188
  const runtimeAttribution = (observed?: {
122
189
  resolvedModel?: { provider: string; modelId: string };
123
190
  thinking?: string;
@@ -177,95 +244,25 @@ export async function executePlannedChild(input: {
177
244
  }
178
245
  }
179
246
 
247
+ async function observeUsage(): Promise<import("../src/executors/activity-session.ts").ChildUsageObservation> {
248
+ try {
249
+ return await activitySession.usage();
250
+ } catch (error) {
251
+ if (error instanceof BoundedReadCleanupError) onReadCleanup(error);
252
+ teardownFailures.push(`usage observation unavailable: ${String(error)}`);
253
+ return { unavailable: "session-invalid" };
254
+ }
255
+ }
180
256
  async function executeWithActivitySession(): Promise<DelegationOutcome> {
181
257
  const args = activitySession.args;
182
258
  // The process executor adds a second signal that is live during a tool call: the child tree's CPU time and
183
259
  // descendants (`process-activity.ts`). Herdr children are started by the daemon, so their pid is not known here.
184
260
  let childPid: number | undefined;
185
- let costStopped = false;
186
- const costUI = input.costGateUI ?? {
187
- hasUI: false,
188
- input: async () => undefined,
189
- notify: () => undefined,
190
- };
191
- const costAbort = new AbortController();
192
- const observeCost = async (cost: number | undefined, terminal: boolean) => {
193
- if (!session.episodeCostGate) return;
194
- if (ledgerPath) {
195
- const recorded = await loadEpisodeCosts(ledgerPath, session.episodeId);
196
- for (const [recordedExecution, recordedCost] of recorded) {
197
- session.episodeCostGate.seedCost(recordedExecution, recordedCost);
198
- }
199
- }
200
- await session.episodeCostGate.observe(executionId, cost, terminal, {
201
- warn: (message) => costUI.notify(message),
202
- pause: async () => {
203
- if (session.executor.kind === "herdr") {
204
- costUI.notify("pi-daddy: Herdr cannot pause a pane; stopping it at the episode cost ceiling");
205
- return;
206
- }
207
- if (childPid !== undefined)
208
- try {
209
- process.kill(childPid, "SIGSTOP");
210
- } catch {
211
- /* the child may have exited between its final usage write and this observation */
212
- }
213
- },
214
- ask: async () => {
215
- if (session.executor.kind === "herdr" || !costUI.hasUI) return null;
216
- const value = await costUI.input(
217
- "pi-daddy: episode cost ceiling crossed",
218
- "New USD ceiling above the current cost; leave blank to stop",
219
- signal,
220
- );
221
- if (value === undefined || value.trim() === "") return null;
222
- const parsed = Number(value);
223
- return Number.isFinite(parsed) ? parsed : null;
224
- },
225
- resume: async () => {
226
- if (childPid !== undefined)
227
- try {
228
- process.kill(childPid, "SIGCONT");
229
- } catch {
230
- /* already exited */
231
- }
232
- },
233
- stop: async () => {
234
- costStopped = true;
235
- if (childPid !== undefined)
236
- try {
237
- process.kill(childPid, "SIGCONT");
238
- } catch {
239
- /* already exited */
240
- }
241
- costAbort.abort();
242
- },
243
- gate: async (event) => {
244
- if (!ledgerPath) return;
245
- await appendLedgerEvent(
246
- {
247
- path: ledgerPath,
248
- strict: true,
249
- },
250
- buildEpisodeCostGateEvent({
251
- episodeId: session.episodeId,
252
- executionId,
253
- parentExecutionId,
254
- childId,
255
- ...event,
256
- now: new Date(),
257
- }),
258
- );
259
- },
260
- });
261
- };
262
261
  const probe = async () => {
263
- const [file, tree, usage] = await Promise.all([
262
+ const [file, tree] = await Promise.all([
264
263
  activitySession.probe(),
265
264
  childPid === undefined ? undefined : processTreeActivity(childPid),
266
- activitySession.usage(),
267
265
  ]);
268
- await observeCost(usage.usage?.cost.total, false);
269
266
  return file === undefined && tree === undefined ? undefined : `${file ?? "-"}|${tree ?? "-"}`;
270
267
  };
271
268
  const startedAt = new Date();
@@ -273,7 +270,11 @@ export async function executePlannedChild(input: {
273
270
  if (ledgerPath) {
274
271
  try {
275
272
  await appendLedgerEvent(
276
- { path: ledgerPath, strict: true },
273
+ {
274
+ path: ledgerPath,
275
+ strict: false,
276
+ onFailure: (cause) => teardownFailures.push(`starting observation failed: ${String(cause)}`),
277
+ },
277
278
  buildChildLifecycleEvent({
278
279
  episodeId: session.episodeId,
279
280
  executionId,
@@ -292,14 +293,6 @@ export async function executePlannedChild(input: {
292
293
  }),
293
294
  );
294
295
  } catch (error) {
295
- await releaseDelegationWorkspace({
296
- prepared: preparedWorkspace,
297
- childId,
298
- episodeId: session.episodeId,
299
- executionId,
300
- parentExecutionId,
301
- reason: "ledger-failed",
302
- });
303
296
  throw error;
304
297
  }
305
298
  }
@@ -319,11 +312,7 @@ export async function executePlannedChild(input: {
319
312
  leaseLost = true;
320
313
  leaseAbort.abort();
321
314
  });
322
- const executionSignals = [
323
- costAbort.signal,
324
- ...(signal ? [signal] : []),
325
- ...(writerLease ? [leaseAbort.signal] : []),
326
- ];
315
+ const executionSignals = [...(signal ? [signal] : []), ...(writerLease ? [leaseAbort.signal] : [])];
327
316
  const executionSignal = AbortSignal.any(executionSignals);
328
317
  const retention = beginExecutionRetention({
329
318
  executionId,
@@ -345,7 +334,7 @@ export async function executePlannedChild(input: {
345
334
  let releaseReason = "failed";
346
335
  let retainWriterLease = false;
347
336
  let terminalAttempted = false;
348
- const teardownFailures: string[] = [];
337
+
349
338
  let runtimeRecord: Promise<void> | undefined;
350
339
  const recordRunning = (executor: "process" | "herdr", pane?: { id: string; agentName: string }): void => {
351
340
  if (!ledgerPath) return;
@@ -379,6 +368,10 @@ export async function executePlannedChild(input: {
379
368
  }
380
369
  };
381
370
  try {
371
+ if (String(session.executor.kind) === "herdr")
372
+ throw new GovernanceRefusal(refusal("EXECUTOR_UNAVAILABLE", HERDR_UNQUALIFIED_REASON));
373
+ executorEntered = true;
374
+ onExecutorEntry();
382
375
  const output =
383
376
  session.executor.kind === "herdr"
384
377
  ? await runHerdrPane({
@@ -410,9 +403,16 @@ export async function executePlannedChild(input: {
410
403
  onNativeTab: (tabId) => retention.native({ tabId }),
411
404
  onSnapshot: onProgress ? (snapshot) => onProgress({ snapshot }) : undefined,
412
405
  })
413
- : await runChild({
414
- command: writerLease ? "setpriv" : "pi",
415
- args: writerLease ? ["--pdeathsig", "KILL", "--", "pi", ...args] : args,
406
+ : await runCapturedExecution({
407
+ executionId,
408
+ sessionPath: activitySession.path,
409
+ onReadCleanup,
410
+ onOwnership: async (identity) => {
411
+ input.capacityReservation?.bindOwnership(identity);
412
+ await preparedWorkspace?.lease.attachCapturedWorker(identity);
413
+ },
414
+ command: "pi",
415
+ args,
416
416
  env: mergeChildEnv(process.env, plan.env),
417
417
  cwd,
418
418
  signal: executionSignal,
@@ -429,7 +429,7 @@ export async function executePlannedChild(input: {
429
429
  // Lease attachment is a security hook and may fail the spawn. The shared reporters isolate
430
430
  // display exceptions before they reach this callback; runChild deliberately kills on any error
431
431
  // here, so presentation must never be added directly without that reporter boundary.
432
- preparedWorkspace?.lease.attachProcess(pid);
432
+
433
433
  retention.native({ pid });
434
434
  recordRunning("process");
435
435
  onProgress?.({ state: "running" });
@@ -438,7 +438,18 @@ export async function executePlannedChild(input: {
438
438
 
439
439
  if (sessionFlag >= 0) retention.observeSession({ source: "pi-session-file", value: plan.args[sessionFlag + 1] });
440
440
  retention.capture("result", Buffer.from(output.text), true);
441
- const childFailed = Boolean(output.spawnError || output.aborted || output.timedOut || output.code !== 0);
441
+ if ("diagnosticsTruncated" in output && output.diagnosticsTruncated)
442
+ teardownFailures.push("diagnostic output truncated");
443
+ const final = "final" in output ? (output.final as ChildFinal) : undefined;
444
+ const cleanup = "cleanup" in output ? (output.cleanup as CapturedWorkerCleanup) : undefined;
445
+ input.capacityReservation?.finalize(
446
+ cleanup ?? { state: "unknown", reason: "executor supplied no qualified cleanup" },
447
+ );
448
+ const workFailed = Boolean(output.spawnError || output.aborted || output.timedOut || output.code !== 0);
449
+ const finalUnavailable = final?.state === "unavailable";
450
+ const cleanupUnknown = cleanup?.state === "unknown";
451
+ retainWriterLease = Boolean(writerLease && cleanupUnknown);
452
+ const childFailed = workFailed || finalUnavailable || cleanupUnknown;
442
453
  retention.finish({
443
454
  code: output.code,
444
455
  signal: output.signal ?? null,
@@ -447,8 +458,8 @@ export async function executePlannedChild(input: {
447
458
  truncated: output.truncated,
448
459
  failed: childFailed,
449
460
  });
450
- const usageObservation = await activitySession.usage();
451
- await observeCost(usageObservation.usage?.cost.total, usageObservation.usage === undefined);
461
+ const usageObservation = await observeUsage();
462
+
452
463
  releaseReason = output.timedOut ? "timeout" : output.aborted ? "cancelled" : childFailed ? "failed" : "completed";
453
464
  if (activityStarted)
454
465
  try {
@@ -522,16 +533,18 @@ export async function executePlannedChild(input: {
522
533
  : output.aborted
523
534
  ? leaseWasLost
524
535
  ? "lost the exclusive writer lease protecting its workspace and was stopped"
525
- : costStopped
526
- ? "crossed the episode cost ceiling and was stopped"
527
- : "was cancelled"
536
+ : "was cancelled"
528
537
  : output.timedOut
529
538
  ? output.idle
530
539
  ? `showed no activity for ${describeBound(configuredIdleMs)} and was killed ` +
531
540
  `(${ENV_CHILD_IDLE_TIMEOUT} sets the bound in seconds)`
532
541
  : `ran past its ${describeBound(configuredTimeoutMs)} ceiling and was killed ` +
533
542
  `(${ENV_CHILD_TIMEOUT} sets it in seconds)`
534
- : `exited with code ${output.code}`;
543
+ : finalUnavailable
544
+ ? `has no attributable complete final: ${final.reason}`
545
+ : cleanupUnknown
546
+ ? `has unverified subtree cleanup: ${cleanup.reason}`
547
+ : `exited with code ${output.code}`;
535
548
  // A stable code for every execution failure. Without these an external controller could tell a
536
549
  // policy refusal from an internal error, but not a lost writer lease from a user pressing stop
537
550
  // (R-103), and not a missing `setpriv` from an ordinary crash (R-107).
@@ -543,10 +556,18 @@ export async function executePlannedChild(input: {
543
556
  ? "CHILD_TIMED_OUT"
544
557
  : output.aborted
545
558
  ? "CHILD_CANCELLED"
546
- : "CHILD_EXIT_NONZERO";
559
+ : output.code === 0 && (finalUnavailable || cleanupUnknown)
560
+ ? "EXECUTOR_UNAVAILABLE"
561
+ : "CHILD_EXIT_NONZERO";
547
562
  const failed: DelegationOutcome = {
548
563
  ok: false,
549
- text: output.text.trim(),
564
+ work: workFailed ? "failed" : final?.state === "complete" ? "succeeded" : "unknown",
565
+ ...(final ? { final } : {}),
566
+ ...(cleanup ? { cleanup } : {}),
567
+ ...("diagnostics" in output ? { diagnostics: String(output.diagnostics) } : {}),
568
+ ...("diagnosticsTruncated" in output ? { diagnosticsTruncated: Boolean(output.diagnosticsTruncated) } : {}),
569
+ ...(cleanupUnknown ? { control: "failed" as const } : {}),
570
+ text: output.text,
550
571
  reason: `the sub-agent ${why}`,
551
572
  granted: plan.effective,
552
573
  depth: plan.childDepth,
@@ -563,7 +584,12 @@ export async function executePlannedChild(input: {
563
584
 
564
585
  const succeeded: DelegationOutcome = {
565
586
  ok: true,
566
- text: output.text.trim(),
587
+ work: "succeeded",
588
+ ...(final ? { final } : {}),
589
+ ...(cleanup ? { cleanup } : {}),
590
+ ...("diagnostics" in output ? { diagnostics: String(output.diagnostics) } : {}),
591
+ ...("diagnosticsTruncated" in output ? { diagnosticsTruncated: Boolean(output.diagnosticsTruncated) } : {}),
592
+ text: output.text,
567
593
  granted: plan.effective,
568
594
  depth: plan.childDepth,
569
595
  exitCode: output.code,
@@ -572,6 +598,7 @@ export async function executePlannedChild(input: {
572
598
  await teardown();
573
599
  return withTeardownNotes(succeeded);
574
600
  } catch (error) {
601
+ if (!executorEntered) throw error;
575
602
  if (activityStarted && !activityFinished)
576
603
  try {
577
604
  await activity.childFinished(
@@ -592,13 +619,8 @@ export async function executePlannedChild(input: {
592
619
  truncated: false,
593
620
  failed: true,
594
621
  });
595
- retainWriterLease = Boolean(writerLease && isHerdrWriterCloseFailure(error));
596
- const usageObservation = await activitySession.usage();
597
- try {
598
- await observeCost(usageObservation.usage?.cost.total, usageObservation.usage === undefined);
599
- } catch {
600
- /* cost observation must not replace the executor's primary failure */
601
- }
622
+ retainWriterLease ||= Boolean(writerLease && isHerdrWriterCloseFailure(error));
623
+ const usageObservation = await observeUsage();
602
624
  if (ledgerPath && !terminalAttempted) {
603
625
  // Best-effort: this records the failure, so it must not REPLACE the failure. A strict append that
604
626
  // throws here would discard the original error — including HerdrWriterCloseError, whose whole
@@ -650,7 +672,7 @@ export async function executePlannedChild(input: {
650
672
  // Attached, not dropped. `withTeardownNotes` was applied on both return paths and neither throw path,
651
673
  // so a failed lease-release record — the thing that makes the NEXT owner report a phantom crash — was
652
674
  // collected into an array nothing read.
653
- throw errorWithTeardownNotes(error, teardownFailures);
675
+ throw errorWithTeardownNotes(error, [...controlFailures, ...teardownFailures]);
654
676
  }
655
677
  /**
656
678
  * Surfaces a teardown failure WITHOUT discarding the result. The child already ran; telling the
@@ -676,18 +698,25 @@ export async function executePlannedChild(input: {
676
698
  return error;
677
699
  }
678
700
  function withTeardownNotes(outcome: DelegationOutcome): DelegationOutcome {
679
- const observed = { ...outcome, retention: retention.status() };
680
- if (teardownFailures.length === 0) return observed;
701
+ const observed = {
702
+ ...outcome,
703
+ retention: retention.status(),
704
+ observation: {
705
+ state: teardownFailures.length ? ("incomplete" as const) : ("complete" as const),
706
+ reasons: [...teardownFailures],
707
+ },
708
+ };
709
+ if (teardownFailures.length === 0 && controlFailures.length === 0) return observed;
681
710
  return {
682
711
  ...observed,
683
- control: "failed",
684
- reason: [outcome.reason, ...teardownFailures].filter(Boolean).join("; "),
712
+ ...(controlFailures.length ? { control: "failed" as const } : {}),
713
+ reason: [outcome.reason, ...controlFailures, ...teardownFailures].filter(Boolean).join("; "),
685
714
  };
686
715
  }
687
716
 
688
717
  async function teardown(): Promise<void> {
689
718
  try {
690
- await releaseDelegationWorkspace({
719
+ const outcome = await releaseDelegationWorkspace({
691
720
  prepared: preparedWorkspace,
692
721
  childId,
693
722
  episodeId: session.episodeId,
@@ -696,9 +725,13 @@ export async function executePlannedChild(input: {
696
725
  ledgerPath,
697
726
  reason: releaseReason,
698
727
  retain: retainWriterLease,
728
+ onObservationFailure: (error) =>
729
+ teardownFailures.push(`workspace release observation failed: ${String(error)}`),
699
730
  });
731
+ if (outcome === "lost" || (outcome === "retained" && !retainWriterLease))
732
+ controlFailures.push(`workspace release is ${outcome}`);
700
733
  } catch (error) {
701
- teardownFailures.push(`workspace lease record failed: ${String(error)}`);
734
+ controlFailures.push(`workspace release failed: ${String(error)}`);
702
735
  }
703
736
  }
704
737
  }
@@ -43,7 +43,7 @@ export function buildFanoutReport(
43
43
  return outcomes
44
44
  .map((outcome, index) => {
45
45
  const label = `### child ${index + 1}${children[index]?.agent ? ` (${children[index].agent})` : ""}`;
46
- return outcome.ok
46
+ return outcome.ok && outcome.control !== "failed"
47
47
  ? `${label} — completed\n\n${outcome.text || "(no output)"}`
48
48
  : `${label} — FAILED: ${outcome.reason}${outcome.text ? `\n\n${outcome.text}` : ""}`;
49
49
  })
@@ -68,14 +68,6 @@ export function throwFanoutInfrastructure(
68
68
  if (infrastructureErrors.length > 0) throw new AggregateError(infrastructureErrors, criticalBlock.text);
69
69
  throw new Error(criticalBlock.text);
70
70
  }
71
- if (infrastructureErrors.length === 1) throw infrastructureErrors[0];
72
- if (infrastructureErrors.length > 1) {
73
- throw new AggregateError(
74
- infrastructureErrors,
75
- `fan-out hit ${infrastructureErrors.length} infrastructure failures: ` +
76
- infrastructureErrors.map((error) => String(error instanceof Error ? error.message : error)).join("; "),
77
- );
78
- }
79
71
  }
80
72
 
81
73
  /**
@@ -34,7 +34,8 @@ export interface GrantsCommandContext {
34
34
  */
35
35
  executor: ExecutorChoice;
36
36
  /** ADR-0077: which advisor and task-egress mode are in force, or why none is. */
37
- advisor: { decider: string; taskEgress: "digest" | "raw"; refusal?: string };
37
+ /** Historical callers may carry this inert field. It is never read. */
38
+ advisor?: unknown;
38
39
  /** ADR-0042: which ids this session pinned, so a routing refusal is discoverable before it happens. */
39
40
  workspacePin?: ReadonlyMap<string, string>;
40
41
  /** Registry ids this machine has never accepted: present, not routable, and worth naming (rule 8). */
@@ -113,7 +114,6 @@ export const grantsCommand = {
113
114
  governed,
114
115
  ownGrant,
115
116
  executor,
116
- advisor,
117
117
  workspacePin,
118
118
  unacceptedWorkspaces,
119
119
  observed,
@@ -408,13 +408,6 @@ export const grantsCommand = {
408
408
  // two facts about what a spawn will be sit together.
409
409
  ` executor ${executor.disclosure}`,
410
410
  ` depth ${depth} of max ${maxDepth}${maxDepth <= 0 ? " (spawning disabled)" : ""}`,
411
- // Rule 8's loud half, which review found missing: an operator who upgraded from 0.34.0, or who mistyped the
412
- // variable, saw an advisor silently absent and nothing saying why. This is also where an operator sees that
413
- // which representation of the task leaves the machine, which no other status surface says.
414
- advisor.decider === "none"
415
- ? ` advisor off${advisor.refusal ? ` — ${advisor.refusal}` : ""}`
416
- : ` advisor ${advisor.decider} — task egress ${advisor.taskEgress}; ` +
417
- `a pruned context handoff also sends session turns`,
418
411
  ` ledger ${ledgerPath || "(not recording — set PI_DADDY_LEDGER)"}`,
419
412
  ` approvals ${sessionApprovals.size} this session, ${valid.size} persisted` +
420
413
  `${inheritedApprovals.size > 0 ? `, ${inheritedApprovals.size} inherited` : ""}` +