pi-daddy 0.47.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +26 -1
- package/dist/executors/child-final.d.ts +3 -0
- package/dist/executors/child-final.d.ts.map +1 -1
- package/dist/executors/child-final.js +13 -1
- package/dist/executors/child-final.js.map +1 -1
- package/dist/governance/auto-mode-policy.d.ts.map +1 -1
- package/dist/governance/auto-mode-policy.js +32 -1
- package/dist/governance/auto-mode-policy.js.map +1 -1
- package/dist/governance/dispatch-operation-client.d.ts +4 -0
- package/dist/governance/dispatch-operation-client.d.ts.map +1 -0
- package/dist/governance/dispatch-operation-client.js +93 -0
- package/dist/governance/dispatch-operation-client.js.map +1 -0
- package/dist/governance/dispatch-operations.d.ts +17 -0
- package/dist/governance/dispatch-operations.d.ts.map +1 -0
- package/dist/governance/dispatch-operations.js +81 -0
- package/dist/governance/dispatch-operations.js.map +1 -0
- package/dist/governance/execution-retention.d.ts +5 -4
- package/dist/governance/execution-retention.d.ts.map +1 -1
- package/dist/governance/execution-retention.js +3 -1
- package/dist/governance/execution-retention.js.map +1 -1
- package/dist/governance/native-session.d.ts +10 -1
- package/dist/governance/native-session.d.ts.map +1 -1
- package/dist/governance/native-session.js +15 -0
- package/dist/governance/native-session.js.map +1 -1
- package/dist/governance/retention-contract.d.ts +30 -0
- package/dist/governance/retention-contract.d.ts.map +1 -1
- package/dist/governance/retention-contract.js +9 -4
- package/dist/governance/retention-contract.js.map +1 -1
- package/dist/kernel/auto-mode.d.ts +1 -0
- package/dist/kernel/auto-mode.d.ts.map +1 -1
- package/dist/kernel/auto-mode.js.map +1 -1
- package/dist/kernel/chain.d.ts +1 -0
- package/dist/kernel/chain.d.ts.map +1 -1
- package/dist/kernel/chain.js.map +1 -1
- package/dist/kernel/delegate.d.ts.map +1 -1
- package/dist/kernel/delegate.js +8 -0
- package/dist/kernel/delegate.js.map +1 -1
- package/dist/kernel/dispatch-operation.d.ts +47 -0
- package/dist/kernel/dispatch-operation.d.ts.map +1 -0
- package/dist/kernel/dispatch-operation.js +4 -0
- package/dist/kernel/dispatch-operation.js.map +1 -0
- package/dist/kernel/propagation.d.ts +4 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +11 -1
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/products/activity-timeline.d.ts +8 -0
- package/dist/products/activity-timeline.d.ts.map +1 -1
- package/dist/products/activity-timeline.js +8 -1
- package/dist/products/activity-timeline.js.map +1 -1
- package/dist/products/dashboard-screen.d.ts +1 -0
- package/dist/products/dashboard-screen.d.ts.map +1 -1
- package/dist/products/dashboard-screen.js +20 -2
- package/dist/products/dashboard-screen.js.map +1 -1
- package/extensions/activity-timeline.ts +12 -2
- package/extensions/delegate-chain.ts +46 -4
- package/extensions/delegation-operations.ts +171 -0
- package/extensions/delegation.ts +34 -10
- package/extensions/execute-child.ts +21 -3
- package/extensions/execution-evidence.ts +3 -0
- package/extensions/fanout-outcome.ts +1 -0
- package/extensions/grants.ts +2 -3
- package/extensions/run-delegation.ts +58 -3
- package/extensions/runtime-snapshot.ts +59 -1
- package/package.json +1 -1
- package/src/executors/child-final.ts +17 -1
- package/src/governance/auto-mode-policy.ts +27 -1
- package/src/governance/dispatch-operation-client.ts +96 -0
- package/src/governance/dispatch-operations.ts +81 -0
- package/src/governance/execution-retention.ts +7 -3
- package/src/governance/native-session.ts +28 -1
- package/src/governance/retention-contract.ts +9 -4
- package/src/kernel/auto-mode.ts +1 -0
- package/src/kernel/chain.ts +1 -0
- package/src/kernel/delegate.ts +14 -0
- package/src/kernel/dispatch-operation.ts +38 -0
- package/src/kernel/propagation.ts +18 -1
- package/src/products/activity-timeline.ts +10 -0
- package/src/products/dashboard-screen.ts +21 -2
|
@@ -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
|
+
}
|
package/extensions/delegation.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
386
|
-
capacityReservation
|
|
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
|
|
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
|
|
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
|
-
|
|
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}` : ""}`;
|
package/extensions/grants.ts
CHANGED
|
@@ -365,12 +365,11 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
|
|
|
365
365
|
return undefined;
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
-
// Observe
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -5,6 +5,7 @@ import { openRuntimeSettlement, settlementHash } from "../src/governance/runtime
|
|
|
5
5
|
import { reconcileDelegationCapacity } from "./session-capacity.ts";
|
|
6
6
|
import { agentDir } from "../src/kernel/project-paths.ts";
|
|
7
7
|
import type { GrantsSession } from "./session.ts";
|
|
8
|
+
export const OPERATION_STATUS_EVENT = "pi-daddy:operation-status:v1";
|
|
8
9
|
export const RUNTIME_SNAPSHOT_EVENT = "pi-daddy:runtime-snapshot:v1";
|
|
9
10
|
export function registerRuntimeSnapshot(
|
|
10
11
|
pi: ExtensionAPI,
|
|
@@ -15,7 +16,7 @@ export function registerRuntimeSnapshot(
|
|
|
15
16
|
let bound: { sessionId: string; cwd: string; lifecycle: object } | undefined, problem: string | undefined;
|
|
16
17
|
const generation = {};
|
|
17
18
|
let bindSequence = 0;
|
|
18
|
-
const
|
|
19
|
+
const unsubscribeRuntime = pi.events?.on?.(RUNTIME_SNAPSHOT_EVENT, (input: unknown) => {
|
|
19
20
|
const request = input as {
|
|
20
21
|
version?: number;
|
|
21
22
|
requestId?: string;
|
|
@@ -91,6 +92,63 @@ export function registerRuntimeSnapshot(
|
|
|
91
92
|
}
|
|
92
93
|
})().catch(() => undefined);
|
|
93
94
|
});
|
|
95
|
+
const unsubscribeOperations = pi.events?.on?.(OPERATION_STATUS_EVENT, (input: unknown) => {
|
|
96
|
+
const request = input as {
|
|
97
|
+
version?: number;
|
|
98
|
+
requestId?: string;
|
|
99
|
+
sessionId?: string;
|
|
100
|
+
cwd?: string;
|
|
101
|
+
operationId?: string;
|
|
102
|
+
reply?: (value: unknown) => void;
|
|
103
|
+
};
|
|
104
|
+
if (
|
|
105
|
+
request?.version !== 1 ||
|
|
106
|
+
typeof request.requestId !== "string" ||
|
|
107
|
+
request.requestId.length > 512 ||
|
|
108
|
+
typeof request.operationId !== "string" ||
|
|
109
|
+
typeof request.reply !== "function"
|
|
110
|
+
)
|
|
111
|
+
return;
|
|
112
|
+
if (bound && session.reloadLifecycle.runtimeSnapshotGeneration !== generation) return;
|
|
113
|
+
void (async () => {
|
|
114
|
+
const current = bound,
|
|
115
|
+
lifecycle = session.reloadLifecycle;
|
|
116
|
+
const envelope = {
|
|
117
|
+
version: 1,
|
|
118
|
+
requestId: request.requestId,
|
|
119
|
+
sessionId: current?.sessionId ?? "",
|
|
120
|
+
cwd: current?.cwd ?? "",
|
|
121
|
+
operationId: request.operationId,
|
|
122
|
+
};
|
|
123
|
+
if (
|
|
124
|
+
!current ||
|
|
125
|
+
current.lifecycle !== lifecycle ||
|
|
126
|
+
request.sessionId !== current.sessionId ||
|
|
127
|
+
request.cwd !== current.cwd ||
|
|
128
|
+
!session.ownerBound ||
|
|
129
|
+
!session.autoMode?.operations
|
|
130
|
+
) {
|
|
131
|
+
request.reply!({ ...envelope, qualified: false, operation: null, reason: "operation owner is not ready" });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const operation = await session.autoMode.operations.read(request.operationId!);
|
|
136
|
+
if (
|
|
137
|
+
bound !== current ||
|
|
138
|
+
session.reloadLifecycle !== lifecycle ||
|
|
139
|
+
lifecycle.runtimeSnapshotGeneration !== generation
|
|
140
|
+
)
|
|
141
|
+
throw Error("operation owner changed during read");
|
|
142
|
+
request.reply!({ ...envelope, qualified: true, operation });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
request.reply!({ ...envelope, qualified: false, operation: null, reason: String(error) });
|
|
145
|
+
}
|
|
146
|
+
})().catch(() => undefined);
|
|
147
|
+
});
|
|
148
|
+
const unsubscribe = () => {
|
|
149
|
+
unsubscribeRuntime?.();
|
|
150
|
+
unsubscribeOperations?.();
|
|
151
|
+
};
|
|
94
152
|
return {
|
|
95
153
|
async bind(ctx) {
|
|
96
154
|
const sequence = ++bindSequence,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -4,6 +4,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readdir } from "node:fs/promises";
|
|
6
6
|
import { SessionManager, type FileEntry } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { CapturedSessionBranch } from "../governance/native-session.ts";
|
|
7
8
|
import { BoundedReadCleanupError, readBoundedBytes } from "../kernel/bounded-read.ts";
|
|
8
9
|
const MAX_FINAL_BYTES = 4 * 1024 * 1024,
|
|
9
10
|
MAX_LINE_BYTES = 32 * 1024 * 1024,
|
|
@@ -13,6 +14,11 @@ const hash = (s: string) => createHash("sha256").update(s, "utf8").digest("hex")
|
|
|
13
14
|
export type ChildFinal =
|
|
14
15
|
| { state: "complete"; text: string; sessionId: string; messageId: string; leafId: string; sha256: string }
|
|
15
16
|
| { state: "unavailable"; reason: string; diagnosticText?: string };
|
|
17
|
+
const capturedSessions = new WeakMap<ChildFinal, Readonly<CapturedSessionBranch>>();
|
|
18
|
+
/** Only the original in-process final carries this observation; JSON/copies do not recreate it. */
|
|
19
|
+
export function capturedFinalSession(final: ChildFinal): Readonly<CapturedSessionBranch> | undefined {
|
|
20
|
+
return capturedSessions.get(final);
|
|
21
|
+
}
|
|
16
22
|
export class ChildFinalCapture {
|
|
17
23
|
private decoder = new TextDecoder("utf-8", { fatal: true });
|
|
18
24
|
private buffer = "";
|
|
@@ -171,7 +177,7 @@ export class ChildFinalCapture {
|
|
|
171
177
|
!isDeepStrictEqual(currentUser.message, this.user)
|
|
172
178
|
)
|
|
173
179
|
throw Error("stream final does not match the current persisted branch and turn");
|
|
174
|
-
|
|
180
|
+
const complete: ChildFinal = {
|
|
175
181
|
state: "complete",
|
|
176
182
|
text,
|
|
177
183
|
sessionId: this.sessionId,
|
|
@@ -179,6 +185,16 @@ export class ChildFinalCapture {
|
|
|
179
185
|
leafId: manager.getLeafId()!,
|
|
180
186
|
sha256: hash(text),
|
|
181
187
|
};
|
|
188
|
+
capturedSessions.set(
|
|
189
|
+
complete,
|
|
190
|
+
Object.freeze({
|
|
191
|
+
path,
|
|
192
|
+
sessionId: this.sessionId,
|
|
193
|
+
leafId: complete.leafId,
|
|
194
|
+
sha256: createHash("sha256").update(read.bytes).digest("hex"),
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
return complete;
|
|
182
198
|
} catch (error) {
|
|
183
199
|
if (error instanceof BoundedReadCleanupError) {
|
|
184
200
|
if (!this.onReadCleanup) throw error;
|