pi-daddy 0.47.1 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +40 -5
  3. package/dist/executors/child-final.d.ts +3 -0
  4. package/dist/executors/child-final.d.ts.map +1 -1
  5. package/dist/executors/child-final.js +13 -1
  6. package/dist/executors/child-final.js.map +1 -1
  7. package/dist/executors/herdr-cli.d.ts.map +1 -1
  8. package/dist/executors/herdr-cli.js +5 -1
  9. package/dist/executors/herdr-cli.js.map +1 -1
  10. package/dist/executors/herdr-qualification.d.ts +1 -3
  11. package/dist/executors/herdr-qualification.d.ts.map +1 -1
  12. package/dist/executors/herdr-qualification.js +14 -19
  13. package/dist/executors/herdr-qualification.js.map +1 -1
  14. package/dist/governance/auto-mode-policy.d.ts.map +1 -1
  15. package/dist/governance/auto-mode-policy.js +32 -1
  16. package/dist/governance/auto-mode-policy.js.map +1 -1
  17. package/dist/governance/dispatch-operation-client.d.ts +4 -0
  18. package/dist/governance/dispatch-operation-client.d.ts.map +1 -0
  19. package/dist/governance/dispatch-operation-client.js +93 -0
  20. package/dist/governance/dispatch-operation-client.js.map +1 -0
  21. package/dist/governance/dispatch-operations.d.ts +17 -0
  22. package/dist/governance/dispatch-operations.d.ts.map +1 -0
  23. package/dist/governance/dispatch-operations.js +81 -0
  24. package/dist/governance/dispatch-operations.js.map +1 -0
  25. package/dist/governance/execution-retention.d.ts +5 -4
  26. package/dist/governance/execution-retention.d.ts.map +1 -1
  27. package/dist/governance/execution-retention.js +3 -1
  28. package/dist/governance/execution-retention.js.map +1 -1
  29. package/dist/governance/native-session.d.ts +10 -1
  30. package/dist/governance/native-session.d.ts.map +1 -1
  31. package/dist/governance/native-session.js +15 -0
  32. package/dist/governance/native-session.js.map +1 -1
  33. package/dist/governance/retention-contract.d.ts +30 -0
  34. package/dist/governance/retention-contract.d.ts.map +1 -1
  35. package/dist/governance/retention-contract.js +9 -4
  36. package/dist/governance/retention-contract.js.map +1 -1
  37. package/dist/kernel/auto-mode.d.ts +1 -0
  38. package/dist/kernel/auto-mode.d.ts.map +1 -1
  39. package/dist/kernel/auto-mode.js.map +1 -1
  40. package/dist/kernel/chain.d.ts +1 -0
  41. package/dist/kernel/chain.d.ts.map +1 -1
  42. package/dist/kernel/chain.js.map +1 -1
  43. package/dist/kernel/delegate.d.ts.map +1 -1
  44. package/dist/kernel/delegate.js +8 -0
  45. package/dist/kernel/delegate.js.map +1 -1
  46. package/dist/kernel/dispatch-operation.d.ts +47 -0
  47. package/dist/kernel/dispatch-operation.d.ts.map +1 -0
  48. package/dist/kernel/dispatch-operation.js +4 -0
  49. package/dist/kernel/dispatch-operation.js.map +1 -0
  50. package/dist/kernel/propagation.d.ts +4 -1
  51. package/dist/kernel/propagation.d.ts.map +1 -1
  52. package/dist/kernel/propagation.js +11 -1
  53. package/dist/kernel/propagation.js.map +1 -1
  54. package/dist/products/activity-timeline.d.ts +8 -0
  55. package/dist/products/activity-timeline.d.ts.map +1 -1
  56. package/dist/products/activity-timeline.js +8 -1
  57. package/dist/products/activity-timeline.js.map +1 -1
  58. package/dist/products/dashboard-screen.d.ts +1 -0
  59. package/dist/products/dashboard-screen.d.ts.map +1 -1
  60. package/dist/products/dashboard-screen.js +20 -2
  61. package/dist/products/dashboard-screen.js.map +1 -1
  62. package/extensions/activity-timeline.ts +12 -2
  63. package/extensions/delegate-chain.ts +46 -4
  64. package/extensions/delegation-operations.ts +171 -0
  65. package/extensions/delegation.ts +34 -10
  66. package/extensions/execute-child.ts +21 -3
  67. package/extensions/execution-evidence.ts +3 -0
  68. package/extensions/fanout-outcome.ts +1 -0
  69. package/extensions/grants.ts +2 -3
  70. package/extensions/run-delegation.ts +58 -3
  71. package/extensions/runtime-snapshot.ts +59 -1
  72. package/package.json +1 -1
  73. package/src/executors/child-final.ts +17 -1
  74. package/src/executors/herdr-cli.ts +4 -1
  75. package/src/executors/herdr-qualification.ts +14 -21
  76. package/src/governance/auto-mode-policy.ts +27 -1
  77. package/src/governance/dispatch-operation-client.ts +96 -0
  78. package/src/governance/dispatch-operations.ts +81 -0
  79. package/src/governance/execution-retention.ts +7 -3
  80. package/src/governance/native-session.ts +28 -1
  81. package/src/governance/retention-contract.ts +9 -4
  82. package/src/kernel/auto-mode.ts +1 -0
  83. package/src/kernel/chain.ts +1 -0
  84. package/src/kernel/delegate.ts +14 -0
  85. package/src/kernel/dispatch-operation.ts +38 -0
  86. package/src/kernel/propagation.ts +18 -1
  87. package/src/products/activity-timeline.ts +10 -0
  88. package/src/products/dashboard-screen.ts +21 -2
@@ -214,9 +214,14 @@ export function registerActivityTimeline(pi: ExtensionAPI, session?: ActivitySes
214
214
  }
215
215
  return undefined;
216
216
  });
217
- pi.on("tool_execution_start", (event) => {
218
- const value = event as { toolCallId?: string; args?: { path?: unknown } };
217
+ pi.on("tool_execution_start", async (event, ctx: HookContext) => {
218
+ const value = event as { toolCallId?: string; toolName?: string; args?: { path?: unknown } };
219
219
  if (value.toolCallId && typeof value.args?.path === "string") paths.set(value.toolCallId, value.args.path);
220
+ try {
221
+ if (value.toolName) await recorder?.append("tool_started", { tool: value.toolName, cwd: ctx.cwd });
222
+ } catch {
223
+ /* observation cannot affect tool execution */
224
+ }
220
225
  return undefined;
221
226
  });
222
227
  pi.on("tool_execution_end", async (event) => {
@@ -224,6 +229,11 @@ export function registerActivityTimeline(pi: ExtensionAPI, session?: ActivitySes
224
229
  const value = event as { isError?: boolean; toolName?: string; toolCallId?: string };
225
230
  const path = value.toolCallId ? paths.get(value.toolCallId) : undefined;
226
231
  if (value.toolCallId) paths.delete(value.toolCallId);
232
+ if (value.toolName)
233
+ await recorder?.append("tool_finished", {
234
+ tool: value.toolName,
235
+ outcome: value.isError ? "failed" : "completed",
236
+ });
227
237
  const skill = path ? available.get(resolve(path)) : undefined;
228
238
  if (!value.isError && value.toolName === "read" && skill) await recorder?.skill("skill_read", skill);
229
239
  } catch {
@@ -1,3 +1,4 @@
1
+ import { operationIdShape, claimDelegationOperation, existingOperationOutcome } from "./delegation-operations.ts";
1
2
  import { publicEvidenceCall } from "./public-evidence.ts";
2
3
  import { executionEvidenceContent } from "./execution-evidence.ts";
3
4
  import { assertDefinitionIdentity } from "./definition-describe.ts";
@@ -76,6 +77,7 @@ function mergeGateOutcomes(outcomes: readonly ApprovalOutcome[]): ApprovalOutcom
76
77
 
77
78
  export function registerChainTool(pi: ExtensionAPI, session: GrantsSession): void {
78
79
  const stepShape = Type.Object({
80
+ operation_id: operationIdShape(),
79
81
  definitionId: Type.Optional(
80
82
  Type.String({ description: "Snapshot id returned by delegate_describe; required for Principal phases." }),
81
83
  ),
@@ -143,9 +145,47 @@ export function registerChainTool(pi: ExtensionAPI, session: GrantsSession): voi
143
145
  // Plan every step first. A step that can never run refuses the chain HERE, before anyone is asked — see
144
146
  // `planChain`.
145
147
  const executionIds = steps.map(() => newExecutionId());
146
- const capture = publicEvidenceCall(session.publicEvidence, _toolCallId, "delegate_chain", steps, executionIds);
147
148
  await reconcileDelegationCapacity(session);
148
- const firstReservation = reserveDelegationCapacity(session, executionIds[0]);
149
+ const firstClaim = await claimDelegationOperation(
150
+ session,
151
+ steps[0],
152
+ {
153
+ executionId: executionIds[0],
154
+ parentExecutionId: session.ownExecutionId ?? null,
155
+ parentId: session.ownSpawnId,
156
+ childId: childSpawnId(session.ownSpawnId, 0),
157
+ },
158
+ ctx,
159
+ signal,
160
+ );
161
+ const capture = publicEvidenceCall(
162
+ session.publicEvidence,
163
+ _toolCallId,
164
+ "delegate_chain",
165
+ steps,
166
+ executionIds.map((id, index) => (index === 0 ? (firstClaim?.operation.executionId ?? id) : id)),
167
+ );
168
+ if (firstClaim?.reused) {
169
+ const outcome = existingOperationOutcome(firstClaim, session.depth + 1);
170
+ return capture.finish(
171
+ {
172
+ isError: false,
173
+ content: [
174
+ { type: "text", text: outcome.text },
175
+ executionEvidenceContent("delegate_chain", [outcome], steps.length),
176
+ ],
177
+ details: { outcomes: [outcome], steps: steps.length, completed: 0, aborted: true },
178
+ },
179
+ [outcome],
180
+ );
181
+ }
182
+ let firstReservation: ReturnType<typeof reserveDelegationCapacity>;
183
+ try {
184
+ firstReservation = reserveDelegationCapacity(session, executionIds[0]);
185
+ } catch (error) {
186
+ await firstClaim?.finish?.("not-started");
187
+ throw error;
188
+ }
149
189
  try {
150
190
  const parentExecutionId = session.ownExecutionId ?? null;
151
191
  const piModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
@@ -328,7 +368,7 @@ export function registerChainTool(pi: ExtensionAPI, session: GrantsSession): voi
328
368
  ctx,
329
369
  signal,
330
370
  {
331
- ...(index === 0 ? { capacityReservation: firstReservation } : {}),
371
+ ...(index === 0 ? { capacityReservation: firstReservation, operationClaim: firstClaim } : {}),
332
372
  resolvedRuntime: runtimeChoices.get(step),
333
373
  onDefinition: (definition) => capture.selected(index, definition),
334
374
  preApproved: availableForStep,
@@ -387,6 +427,7 @@ export function registerChainTool(pi: ExtensionAPI, session: GrantsSession): voi
387
427
  const report = outcomes
388
428
  .map((o) => {
389
429
  const label = `### step ${o.step}${o.agent ? ` (${o.agent})` : ""}`;
430
+ if (o.operation?.reused) return `${label} — existing operation\n\n${o.text}`;
390
431
  return o.ok && o.control !== "failed"
391
432
  ? `${label} — completed\n\n${o.text || "(no output)"}`
392
433
  : `${label} — FAILED: ${o.reason}${o.text ? `\n\n${o.text}` : ""}`;
@@ -414,7 +455,7 @@ export function registerChainTool(pi: ExtensionAPI, session: GrantsSession): voi
414
455
  }
415
456
  return capture.finish(
416
457
  {
417
- isError: aborted || outcomes.some((o) => o.control === "failed"),
458
+ isError: (aborted && !outcomes.at(-1)?.operation?.reused) || outcomes.some((o) => o.control === "failed"),
418
459
  content: [
419
460
  { type: "text", text: `${report}${tail}` },
420
461
  executionEvidenceContent("delegate_chain", outcomes, steps.length),
@@ -433,6 +474,7 @@ export function registerChainTool(pi: ExtensionAPI, session: GrantsSession): voi
433
474
  );
434
475
  } finally {
435
476
  firstReservation.finalize({ state: "not-started", reason: "chain ended before its first execution" });
477
+ await firstClaim?.finish?.("not-started").catch(() => undefined);
436
478
  }
437
479
  },
438
480
  });
@@ -0,0 +1,171 @@
1
+ /** Non-authorizing dispatch election. The elected caller still runs the ordinary governance path. */
2
+ import { createHash } from "node:crypto";
3
+ import { realpath } from "node:fs/promises";
4
+ import { Type } from "typebox";
5
+ import { shouldSeekApproval } from "../src/kernel/approval.ts";
6
+ import { planDelegation } from "../src/kernel/delegate.ts";
7
+ import type { OperationClaim } from "../src/kernel/dispatch-operation.ts";
8
+ import { validOperationId } from "../src/kernel/dispatch-operation.ts";
9
+ import { GovernanceRefusal } from "../src/kernel/refusals.ts";
10
+ import { assertDefinitionIdentity } from "./definition-describe.ts";
11
+ import { assertDelegationAuthority } from "./delegation-authority.ts";
12
+ import { recordDelegationDecision } from "./delegation-ledger.ts";
13
+ import type { GrantsSession } from "./session.ts";
14
+ import type { ChildSpec, DelegationToolContext } from "./run-delegation.ts";
15
+ import type { ExecutionOccurrenceIds } from "./execution-occurrence.ts";
16
+ import type { DelegationOutcome } from "./execute-child.ts";
17
+
18
+ export const operationIdShape = () =>
19
+ Type.Optional(
20
+ Type.String({
21
+ minLength: 1,
22
+ maxLength: 160,
23
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$",
24
+ description:
25
+ "Stable caller operation ID. Identical concurrent dispatch returns its existing reference; terminal IDs never replay work. Use a new ID only for an intentional new attempt.",
26
+ }),
27
+ );
28
+ function canonical(value: unknown): unknown {
29
+ if (Array.isArray(value)) return value.map(canonical);
30
+ if (value && typeof value === "object")
31
+ return Object.fromEntries(
32
+ Object.entries(value)
33
+ .filter(([, v]) => v !== undefined)
34
+ .sort(([a], [b]) => a.localeCompare(b))
35
+ .map(([k, v]) => [k, canonical(v)]),
36
+ );
37
+ return value;
38
+ }
39
+ export async function claimDelegationOperation(
40
+ session: GrantsSession,
41
+ spec: ChildSpec,
42
+ ids: ExecutionOccurrenceIds,
43
+ ctx: DelegationToolContext,
44
+ signal?: AbortSignal,
45
+ ): Promise<OperationClaim | undefined> {
46
+ if (spec.operation_id === undefined) return undefined;
47
+ assertDelegationAuthority(session);
48
+ if (!validOperationId(spec.operation_id)) throw Error("Invalid operation_id");
49
+ if (signal?.aborted) throw Error("Operation request aborted");
50
+ await session.ensureDefinitions?.();
51
+ assertDefinitionIdentity(session, spec);
52
+ const context = await session.delegationContext();
53
+ const model = spec.model ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
54
+ const preview = planDelegation(
55
+ { ...spec, model, boundWorkspaceId: spec.workspace?.workspace_id },
56
+ {
57
+ ...context,
58
+ stageHandoff: undefined,
59
+ fanoutBudget: 0,
60
+ spawnId: ids.parentId,
61
+ childSpawnId: ids.childId,
62
+ childExecutionId: ids.executionId,
63
+ },
64
+ );
65
+ if (!preview.ok && !shouldSeekApproval(preview.result)) {
66
+ await recordDelegationDecision({ session, plan: preview, ids, agent: spec.agent });
67
+ if (preview.refusal) throw new GovernanceRefusal(preview.refusal);
68
+ throw Error(preview.reason ?? "Operation request is not authorized");
69
+ }
70
+ const operations = session.autoMode?.operations;
71
+ if (!operations) throw Error("Session operation admission is unavailable; no dispatch started");
72
+ const cwd = await realpath(ctx.cwd);
73
+ const { operation_id: _id, ...request } = spec;
74
+ const requestDigest = createHash("sha256")
75
+ .update(
76
+ JSON.stringify(
77
+ canonical({
78
+ request,
79
+ cwd,
80
+ model,
81
+ piThinking: session.currentThinking?.(),
82
+ definitionDigest: preview.definitionDigest,
83
+ grant: [...session.ownGrant].sort(),
84
+ gated: [...session.gated].sort(),
85
+ depth: session.depth,
86
+ maxDepth: session.maxDepth,
87
+ executor: session.executor.kind,
88
+ definition: spec.agent ? session.definitions.get(spec.agent)?.body : null,
89
+ runtimeSettings: session.definitionRuntimeSettings,
90
+ runtimeOverrides: [...session.definitionRuntimeOverrides.entries()].sort(([a], [b]) => a.localeCompare(b)),
91
+ workspaceRoot: spec.workspace ? session.workspacePin?.get(spec.workspace.workspace_id) : null,
92
+ }),
93
+ ),
94
+ )
95
+ .digest("hex");
96
+ return operations.claim(
97
+ {
98
+ operationId: spec.operation_id,
99
+ requestDigest,
100
+ executionId: ids.executionId,
101
+ cwd,
102
+ workspaceId: spec.workspace?.workspace_id ?? null,
103
+ },
104
+ signal,
105
+ );
106
+ }
107
+ export function existingOperationOutcome(claim: OperationClaim, depth: number): DelegationOutcome {
108
+ const active = ["admitting", "running"].includes(claim.operation.state);
109
+ return {
110
+ ok: false,
111
+ work: "unknown",
112
+ text:
113
+ `No new child started. Operation ${claim.operation.operationId} already exists (${claim.operation.state}), execution ${claim.operation.executionId}. ` +
114
+ (active
115
+ ? "Follow the original operation; this reference is not its final result."
116
+ : "No mutation result was replayed. Inspect the original outcome before an intentional new attempt with a new operation_id."),
117
+ reason: active ? "existing-operation" : "operation-already-ended",
118
+ granted: [],
119
+ depth,
120
+ exitCode: null,
121
+ operation: { ...claim.operation, reused: true },
122
+ };
123
+ }
124
+ export async function finishDelegationOperation(claim: OperationClaim, result: DelegationOutcome) {
125
+ const cleanup = result.cleanup;
126
+ const state =
127
+ cleanup?.state === "settled"
128
+ ? "settled"
129
+ : cleanup?.state === "not-started" || (!result.work && !result.final && !cleanup)
130
+ ? "not-started"
131
+ : "uncertain";
132
+ await claim.finish!(state, {
133
+ work: result.work,
134
+ control: result.control,
135
+ ...(result.final
136
+ ? {
137
+ final:
138
+ result.final.state === "complete"
139
+ ? {
140
+ state: result.final.state,
141
+ sessionId: result.final.sessionId,
142
+ messageId: result.final.messageId,
143
+ leafId: result.final.leafId,
144
+ sha256: result.final.sha256,
145
+ }
146
+ : { state: result.final.state },
147
+ }
148
+ : {}),
149
+ ...(state === "not-started" && !cleanup ? { cleanup: { state: "not-started" } } : {}),
150
+ ...(cleanup
151
+ ? {
152
+ cleanup: {
153
+ state: cleanup.state,
154
+ ...(cleanup.state === "settled"
155
+ ? {
156
+ receiptPath: cleanup.identity.receiptPath,
157
+ executionId: cleanup.identity.executionId,
158
+ }
159
+ : {}),
160
+ },
161
+ }
162
+ : {}),
163
+ });
164
+ result.operation = {
165
+ ...claim.operation,
166
+ state,
167
+ cwd: result.cwd ?? claim.operation.cwd,
168
+ workspaceId: result.workspaceId ?? claim.operation.workspaceId,
169
+ reused: false,
170
+ };
171
+ }
@@ -1,3 +1,4 @@
1
+ import { operationIdShape, claimDelegationOperation } from "./delegation-operations.ts";
1
2
  import { publicEvidenceCall } from "./public-evidence.ts";
2
3
  import { executionEvidenceContent } from "./execution-evidence.ts";
3
4
  import { reconcileDelegationCapacity, reserveDelegationCapacity } from "./session-capacity.ts";
@@ -190,6 +191,7 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
190
191
  ),
191
192
  );
192
193
  const childShape = Type.Object({
194
+ operation_id: operationIdShape(),
193
195
  definitionId: Type.Optional(
194
196
  Type.String({ description: "Snapshot id returned by delegate_describe; required for Principal phases." }),
195
197
  ),
@@ -212,6 +214,7 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
212
214
  });
213
215
 
214
216
  const delegateParams = Type.Object({
217
+ operation_id: operationIdShape(),
215
218
  definitionId: Type.Optional(
216
219
  Type.String({ description: "Snapshot id returned by delegate_describe; required for Principal phases." }),
217
220
  ),
@@ -255,16 +258,18 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
255
258
  // `delegate` for up to DEFAULT_TIMEOUT_MS — sixty minutes by default.
256
259
  const progress = progressReporter(session, [params.agent ?? "delegate"], onUpdate as never);
257
260
  const occurrence = newDelegationOccurrence(session, 0);
261
+ const operationClaim = await claimDelegationOperation(session, params, occurrence, ctx, signal);
258
262
  const capture = publicEvidenceCall(
259
263
  session.publicEvidence,
260
264
  _toolCallId,
261
265
  "delegate",
262
266
  [params],
263
- [occurrence.executionId],
267
+ [operationClaim?.operation.executionId ?? occurrence.executionId],
264
268
  );
265
269
  const outcome = await runOneDelegation(
266
270
  session,
267
271
  {
272
+ operation_id: params.operation_id,
268
273
  definitionId: params.definitionId,
269
274
  task: params.task,
270
275
  agent: params.agent,
@@ -281,6 +286,7 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
281
286
  ctx,
282
287
  signal,
283
288
  {
289
+ operationClaim,
284
290
  onProgress: progress.sink(0),
285
291
  toolCallId: _toolCallId,
286
292
  onDefinition: (definition) => capture.selected(0, definition),
@@ -296,7 +302,7 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
296
302
  throw new Error(message);
297
303
  }
298
304
  // Pi 1.0.4 preserves returned isError. Keep every outcome dimension and useful output together.
299
- const failed = !outcome.ok || outcome.control === "failed";
305
+ const failed = !outcome.operation?.reused && (!outcome.ok || outcome.control === "failed");
300
306
  return capture.finish(
301
307
  {
302
308
  isError: failed,
@@ -348,8 +354,19 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
348
354
  const children = params.children ?? [];
349
355
  if (session.capacityRefusal) throw new GovernanceRefusal(session.capacityRefusal);
350
356
  await reconcileDelegationCapacity(session);
351
- const split = splitBudget(session.capacity.available, children.length);
357
+ const occurrences = children.map((_, index) => newDelegationOccurrence(session, index));
358
+ const claims: Array<import("../src/kernel/dispatch-operation.ts").OperationClaim | undefined> = [];
359
+ try {
360
+ for (const [index, child] of children.entries())
361
+ claims.push(await claimDelegationOperation(session, child, occurrences[index], ctx, signal));
362
+ } catch (error) {
363
+ await Promise.all(claims.map((claim) => claim?.finish?.("not-started")));
364
+ throw error;
365
+ }
366
+ const fresh = children.filter((_, index) => !claims[index]?.reused).length;
367
+ const split = fresh === 0 ? { ok: true as const, perChild: 0 } : splitBudget(session.capacity.available, fresh);
352
368
  if (!split.ok) {
369
+ await Promise.all(claims.map((claim) => claim?.finish?.("not-started")));
353
370
  // Thrown, not returned: a returned `isError` is discarded by pi, so a refusal that came back as a
354
371
  // normal result would read to the orchestrator as a successful fan-out of zero children.
355
372
  throw new GovernanceRefusal(refusal("FANOUT_EXCEEDED", `fan-out refused: ${split.reason}`));
@@ -370,25 +387,32 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
370
387
  // live and its writer lease is deliberately retained" — a resource-retention notice, not a per-child
371
388
  // failure. Losing it meant nobody was told a lease is held with no owner until the process exits
372
389
  // (R-116).
373
- const infrastructureErrors: unknown[] = [],
374
- occurrences = children.map((_, index) => newDelegationOccurrence(session, index));
390
+ const infrastructureErrors: unknown[] = [];
375
391
  const capture = publicEvidenceCall(
376
392
  session.publicEvidence,
377
393
  _toolCallId,
378
394
  "delegate_all",
379
395
  children,
380
- occurrences.map((row) => row.executionId),
396
+ occurrences.map((row, index) => claims[index]?.operation.executionId ?? row.executionId),
381
397
  );
382
398
  const pending = children.map(async (child, index): Promise<DelegationOutcome> => {
399
+ let enteredDelegation = false;
383
400
  try {
401
+ // Reserve each batch share synchronously here, before another dispatch can interleave.
402
+ const capacityReservation = claims[index]?.reused
403
+ ? undefined
404
+ : reserveDelegationCapacity(session, occurrences[index].executionId, split.perChild);
405
+ enteredDelegation = true;
384
406
  return await runOneDelegation(session, child, occurrences[index], split.perChild, ctx, signal, {
385
- // Reserve each batch share synchronously here, before another dispatch can interleave.
386
- capacityReservation: reserveDelegationCapacity(session, occurrences[index].executionId, split.perChild),
407
+ operationClaim: claims[index],
408
+ capacityReservation,
387
409
  onDefinition: (definition) => capture.selected(index, definition),
388
410
  onProgress: progress.sink(index),
389
411
  toolCallId: _toolCallId,
390
412
  });
391
413
  } catch (error) {
414
+ if (!enteredDelegation)
415
+ await claims[index]?.finish?.("not-started", { cleanup: { state: "not-started" } }).catch(() => undefined);
392
416
  infrastructureErrors.push(error);
393
417
  return childFailureOutcome(error, session.depth + 1);
394
418
  }
@@ -401,7 +425,7 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
401
425
  // critical block landed in the same fan-out (R-117).
402
426
  throwFanoutInfrastructure(outcomes, infrastructureErrors);
403
427
 
404
- const failed = outcomes.filter((o) => !o.ok);
428
+ const failed = outcomes.filter((o) => !o.ok && !o.operation?.reused);
405
429
  // Every child is reported, including the ones that failed. R-03's rule: a missing result must never
406
430
  // be indistinguishable from an empty one, and a fan-out that hid its refusals would let an
407
431
  // orchestrator summarise four reviews when only three happened.
@@ -411,7 +435,7 @@ export function registerDelegationTools(pi: ExtensionAPI, session: GrantsSession
411
435
 
412
436
  return capture.finish(
413
437
  {
414
- isError: outcomes.some((o) => !o.ok || o.control === "failed"),
438
+ isError: outcomes.some((o) => !o.operation?.reused && (!o.ok || o.control === "failed")),
415
439
  content: [
416
440
  { type: "text", text: report },
417
441
  executionEvidenceContent("delegate_all", outcomes, children.length),
@@ -1,9 +1,10 @@
1
+ import { realpath } from "node:fs/promises";
1
2
  import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
2
3
  import { retainDiscoveryCleanupFailure } from "./session.ts";
3
4
  import type { CapacityReservation } from "../src/kernel/capacity.ts";
4
5
  import { runHerdrOwned } from "../src/executors/herdr-owned.ts";
5
6
  import { runCapturedExecution } from "../src/executors/captured-execution.ts";
6
- import type { ChildFinal } from "../src/executors/child-final.ts";
7
+ import { capturedFinalSession, type ChildFinal } from "../src/executors/child-final.ts";
7
8
  import type { CapturedWorkerCleanup } from "../src/kernel/captured-worker-contract.ts";
8
9
  import type { Delegation } from "../src/kernel/delegate.ts";
9
10
  import {
@@ -33,6 +34,9 @@ import { releaseDelegationWorkspace, type PreparedWorkspace } from "./workspace-
33
34
  import { ActivityTimelineRecorder, ENV_ACTIVITY_PARENT_TASK } from "../src/products/activity-timeline.ts";
34
35
  import { resolvedModelOf, type ResolvedDefinitionRuntime } from "./definition-runtime.ts";
35
36
  export interface DelegationOutcome {
37
+ operation?: import("../src/kernel/dispatch-operation.ts").OperationReference & { reused: boolean };
38
+ cwd?: string;
39
+ workspaceId?: string | null;
36
40
  ok: boolean;
37
41
  text: string;
38
42
  reason?: string;
@@ -121,6 +125,9 @@ export interface ChildProgressUpdate {
121
125
  * blaming "ledger" (R-99). The failure is reported alongside the outcome instead of replacing it.
122
126
  */
123
127
  export interface ChildExecutionInput {
128
+ operationClaim?: import("../src/kernel/dispatch-operation.ts").OperationClaim;
129
+ /** Composition-only launch observation; no permission or result authority. */
130
+ onExecutorEntry?: () => void;
124
131
  session: GrantsSession;
125
132
  capacityReservation?: CapacityReservation;
126
133
  plan: Delegation;
@@ -140,12 +147,14 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
140
147
  const runtime = input.session.reloadLifecycle?.runtimeSettlement;
141
148
  let registered = false;
142
149
  try {
150
+ input = { ...input, cwd: await realpath(input.preparedWorkspace?.workspace.root ?? input.cwd) };
143
151
  runtime?.begin(input.executionId);
144
152
  registered = Boolean(runtime);
145
153
  const result = await executePreparedChild(
146
154
  input,
147
155
  () => {
148
156
  executorEntered = true;
157
+ input.onExecutorEntry?.();
149
158
  },
150
159
  runtime,
151
160
  );
@@ -162,7 +171,11 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
162
171
  reason: [result.reason, `runtime settlement record failed: ${String(error)}`].filter(Boolean).join("; "),
163
172
  };
164
173
  }
165
- return result;
174
+ return {
175
+ ...result,
176
+ cwd: input.preparedWorkspace?.workspace.root ?? input.cwd,
177
+ workspaceId: input.preparedWorkspace?.workspace.workspaceId ?? null,
178
+ };
166
179
  } catch (error) {
167
180
  const failures: unknown[] = [error];
168
181
  if (registered)
@@ -336,6 +349,7 @@ async function executePreparedChild(
336
349
  const remainingTimeoutMs = Math.max(1, Date.parse(deadlineAt) - Date.now());
337
350
  const terminationGraceMs = Math.min(DEFAULT_KILL_GRACE_MS, Math.max(1, Math.floor(remainingTimeoutMs / 10)));
338
351
  const cwd = preparedWorkspace?.workspace.root ?? input.cwd;
352
+ await input.operationClaim?.started?.(cwd, preparedWorkspace?.workspace.workspaceId ?? null);
339
353
  const leaseAbort = new AbortController();
340
354
  const writerLease = preparedWorkspace?.lease.access === "write" ? preparedWorkspace.lease : undefined;
341
355
  // Tracked as a FACT, not only as an abort: "the kernel lock protecting this workspace evaporated under
@@ -460,7 +474,11 @@ async function executePreparedChild(
460
474
  : undefined,
461
475
  );
462
476
 
463
- if (sessionFlag >= 0) retention.observeSession({ source: "pi-session-file", value: plan.args[sessionFlag + 1] });
477
+ const capturedBranch = capturedFinalSession(output.final);
478
+ if (capturedBranch)
479
+ retention.observeSession({ source: "pi-captured-final", value: capturedBranch.path, capturedBranch });
480
+ else if (sessionFlag >= 0)
481
+ retention.observeSession({ source: "pi-session-file", value: plan.args[sessionFlag + 1] });
464
482
  retention.capture("result", Buffer.from(output.text), true);
465
483
  if ("diagnosticsTruncated" in output && output.diagnosticsTruncated)
466
484
  teardownFailures.push("diagnostic output truncated");
@@ -49,6 +49,9 @@ function outcomeEvidence(outcome: DelegationOutcome, index: number) {
49
49
  const final = outcome.final;
50
50
  return {
51
51
  ordinal: index + 1,
52
+ cwd: outcome.cwd ?? outcome.operation?.cwd ?? null,
53
+ workspaceId: outcome.workspaceId ?? outcome.operation?.workspaceId ?? null,
54
+ operation: outcome.operation ?? null,
52
55
  ok: outcome.ok,
53
56
  work: outcome.work ?? null,
54
57
  control: outcome.control ?? null,
@@ -43,6 +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
+ if (outcome.operation?.reused) return `${label} — existing operation\n\n${outcome.text}`;
46
47
  return outcome.ok && outcome.control !== "failed"
47
48
  ? `${label} — completed\n\n${outcome.text || "(no output)"}`
48
49
  : `${label} — FAILED: ${outcome.reason}${outcome.text ? `\n\n${outcome.text}` : ""}`;
@@ -365,12 +365,11 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
365
365
  return undefined;
366
366
  });
367
367
 
368
- // Observe this session's real tool surface once, and tighten the grant to it. Authoritative because
369
- // it is exactly what pi sent the model.
368
+ // Observe the native reachable tool surface, including callable tools whose declarations Codemode hides.
370
369
  pi.on("before_provider_request", (event, ctx) => {
371
370
  try {
372
371
  if (session.observed) return undefined;
373
- const names = observeToolNames(event.payload);
372
+ const names = observeToolNames(event.payload, pi.getAllTools(), pi.getActiveTools?.());
374
373
  if (names === null) return undefined;
375
374
  session.observed = true;
376
375
  session.observedTools = names;
@@ -1,3 +1,9 @@
1
+ import {
2
+ claimDelegationOperation,
3
+ existingOperationOutcome,
4
+ finishDelegationOperation,
5
+ } from "./delegation-operations.ts";
6
+ import type { OperationClaim } from "../src/kernel/dispatch-operation.ts";
1
7
  import { reconcileDelegationCapacity, reserveDelegationCapacity } from "./session-capacity.ts";
2
8
  import type { CapacityReservation } from "../src/kernel/capacity.ts";
3
9
  import { assertDefinitionIdentity } from "./definition-describe.ts";
@@ -43,7 +49,8 @@ import {
43
49
  } from "./workspace-runtime.ts";
44
50
 
45
51
  /** What one child was asked to do. The shape both tools accept, per child. */
46
- interface ChildSpec {
52
+ export interface ChildSpec {
53
+ operation_id?: string;
47
54
  definitionId?: string;
48
55
  task: string;
49
56
  agent?: string;
@@ -64,7 +71,7 @@ interface ChildSpec {
64
71
  * bridged the gap, which is the same "a value that was whatever happened to be in scope" shape the module
65
72
  * header lists four defects for.
66
73
  */
67
- interface DelegationToolContext extends ApprovalUIContext {
74
+ export interface DelegationToolContext extends ApprovalUIContext {
68
75
  cwd: string;
69
76
  model?: { provider: string; id: string };
70
77
  modelRegistry: ModelCatalogue;
@@ -242,7 +249,7 @@ export async function planWithApprovals(
242
249
  * was refused". `delegate` converts a failure back into a throw to keep its own contract, which matters:
243
250
  * `AgentToolResult` has no `isError` field, so a returned error is silently discarded by pi.
244
251
  */
245
- export async function runOneDelegation(
252
+ async function runOneDelegationImplementation(
246
253
  session: GrantsSession,
247
254
  spec: ChildSpec,
248
255
  ids: ExecutionOccurrenceIds,
@@ -265,6 +272,9 @@ export async function runOneDelegation(
265
272
  * mistake unspellable.
266
273
  */
267
274
  options: {
275
+ operationClaim?: OperationClaim;
276
+ /** Trusted observation at the actual executor boundary, not a caller/tool parameter. */
277
+ onExecutorEntry?: () => void;
268
278
  /** Trusted reservation made before a chain's upfront gate. */
269
279
  capacityReservation?: CapacityReservation;
270
280
  /** Exact pair selected during the chain preflight; never reselect after its approval. */
@@ -513,6 +523,8 @@ export async function runOneDelegation(
513
523
  executorEntered = true;
514
524
  return await executePlannedChild({
515
525
  capacityReservation: capacity,
526
+ operationClaim: options.operationClaim,
527
+ onExecutorEntry: options.onExecutorEntry,
516
528
  session,
517
529
  plan,
518
530
  agent: spec.agent,
@@ -534,3 +546,46 @@ export async function runOneDelegation(
534
546
  );
535
547
  }
536
548
  }
549
+
550
+ /** Duplicate references never enter the executor, consume capacity, or become chain input. */
551
+ export async function runOneDelegation(
552
+ ...args: Parameters<typeof runOneDelegationImplementation>
553
+ ): Promise<DelegationOutcome> {
554
+ const [session, spec, ids, , ctx, signal] = args;
555
+ const options = args[6] ?? {};
556
+ const claim = options.operationClaim ?? (await claimDelegationOperation(session, spec, ids, ctx, signal));
557
+ if (claim?.reused) {
558
+ options.capacityReservation?.finalize({ state: "not-started", reason: "existing operation reused" });
559
+ return existingOperationOutcome(claim, session.depth + 1);
560
+ }
561
+ let executorEntered = false;
562
+ try {
563
+ args[6] = {
564
+ ...options,
565
+ operationClaim: claim,
566
+ onExecutorEntry: () => {
567
+ executorEntered = true;
568
+ options.onExecutorEntry?.();
569
+ },
570
+ };
571
+ const result = await runOneDelegationImplementation(...args);
572
+ if (claim) {
573
+ try {
574
+ await finishDelegationOperation(claim, result);
575
+ } catch (error) {
576
+ // A post-execution observation failure must not erase the native final or settlement receipt.
577
+ result.control = "failed";
578
+ result.reason = [result.reason, `operation status update failed: ${String(error)}`].filter(Boolean).join("; ");
579
+ result.operation = { ...claim.operation, state: "uncertain", reused: false };
580
+ }
581
+ }
582
+ return result;
583
+ } catch (error) {
584
+ if (claim)
585
+ await claim.finish!(
586
+ executorEntered ? "uncertain" : "not-started",
587
+ executorEntered ? undefined : { cleanup: { state: "not-started" } },
588
+ ).catch(() => undefined);
589
+ throw error;
590
+ }
591
+ }