pi-daddy 0.19.0 → 0.20.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 +65 -0
- package/README.md +105 -36
- package/contracts/ledger/v3/README.md +36 -0
- package/contracts/ledger/v3/fixtures/capability-decision.json +98 -0
- package/contracts/ledger/v3/fixtures/check-receipt.json +42 -0
- package/contracts/ledger/v3/fixtures/child-lifecycle.json +45 -0
- package/contracts/ledger/v3/fixtures/workflow-fact.json +41 -0
- package/contracts/ledger/v3/fixtures/workspace-lease.json +43 -0
- package/contracts/ledger/v3/ledger-event.schema.json +930 -0
- package/dist/check-runner.d.ts.map +1 -1
- package/dist/check-runner.js +14 -0
- package/dist/check-runner.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/correlation.d.ts.map +1 -1
- package/dist/correlation.js +10 -0
- package/dist/correlation.js.map +1 -1
- package/dist/dashboard-cli.d.ts +18 -0
- package/dist/dashboard-cli.d.ts.map +1 -0
- package/dist/dashboard-cli.js +154 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-handshake.d.ts +37 -0
- package/dist/dashboard-handshake.d.ts.map +1 -0
- package/dist/dashboard-handshake.js +127 -0
- package/dist/dashboard-handshake.js.map +1 -0
- package/dist/dashboard-herdr.d.ts +54 -0
- package/dist/dashboard-herdr.d.ts.map +1 -0
- package/dist/dashboard-herdr.js +286 -0
- package/dist/dashboard-herdr.js.map +1 -0
- package/dist/dashboard-projection.d.ts +73 -0
- package/dist/dashboard-projection.d.ts.map +1 -0
- package/dist/dashboard-projection.js +294 -0
- package/dist/dashboard-projection.js.map +1 -0
- package/dist/dashboard-render.d.ts +10 -0
- package/dist/dashboard-render.d.ts.map +1 -0
- package/dist/dashboard-render.js +208 -0
- package/dist/dashboard-render.js.map +1 -0
- package/dist/delegate-types.d.ts +6 -2
- package/dist/delegate-types.d.ts.map +1 -1
- package/dist/delegate-types.js.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +4 -1
- package/dist/delegate.js.map +1 -1
- package/dist/execution-id.d.ts +6 -0
- package/dist/execution-id.d.ts.map +1 -0
- package/dist/execution-id.js +13 -0
- package/dist/execution-id.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/ledger-events.d.ts +27 -4
- package/dist/ledger-events.d.ts.map +1 -1
- package/dist/ledger-events.js +30 -9
- package/dist/ledger-events.js.map +1 -1
- package/dist/ledger-identifiers.d.ts +7 -0
- package/dist/ledger-identifiers.d.ts.map +1 -0
- package/dist/ledger-identifiers.js +20 -0
- package/dist/ledger-identifiers.js.map +1 -0
- package/dist/ledger-report.d.ts +5 -2
- package/dist/ledger-report.d.ts.map +1 -1
- package/dist/ledger-report.js +38 -14
- package/dist/ledger-report.js.map +1 -1
- package/dist/ledger-v3-validation.d.ts +11 -0
- package/dist/ledger-v3-validation.d.ts.map +1 -0
- package/dist/ledger-v3-validation.js +265 -0
- package/dist/ledger-v3-validation.js.map +1 -0
- package/dist/ledger.d.ts +18 -5
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +29 -7
- package/dist/ledger.js.map +1 -1
- package/dist/propagation.d.ts +3 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +3 -0
- package/dist/propagation.js.map +1 -1
- package/dist/run-child.d.ts +3 -1
- package/dist/run-child.d.ts.map +1 -1
- package/dist/run-child.js +30 -2
- package/dist/run-child.js.map +1 -1
- package/dist/run-herdr.d.ts +2 -0
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +8 -0
- package/dist/run-herdr.js.map +1 -1
- package/dist/workflow-fact-id.d.ts +4 -0
- package/dist/workflow-fact-id.d.ts.map +1 -0
- package/dist/workflow-fact-id.js +14 -0
- package/dist/workflow-fact-id.js.map +1 -0
- package/dist/workflow-facts.d.ts +34 -0
- package/dist/workflow-facts.d.ts.map +1 -0
- package/dist/workflow-facts.js +43 -0
- package/dist/workflow-facts.js.map +1 -0
- package/extensions/chain-ledger.ts +4 -0
- package/extensions/chain-plan.ts +98 -0
- package/extensions/delegate-chain.ts +63 -122
- package/extensions/delegation-ledger.ts +60 -0
- package/extensions/delegation.ts +13 -13
- package/extensions/execute-child.ts +85 -21
- package/extensions/execution-occurrence.ts +20 -0
- package/extensions/grants-command.ts +22 -3
- package/extensions/grants.ts +44 -0
- package/extensions/run-delegation.ts +36 -56
- package/extensions/session.ts +7 -2
- package/extensions/workspace-runtime.ts +10 -0
- package/herdr-plugin/herdr-plugin.toml +18 -0
- package/package.json +16 -4
- package/src/check-runner.ts +16 -0
- package/src/correlation.ts +11 -0
- package/src/dashboard-cli.ts +171 -0
- package/src/dashboard-handshake.ts +185 -0
- package/src/dashboard-herdr.ts +355 -0
- package/src/dashboard-projection.ts +390 -0
- package/src/dashboard-render.ts +254 -0
- package/src/delegate-types.ts +6 -2
- package/src/delegate.ts +4 -2
- package/src/execution-id.ts +18 -0
- package/src/index.ts +25 -0
- package/src/ledger-events.ts +54 -10
- package/src/ledger-identifiers.ts +21 -0
- package/src/ledger-report.ts +40 -19
- package/src/ledger-v3-validation.ts +266 -0
- package/src/ledger.ts +49 -10
- package/src/propagation.ts +3 -0
- package/src/run-child.ts +35 -3
- package/src/run-herdr.ts +9 -0
- package/src/workflow-fact-id.ts +16 -0
- package/src/workflow-facts.ts +65 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Compile } from "typebox/compile";
|
|
3
|
+
import type { CorrelationMetadata } from "./correlation.ts";
|
|
4
|
+
import {
|
|
5
|
+
isLedgerObject as object,
|
|
6
|
+
isNonEmptyString as nonEmpty,
|
|
7
|
+
isStringArray as strings,
|
|
8
|
+
isTimestamp as timestamp,
|
|
9
|
+
validateLedgerV3Event as validateV3,
|
|
10
|
+
type LedgerV3Object as ObjectRecord,
|
|
11
|
+
} from "./ledger-v3-validation.ts";
|
|
12
|
+
import { isLedgerCapabilityIdentifier, isLedgerDisplayIdentifier } from "./ledger-identifiers.ts";
|
|
13
|
+
|
|
14
|
+
export type DashboardState =
|
|
15
|
+
| "authorised"
|
|
16
|
+
| "starting"
|
|
17
|
+
| "running"
|
|
18
|
+
| "completed"
|
|
19
|
+
| "failed"
|
|
20
|
+
| "refused"
|
|
21
|
+
| "incomplete"
|
|
22
|
+
| "historical";
|
|
23
|
+
|
|
24
|
+
export interface DashboardRuntimeIdentity {
|
|
25
|
+
herdrPaneId?: string;
|
|
26
|
+
herdrAgentName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DashboardNode {
|
|
30
|
+
executionId: string;
|
|
31
|
+
parentExecutionId: string | null;
|
|
32
|
+
logicalParentId?: string;
|
|
33
|
+
logicalChildId: string;
|
|
34
|
+
depth: number;
|
|
35
|
+
agentName: string;
|
|
36
|
+
state: DashboardState;
|
|
37
|
+
provenance: "pi-daddy-enforced" | "historical-unjoinable";
|
|
38
|
+
startedAt: string;
|
|
39
|
+
endedAt?: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
durationMs: number;
|
|
42
|
+
effectiveGrant: string[];
|
|
43
|
+
denied: string[];
|
|
44
|
+
executor?: "process" | "herdr";
|
|
45
|
+
workspace?: { id: string; access: "read" | "write"; root: string };
|
|
46
|
+
runtime?: DashboardRuntimeIdentity;
|
|
47
|
+
correlation?: CorrelationMetadata;
|
|
48
|
+
refusal?: { code?: string; message?: string };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DashboardWorkflowFact {
|
|
52
|
+
factId: string;
|
|
53
|
+
source: string;
|
|
54
|
+
provenance: "planned" | "observed" | "controller-validated";
|
|
55
|
+
kind: "workflow_phase" | "inline_skill" | "transition";
|
|
56
|
+
subject: string;
|
|
57
|
+
state: "pending" | "observed" | "started" | "completed" | "blocked";
|
|
58
|
+
runId: string;
|
|
59
|
+
correlation: CorrelationMetadata;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DashboardWorkflow {
|
|
63
|
+
runId: string;
|
|
64
|
+
label: string;
|
|
65
|
+
assurance?: string;
|
|
66
|
+
phases: string[];
|
|
67
|
+
/** Correlation is supplied by a caller. It is a label, never controller validation or enforcement. */
|
|
68
|
+
provenance: "caller-declared";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DashboardCorruptLine {
|
|
72
|
+
line: number;
|
|
73
|
+
/** Raw corrupt content is deliberately excluded: it may contain forbidden task/output text. */
|
|
74
|
+
reason: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DashboardProjection {
|
|
78
|
+
nodes: DashboardNode[];
|
|
79
|
+
workflows: DashboardWorkflow[];
|
|
80
|
+
workflowFacts: DashboardWorkflowFact[];
|
|
81
|
+
corrupt: DashboardCorruptLine[];
|
|
82
|
+
orphanEvents: number;
|
|
83
|
+
active: number;
|
|
84
|
+
maxDepth: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface DashboardProjectionOptions {
|
|
88
|
+
now?: Date;
|
|
89
|
+
/** A decision with no start is briefly authorised, then incomplete. */
|
|
90
|
+
startGraceMs?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const START_GRACE_MS = 5_000;
|
|
94
|
+
const EXECUTORS = new Set(["process", "herdr"]);
|
|
95
|
+
// The artifact is frozen, so projection can validate the exact published v2 contract rather than grow a
|
|
96
|
+
// second partial reader. `createRequire` resolves from both src/ in tests and dist/ in the installed package.
|
|
97
|
+
const LEDGER_V2 = Compile(createRequire(import.meta.url)("../contracts/ledger/v2/ledger-event.schema.json"));
|
|
98
|
+
const TERMINAL_STATES = new Set(["completed", "failed"]);
|
|
99
|
+
|
|
100
|
+
interface Occurrence {
|
|
101
|
+
executionId: string;
|
|
102
|
+
parentExecutionId: string | null;
|
|
103
|
+
childId: string;
|
|
104
|
+
firstLine: number;
|
|
105
|
+
firstTs: string;
|
|
106
|
+
lastTs: string;
|
|
107
|
+
decision?: ObjectRecord;
|
|
108
|
+
lifecycle: ObjectRecord[];
|
|
109
|
+
leases: ObjectRecord[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const DISPLAYED_CORRELATION_FIELDS = [
|
|
113
|
+
"run_id", "task_id", "phase", "assurance", "assurance_effective", "policy_label",
|
|
114
|
+
] as const satisfies ReadonlyArray<keyof CorrelationMetadata>;
|
|
115
|
+
|
|
116
|
+
function correlationOf(value: unknown): CorrelationMetadata | undefined {
|
|
117
|
+
if (!object(value)) return undefined;
|
|
118
|
+
return DISPLAYED_CORRELATION_FIELDS.every(
|
|
119
|
+
(field) => value[field] === undefined || isLedgerDisplayIdentifier(value[field]),
|
|
120
|
+
) ? value as CorrelationMetadata : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function legacyNode(event: ObjectRecord, line: number, now: Date): DashboardNode | null {
|
|
124
|
+
if (event.event !== undefined && event.event !== "capability_decision") return null;
|
|
125
|
+
if (!strings(event.denied)) return null;
|
|
126
|
+
const ts = timestamp(event.ts) ? event.ts : now.toISOString();
|
|
127
|
+
return {
|
|
128
|
+
executionId: `legacy-line:${line}`,
|
|
129
|
+
parentExecutionId: null,
|
|
130
|
+
logicalParentId: nonEmpty(event.parentId) ? event.parentId : undefined,
|
|
131
|
+
logicalChildId: nonEmpty(event.childId) ? event.childId : `legacy:${line}`,
|
|
132
|
+
depth: Number.isInteger(event.depth) ? event.depth as number : 0,
|
|
133
|
+
agentName: isLedgerDisplayIdentifier(event.agentType) ? event.agentType : "historical-delegation",
|
|
134
|
+
state: event.blocked === true ? "refused" : "historical",
|
|
135
|
+
provenance: "historical-unjoinable",
|
|
136
|
+
startedAt: ts,
|
|
137
|
+
updatedAt: ts,
|
|
138
|
+
durationMs: 0,
|
|
139
|
+
effectiveGrant: strings(event.effective) ? event.effective.filter(isLedgerCapabilityIdentifier) : [],
|
|
140
|
+
denied: event.denied.filter(isLedgerCapabilityIdentifier),
|
|
141
|
+
executor: EXECUTORS.has(String(event.executor)) ? event.executor as "process" | "herdr" : undefined,
|
|
142
|
+
correlation: correlationOf(event.correlation),
|
|
143
|
+
refusal: object(event.refusal) ? event.refusal as DashboardNode["refusal"] : undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stateOf(occurrence: Occurrence, nowMs: number, graceMs: number): DashboardState {
|
|
148
|
+
if (occurrence.decision?.blocked === true) return "refused";
|
|
149
|
+
const latest = occurrence.lifecycle.at(-1);
|
|
150
|
+
if (latest) {
|
|
151
|
+
const state = String(latest.state);
|
|
152
|
+
if (TERMINAL_STATES.has(state)) return state as "completed" | "failed";
|
|
153
|
+
const deadline = timestamp(latest.deadlineAt) ? Date.parse(latest.deadlineAt) : Number.POSITIVE_INFINITY;
|
|
154
|
+
return nowMs > deadline ? "incomplete" : state as "starting" | "running";
|
|
155
|
+
}
|
|
156
|
+
const age = nowMs - Date.parse(occurrence.firstTs);
|
|
157
|
+
return occurrence.decision ? (age <= graceMs ? "authorised" : "incomplete") : "incomplete";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nodeOf(occurrence: Occurrence, now: Date, graceMs: number): DashboardNode {
|
|
161
|
+
const decision = occurrence.decision;
|
|
162
|
+
const latestLifecycle = occurrence.lifecycle.at(-1);
|
|
163
|
+
const firstLifecycle = occurrence.lifecycle[0];
|
|
164
|
+
const lease = occurrence.leases.findLast((event) => ["acquired", "uncontended", "recovered"].includes(String(event.outcome)));
|
|
165
|
+
const start = timestamp(firstLifecycle?.ts) ? firstLifecycle.ts : occurrence.firstTs;
|
|
166
|
+
const terminal = latestLifecycle && TERMINAL_STATES.has(String(latestLifecycle.state)) ? latestLifecycle : undefined;
|
|
167
|
+
const state = stateOf(occurrence, now.getTime(), graceMs);
|
|
168
|
+
const deadline = timestamp(latestLifecycle?.deadlineAt)
|
|
169
|
+
? Date.parse(latestLifecycle.deadlineAt)
|
|
170
|
+
: latestLifecycle ? undefined : Date.parse(occurrence.firstTs) + graceMs;
|
|
171
|
+
const durationEnd = state === "refused"
|
|
172
|
+
? Date.parse(start)
|
|
173
|
+
: terminal ? Date.parse(String(terminal.ts)) : state === "incomplete" && deadline ? deadline : now.getTime();
|
|
174
|
+
const runtimeEvent = occurrence.lifecycle.findLast(
|
|
175
|
+
(event) => nonEmpty(event.herdrPaneId) || nonEmpty(event.herdrAgentName),
|
|
176
|
+
);
|
|
177
|
+
const runtime = runtimeEvent
|
|
178
|
+
? {
|
|
179
|
+
...(nonEmpty(runtimeEvent.herdrPaneId) ? { herdrPaneId: runtimeEvent.herdrPaneId } : {}),
|
|
180
|
+
...(nonEmpty(runtimeEvent.herdrAgentName) ? { herdrAgentName: runtimeEvent.herdrAgentName } : {}),
|
|
181
|
+
}
|
|
182
|
+
: undefined;
|
|
183
|
+
const correlation = correlationOf(decision?.correlation)
|
|
184
|
+
?? correlationOf(latestLifecycle?.correlation)
|
|
185
|
+
?? correlationOf(lease?.correlation);
|
|
186
|
+
const logicalParentId = decision && nonEmpty(decision.parentId) ? decision.parentId : undefined;
|
|
187
|
+
const agentName = decision && nonEmpty(decision.agentType) ? decision.agentType : "governed execution";
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
executionId: occurrence.executionId,
|
|
191
|
+
parentExecutionId: occurrence.parentExecutionId,
|
|
192
|
+
logicalParentId,
|
|
193
|
+
logicalChildId: occurrence.childId,
|
|
194
|
+
depth: Number.isInteger(decision?.depth) ? decision!.depth as number : 0,
|
|
195
|
+
agentName,
|
|
196
|
+
state,
|
|
197
|
+
provenance: "pi-daddy-enforced",
|
|
198
|
+
startedAt: start,
|
|
199
|
+
...(terminal ? { endedAt: String(terminal.ts) } : {}),
|
|
200
|
+
updatedAt: occurrence.lastTs,
|
|
201
|
+
durationMs: Math.max(0, durationEnd - Date.parse(start)),
|
|
202
|
+
effectiveGrant: strings(decision?.effective) ? decision.effective : [],
|
|
203
|
+
denied: strings(decision?.denied) ? decision.denied : [],
|
|
204
|
+
executor: EXECUTORS.has(String(decision?.executor ?? latestLifecycle?.executor))
|
|
205
|
+
? (decision?.executor ?? latestLifecycle?.executor) as "process" | "herdr"
|
|
206
|
+
: undefined,
|
|
207
|
+
...(lease ? { workspace: { id: String(lease.workspaceId), access: lease.access as "read" | "write", root: String(lease.root) } } : {}),
|
|
208
|
+
...(runtime ? { runtime } : {}),
|
|
209
|
+
...(correlation ? { correlation } : {}),
|
|
210
|
+
...(object(decision?.refusal) ? { refusal: decision.refusal as DashboardNode["refusal"] } : {}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function executionParentCycles(occurrences: Map<string, Occurrence>): Set<string> {
|
|
215
|
+
const cyclic = new Set<string>();
|
|
216
|
+
const complete = new Set<string>();
|
|
217
|
+
for (const start of occurrences.keys()) {
|
|
218
|
+
if (complete.has(start)) continue;
|
|
219
|
+
const path: string[] = [];
|
|
220
|
+
const positions = new Map<string, number>();
|
|
221
|
+
let current: string | null = start;
|
|
222
|
+
while (current && occurrences.has(current) && !complete.has(current)) {
|
|
223
|
+
const position = positions.get(current);
|
|
224
|
+
if (position !== undefined) {
|
|
225
|
+
for (const id of path.slice(position)) cyclic.add(id);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
positions.set(current, path.length);
|
|
229
|
+
path.push(current);
|
|
230
|
+
current = occurrences.get(current)?.parentExecutionId ?? null;
|
|
231
|
+
}
|
|
232
|
+
for (const id of path) complete.add(id);
|
|
233
|
+
}
|
|
234
|
+
return cyclic;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function workflowsOf(nodes: DashboardNode[], facts: DashboardWorkflowFact[]): DashboardWorkflow[] {
|
|
238
|
+
const groups = new Map<string, DashboardWorkflow>();
|
|
239
|
+
const correlations = [
|
|
240
|
+
...nodes.map((node) => node.correlation).filter((value): value is CorrelationMetadata => Boolean(value)),
|
|
241
|
+
...facts.map((fact) => fact.correlation),
|
|
242
|
+
];
|
|
243
|
+
for (const correlation of correlations) {
|
|
244
|
+
const runId = correlation.run_id;
|
|
245
|
+
if (!runId) continue;
|
|
246
|
+
const existing = groups.get(runId);
|
|
247
|
+
const phase = correlation.phase;
|
|
248
|
+
if (existing) {
|
|
249
|
+
if (phase && !existing.phases.includes(phase)) existing.phases.push(phase);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
groups.set(runId, {
|
|
253
|
+
runId,
|
|
254
|
+
label: correlation.policy_label || runId,
|
|
255
|
+
...(correlation.assurance_effective || correlation.assurance
|
|
256
|
+
? { assurance: correlation.assurance_effective || correlation.assurance }
|
|
257
|
+
: {}),
|
|
258
|
+
phases: phase ? [phase] : [],
|
|
259
|
+
provenance: "caller-declared",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return [...groups.values()]
|
|
263
|
+
.map((workflow) => ({ ...workflow, phases: [...workflow.phases].sort() }))
|
|
264
|
+
.sort((left, right) => left.runId.localeCompare(right.runId));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function parseDashboardLedger(text: string, options: DashboardProjectionOptions = {}): DashboardProjection {
|
|
268
|
+
const now = options.now ?? new Date();
|
|
269
|
+
const graceMs = options.startGraceMs ?? START_GRACE_MS;
|
|
270
|
+
const corrupt: DashboardCorruptLine[] = [];
|
|
271
|
+
const occurrences = new Map<string, Occurrence>();
|
|
272
|
+
const historical: DashboardNode[] = [];
|
|
273
|
+
const workflowFacts: DashboardWorkflowFact[] = [];
|
|
274
|
+
let orphanEvents = 0;
|
|
275
|
+
|
|
276
|
+
text.split("\n").forEach((raw, index) => {
|
|
277
|
+
if (raw.trim() === "") return;
|
|
278
|
+
const line = index + 1;
|
|
279
|
+
let event: ObjectRecord;
|
|
280
|
+
try {
|
|
281
|
+
const parsed: unknown = JSON.parse(raw);
|
|
282
|
+
if (!object(parsed)) throw new Error("line is not a JSON object");
|
|
283
|
+
event = parsed;
|
|
284
|
+
} catch {
|
|
285
|
+
// JSON.parse diagnostics may quote the offending input. A foreign/torn line can contain exactly the
|
|
286
|
+
// task or output text this surface promises never to render, so only the failure class crosses here.
|
|
287
|
+
corrupt.push({ line, reason: "invalid JSON" });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (event.ledgerVersion === undefined && event.event === undefined) {
|
|
292
|
+
const node = legacyNode(event, line, now);
|
|
293
|
+
if (node) historical.push(node);
|
|
294
|
+
else corrupt.push({ line, reason: "invalid legacy grant record" });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (event.ledgerVersion === 2) {
|
|
298
|
+
if (!LEDGER_V2.Check(event)) {
|
|
299
|
+
corrupt.push({ line, reason: "invalid ledger v2 event" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const node = legacyNode(event, line, now);
|
|
303
|
+
if (node) historical.push(node);
|
|
304
|
+
else if (["workspace_lease", "child_lifecycle", "check_receipt"].includes(String(event.event))) orphanEvents += 1;
|
|
305
|
+
else corrupt.push({ line, reason: "invalid ledger v2 event" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const invalid = validateV3(event);
|
|
310
|
+
if (invalid) {
|
|
311
|
+
corrupt.push({ line, reason: invalid });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (event.event === "workflow_fact") {
|
|
315
|
+
const correlation = event.correlation as CorrelationMetadata & { run_id: string };
|
|
316
|
+
workflowFacts.push({
|
|
317
|
+
factId: event.factId as string,
|
|
318
|
+
source: event.source as string,
|
|
319
|
+
provenance: event.provenance === "controller_validated" ? "controller-validated" : event.provenance as "planned" | "observed",
|
|
320
|
+
kind: event.kind as DashboardWorkflowFact["kind"],
|
|
321
|
+
subject: event.subject as string,
|
|
322
|
+
state: event.state as DashboardWorkflowFact["state"],
|
|
323
|
+
runId: correlation.run_id,
|
|
324
|
+
correlation,
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const executionId = event.executionId as string;
|
|
329
|
+
const parentExecutionId = event.parentExecutionId as string | null;
|
|
330
|
+
const childId = event.childId as string;
|
|
331
|
+
// Named checks have their own receipt identity and are not delegated-agent tree nodes.
|
|
332
|
+
if (event.event === "check_receipt" || childId.startsWith("check:")) return;
|
|
333
|
+
const seen = occurrences.get(executionId);
|
|
334
|
+
if (seen && (seen.parentExecutionId !== parentExecutionId || seen.childId !== childId)) {
|
|
335
|
+
corrupt.push({ line, reason: "execution identity changed parent or logical child" });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const occurrence = seen ?? {
|
|
339
|
+
executionId, parentExecutionId, childId, firstLine: line,
|
|
340
|
+
firstTs: event.ts as string, lastTs: event.ts as string,
|
|
341
|
+
lifecycle: [], leases: [],
|
|
342
|
+
};
|
|
343
|
+
if (event.event === "capability_decision" && occurrence.decision) {
|
|
344
|
+
corrupt.push({ line, reason: "duplicate capability decision for one executionId" });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
occurrence.lastTs = event.ts as string;
|
|
348
|
+
if (event.event === "capability_decision") occurrence.decision = event;
|
|
349
|
+
else if (event.event === "child_lifecycle") {
|
|
350
|
+
const previous = occurrence.lifecycle.at(-1);
|
|
351
|
+
if (previous && TERMINAL_STATES.has(String(previous.state))) {
|
|
352
|
+
corrupt.push({ line, reason: "lifecycle event appeared after terminal lifecycle state" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (previous && event.state === "starting") {
|
|
356
|
+
corrupt.push({ line, reason: "duplicate starting lifecycle event" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const recordedDeadline = occurrence.lifecycle.find((entry) => timestamp(entry.deadlineAt))?.deadlineAt;
|
|
360
|
+
if (recordedDeadline !== undefined && timestamp(event.deadlineAt) && event.deadlineAt !== recordedDeadline) {
|
|
361
|
+
corrupt.push({ line, reason: "lifecycle deadline changed within one execution" });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
occurrence.lifecycle.push(event);
|
|
365
|
+
} else if (event.event === "workspace_lease") occurrence.leases.push(event);
|
|
366
|
+
occurrences.set(executionId, occurrence);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const cycles = executionParentCycles(occurrences);
|
|
370
|
+
for (const executionId of cycles) {
|
|
371
|
+
const occurrence = occurrences.get(executionId)!;
|
|
372
|
+
corrupt.push({ line: occurrence.firstLine, reason: "execution parent cycle" });
|
|
373
|
+
}
|
|
374
|
+
const current = [...occurrences.values()]
|
|
375
|
+
.filter((occurrence) => !cycles.has(occurrence.executionId))
|
|
376
|
+
.map((occurrence) => nodeOf(occurrence, now, graceMs));
|
|
377
|
+
const nodes = [...current, ...historical].sort(
|
|
378
|
+
(left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt) || left.executionId.localeCompare(right.executionId),
|
|
379
|
+
);
|
|
380
|
+
const active = nodes.filter((node) => ["authorised", "starting", "running"].includes(node.state)).length;
|
|
381
|
+
return {
|
|
382
|
+
nodes,
|
|
383
|
+
workflows: workflowsOf(nodes, workflowFacts),
|
|
384
|
+
workflowFacts,
|
|
385
|
+
corrupt,
|
|
386
|
+
orphanEvents,
|
|
387
|
+
active,
|
|
388
|
+
maxDepth: nodes.reduce((max, node) => Math.max(max, node.depth), 0),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DashboardNode,
|
|
3
|
+
DashboardProjection,
|
|
4
|
+
DashboardState,
|
|
5
|
+
DashboardWorkflow,
|
|
6
|
+
DashboardWorkflowFact,
|
|
7
|
+
} from "./dashboard-projection.ts";
|
|
8
|
+
import { isLedgerCapabilityIdentifier, isLedgerDisplayIdentifier } from "./ledger-identifiers.ts";
|
|
9
|
+
|
|
10
|
+
export interface DashboardRenderOptions {
|
|
11
|
+
width?: number;
|
|
12
|
+
color?: boolean;
|
|
13
|
+
details?: boolean;
|
|
14
|
+
completedRoots?: number;
|
|
15
|
+
completedChildren?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ACTIVE = new Set<DashboardState>(["authorised", "starting", "running"]);
|
|
19
|
+
const ATTENTION = new Set<DashboardState>(["failed", "refused", "incomplete"]);
|
|
20
|
+
|
|
21
|
+
const STYLE: Record<DashboardState, { symbol: string; color: number; label: string }> = {
|
|
22
|
+
authorised: { symbol: "●", color: 33, label: "authorised" },
|
|
23
|
+
starting: { symbol: "●", color: 33, label: "starting" },
|
|
24
|
+
running: { symbol: "●", color: 33, label: "running" },
|
|
25
|
+
completed: { symbol: "✓", color: 32, label: "done" },
|
|
26
|
+
failed: { symbol: "✕", color: 31, label: "failed" },
|
|
27
|
+
refused: { symbol: "⛔", color: 31, label: "refused" },
|
|
28
|
+
incomplete: { symbol: "○", color: 90, label: "incomplete" },
|
|
29
|
+
historical: { symbol: "○", color: 90, label: "historical" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function clean(value: string | undefined): string {
|
|
33
|
+
return (value ?? "").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const displayIdentifier = (value: string | undefined): string =>
|
|
37
|
+
isLedgerDisplayIdentifier(value) ? value : "";
|
|
38
|
+
|
|
39
|
+
const ANSI_SGR = /\u001b\[[0-9;]*m/g;
|
|
40
|
+
const WIDE_CELL = /\p{Extended_Pictographic}|[\u2E80-\u9FFF\uF900-\uFAFF]/u;
|
|
41
|
+
|
|
42
|
+
function cellWidth(value: string): number {
|
|
43
|
+
return [...value.replace(ANSI_SGR, "")].reduce((sum, char) => sum + (WIDE_CELL.test(char) ? 2 : 1), 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function truncate(value: string, width: number): string {
|
|
47
|
+
if (cellWidth(value) <= width) return value;
|
|
48
|
+
const target = Math.max(0, width - 1);
|
|
49
|
+
let cells = 0;
|
|
50
|
+
let output = "";
|
|
51
|
+
let styled = false;
|
|
52
|
+
for (let index = 0; index < value.length;) {
|
|
53
|
+
if (value[index] === "\u001b") {
|
|
54
|
+
const match = /^\u001b\[([0-9;]*)m/.exec(value.slice(index));
|
|
55
|
+
if (match) {
|
|
56
|
+
output += match[0];
|
|
57
|
+
styled = match[1] !== "0" && match[1] !== "";
|
|
58
|
+
index += match[0].length;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const codePoint = value.codePointAt(index);
|
|
63
|
+
if (codePoint === undefined) break;
|
|
64
|
+
const char = String.fromCodePoint(codePoint);
|
|
65
|
+
const next = WIDE_CELL.test(char) ? 2 : 1;
|
|
66
|
+
if (cells + next > target) break;
|
|
67
|
+
output += char;
|
|
68
|
+
cells += next;
|
|
69
|
+
index += char.length;
|
|
70
|
+
}
|
|
71
|
+
output += "…";
|
|
72
|
+
return styled ? `${output}\u001b[0m` : output;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function paint(value: string, code: number, enabled: boolean): string {
|
|
76
|
+
return enabled ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function elapsed(ms: number): string {
|
|
80
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
81
|
+
const minutes = Math.floor(seconds / 60);
|
|
82
|
+
return `${minutes}:${String(seconds % 60).padStart(2, "0")}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function phaseLabel(node: DashboardNode): string {
|
|
86
|
+
const phase = displayIdentifier(node.correlation?.phase);
|
|
87
|
+
return phase ? phase.replaceAll("-", " ") : "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function workflowFactLine(fact: DashboardWorkflowFact, color: boolean): string {
|
|
91
|
+
const marker = fact.provenance === "planned" ? "P" : fact.provenance === "observed" ? "O" : "V";
|
|
92
|
+
const symbol = fact.provenance === "planned" ? "○" : fact.provenance === "observed" ? "◉" : fact.state === "blocked" ? "✕" : "✓";
|
|
93
|
+
const code = fact.provenance === "planned" ? 90 : fact.provenance === "observed" ? 33 : fact.state === "blocked" ? 31 : 32;
|
|
94
|
+
return `${marker} ${paint(symbol, code, color)} ${displayIdentifier(fact.subject) || "unlabelled"} ${fact.state}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function workflowHeader(workflow: DashboardWorkflow, color: boolean): string {
|
|
98
|
+
const assurance = displayIdentifier(workflow.assurance);
|
|
99
|
+
return paint(
|
|
100
|
+
`◆ ${displayIdentifier(workflow.label) || "declared-workflow"}${assurance ? ` · ${assurance}` : ""} · declared`,
|
|
101
|
+
36,
|
|
102
|
+
color,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface TreeIndex {
|
|
107
|
+
byId: Map<string, DashboardNode>;
|
|
108
|
+
children: Map<string, DashboardNode[]>;
|
|
109
|
+
roots: DashboardNode[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function treeIndex(nodes: DashboardNode[]): TreeIndex {
|
|
113
|
+
const byId = new Map(nodes.map((node) => [node.executionId, node]));
|
|
114
|
+
const children = new Map<string, DashboardNode[]>();
|
|
115
|
+
const roots: DashboardNode[] = [];
|
|
116
|
+
for (const node of nodes) {
|
|
117
|
+
if (node.parentExecutionId && byId.has(node.parentExecutionId)) {
|
|
118
|
+
const list = children.get(node.parentExecutionId) ?? [];
|
|
119
|
+
list.push(node);
|
|
120
|
+
children.set(node.parentExecutionId, list);
|
|
121
|
+
} else roots.push(node);
|
|
122
|
+
}
|
|
123
|
+
const sort = (left: DashboardNode, right: DashboardNode) =>
|
|
124
|
+
Date.parse(left.startedAt) - Date.parse(right.startedAt) || left.executionId.localeCompare(right.executionId);
|
|
125
|
+
roots.sort(sort);
|
|
126
|
+
for (const list of children.values()) list.sort(sort);
|
|
127
|
+
return { byId, children, roots };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasInterestingDescendant(node: DashboardNode, index: TreeIndex): boolean {
|
|
131
|
+
if (ACTIVE.has(node.state) || ATTENTION.has(node.state)) return true;
|
|
132
|
+
return (index.children.get(node.executionId) ?? []).some((child) => hasInterestingDescendant(child, index));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function selectedRoots(roots: DashboardNode[], index: TreeIndex, limit: number): DashboardNode[] {
|
|
136
|
+
const important = roots.filter((root) => hasInterestingDescendant(root, index));
|
|
137
|
+
const quiet = roots.filter((root) => !hasInterestingDescendant(root, index));
|
|
138
|
+
const completed = limit === 0 ? [] : quiet.slice(-limit);
|
|
139
|
+
return [...new Set([...important, ...completed])].sort(
|
|
140
|
+
(left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function nodeLine(node: DashboardNode, color: boolean): string {
|
|
145
|
+
const style = STYLE[node.state];
|
|
146
|
+
const phase = phaseLabel(node);
|
|
147
|
+
const pane = displayIdentifier(node.runtime?.herdrPaneId);
|
|
148
|
+
const suffix = [phase, pane ? `pane ${pane}` : ""].filter(Boolean).join(" · ");
|
|
149
|
+
return [
|
|
150
|
+
paint(style.symbol, style.color, color),
|
|
151
|
+
displayIdentifier(node.agentName) || "governed execution",
|
|
152
|
+
style.label.padEnd(10),
|
|
153
|
+
elapsed(node.durationMs),
|
|
154
|
+
suffix,
|
|
155
|
+
].filter(Boolean).join(" ");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function detailLines(node: DashboardNode): string[] {
|
|
159
|
+
const capabilities = node.effectiveGrant.filter(isLedgerCapabilityIdentifier);
|
|
160
|
+
const lines = [`grant ${capabilities.join(", ") || "(none)"}`];
|
|
161
|
+
const workspace = displayIdentifier(node.workspace?.id);
|
|
162
|
+
if (node.workspace && workspace) lines.push(`workspace ${workspace} · ${node.workspace.access}`);
|
|
163
|
+
if (node.executor) lines.push(`executor ${node.executor}`);
|
|
164
|
+
const runId = displayIdentifier(node.correlation?.run_id);
|
|
165
|
+
const taskId = displayIdentifier(node.correlation?.task_id);
|
|
166
|
+
if (runId) lines.push(`correlation ${runId}${taskId ? ` · ${taskId}` : ""}`);
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderTree(
|
|
171
|
+
root: DashboardNode,
|
|
172
|
+
index: TreeIndex,
|
|
173
|
+
options: Required<Pick<DashboardRenderOptions, "width" | "color" | "details" | "completedChildren">>,
|
|
174
|
+
): string[] {
|
|
175
|
+
const lines: string[] = [];
|
|
176
|
+
const visit = (node: DashboardNode, prefix: string, branch: string): void => {
|
|
177
|
+
lines.push(truncate(`${prefix}${branch}${nodeLine(node, options.color)}`, options.width));
|
|
178
|
+
const childPrefix = `${prefix}${branch === "" ? "" : branch === "└─ " ? " " : "│ "}`;
|
|
179
|
+
if (options.details) {
|
|
180
|
+
for (const detail of detailLines(node)) {
|
|
181
|
+
lines.push(truncate(`${childPrefix} ${paint(clean(detail), 90, options.color)}`, options.width));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const children = index.children.get(node.executionId) ?? [];
|
|
185
|
+
const important = children.filter((child) => hasInterestingDescendant(child, index));
|
|
186
|
+
const quiet = children.filter((child) => !hasInterestingDescendant(child, index));
|
|
187
|
+
const shownQuiet = options.completedChildren === 0 ? [] : quiet.slice(-options.completedChildren);
|
|
188
|
+
const shown = [...important, ...shownQuiet].sort(
|
|
189
|
+
(left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt),
|
|
190
|
+
);
|
|
191
|
+
const collapsed = quiet.length - shownQuiet.length;
|
|
192
|
+
const entries: Array<{ node?: DashboardNode; collapsed?: number }> = shown.map((child) => ({ node: child }));
|
|
193
|
+
if (collapsed > 0) entries.unshift({ collapsed });
|
|
194
|
+
entries.forEach((entry, position) => {
|
|
195
|
+
const last = position === entries.length - 1;
|
|
196
|
+
if (entry.node) visit(entry.node, childPrefix, last ? "└─ " : "├─ ");
|
|
197
|
+
else lines.push(truncate(
|
|
198
|
+
`${childPrefix}${last ? "└─ " : "├─ "}${paint(`… ${entry.collapsed} completed subtree${entry.collapsed === 1 ? "" : "s"}`, 90, options.color)}`,
|
|
199
|
+
options.width,
|
|
200
|
+
));
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
visit(root, "", "");
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function renderDashboard(projection: DashboardProjection, options: DashboardRenderOptions = {}): string {
|
|
208
|
+
const resolved = {
|
|
209
|
+
width: Math.max(10, options.width ?? 80),
|
|
210
|
+
color: options.color ?? true,
|
|
211
|
+
details: options.details ?? false,
|
|
212
|
+
completedRoots: Math.max(0, options.completedRoots ?? 3),
|
|
213
|
+
completedChildren: Math.max(0, options.completedChildren ?? 2),
|
|
214
|
+
};
|
|
215
|
+
const index = treeIndex(projection.nodes);
|
|
216
|
+
const lines = [paint("PI-DADDY", 1, resolved.color), ""];
|
|
217
|
+
|
|
218
|
+
const rendered = new Set<string>();
|
|
219
|
+
for (const workflow of projection.workflows) {
|
|
220
|
+
const roots = index.roots.filter((root) => root.correlation?.run_id === workflow.runId);
|
|
221
|
+
const facts = projection.workflowFacts.filter((fact) => fact.runId === workflow.runId);
|
|
222
|
+
if (roots.length === 0 && facts.length === 0) continue;
|
|
223
|
+
for (const root of roots) rendered.add(root.executionId);
|
|
224
|
+
lines.push(workflowHeader(workflow, resolved.color));
|
|
225
|
+
for (const fact of facts) lines.push(truncate(workflowFactLine(fact, resolved.color), resolved.width));
|
|
226
|
+
for (const root of selectedRoots(roots, index, resolved.completedRoots)) {
|
|
227
|
+
lines.push(...renderTree(root, index, resolved));
|
|
228
|
+
}
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const generic = index.roots.filter((root) => !rendered.has(root.executionId));
|
|
233
|
+
for (const root of selectedRoots(generic, index, resolved.completedRoots)) {
|
|
234
|
+
lines.push(...renderTree(root, index, resolved));
|
|
235
|
+
}
|
|
236
|
+
if (projection.nodes.length === 0) lines.push(paint("No governed executions recorded yet.", 90, resolved.color));
|
|
237
|
+
|
|
238
|
+
if (projection.corrupt.length > 0) {
|
|
239
|
+
lines.push("", paint(`ledger: ${projection.corrupt.length} corrupt line(s)`, 31, resolved.color));
|
|
240
|
+
for (const entry of projection.corrupt.slice(0, 3)) {
|
|
241
|
+
// Never render raw corrupt text: it may contain exactly the task/output fields the ledger forbids.
|
|
242
|
+
lines.push(truncate(` line ${entry.line}: ${clean(entry.reason)}`, resolved.width));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (projection.orphanEvents > 0) {
|
|
246
|
+
lines.push(paint(`${projection.orphanEvents} historical event(s) not joined — childId is not an occurrence id`, 90, resolved.color));
|
|
247
|
+
}
|
|
248
|
+
lines.push(
|
|
249
|
+
"",
|
|
250
|
+
paint(`depth ${projection.maxDepth} · ${projection.active} active`, 90, resolved.color),
|
|
251
|
+
paint("P planned · O observed inline · V controller-validated · E enforced child · D declared labels", 90, resolved.color),
|
|
252
|
+
);
|
|
253
|
+
return lines.map((line) => truncate(line, resolved.width)).join("\n");
|
|
254
|
+
}
|
package/src/delegate-types.ts
CHANGED
|
@@ -91,8 +91,10 @@ export interface DelegationContext {
|
|
|
91
91
|
fanoutBudget?: number;
|
|
92
92
|
/** This session's ledger id, so a child's `parentId` names its real parent (F8). */
|
|
93
93
|
spawnId?: string;
|
|
94
|
-
/** Ledger id assigned to THIS child, distinguishing
|
|
94
|
+
/** Ledger id assigned to THIS child, distinguishing logical siblings (F8). */
|
|
95
95
|
childSpawnId?: string;
|
|
96
|
+
/** Unique identity assigned to THIS execution occurrence; never derive joins from childSpawnId. */
|
|
97
|
+
childExecutionId?: string;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
export interface Delegation {
|
|
@@ -119,8 +121,10 @@ export interface Delegation {
|
|
|
119
121
|
* read the tool parameters would record an empty request for every definition spawn.
|
|
120
122
|
*/
|
|
121
123
|
requested: Capability[];
|
|
122
|
-
/** Ledger id for this child, if the caller assigned one (F8). */
|
|
124
|
+
/** Ledger id for this child's readable logical position, if the caller assigned one (F8). */
|
|
123
125
|
childId?: string;
|
|
126
|
+
/** Unique identity for this occurrence, if the caller assigned one. */
|
|
127
|
+
executionId?: string;
|
|
124
128
|
/**
|
|
125
129
|
* Which operator-authored instructions this spawn used (ADR-0018).
|
|
126
130
|
*
|