ai-workflow-logic 0.2.0 → 0.2.2

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.
@@ -8,9 +8,11 @@ import type {
8
8
  AIAgentRunResult,
9
9
  AIAgentSelector,
10
10
  AIWorkflowAuthoredEffects,
11
+ AIWorkflowNodeTasks,
11
12
  AIWorkflowFlowRunCheckpoint,
12
13
  AIWorkflowProfileDurableState,
13
14
  FlowClosedValue,
15
+ FlowClosedObject,
14
16
  } from 'ai-workflow-contract';
15
17
  import {
16
18
  AIAgentInvocationError,
@@ -58,6 +60,37 @@ function same(left: unknown, right: unknown): boolean {
58
60
  return JSON.stringify(left) === JSON.stringify(right);
59
61
  }
60
62
 
63
+ function assertNoTaskSourceOverride(checkpoint: AIWorkflowFlowRunCheckpoint, nodeId: string): void {
64
+ const object = (value: FlowClosedValue): value is FlowClosedObject => value !== null && typeof value === 'object' && !Array.isArray(value);
65
+ const bindings = object(checkpoint.config) ? checkpoint.config.workflowDelegations : undefined;
66
+ if (bindings && typeof bindings === 'object' && !Array.isArray(bindings) && Object.hasOwn(bindings, nodeId)) {
67
+ fail('AI_AGENT_TASK_DELEGATION_REQUIRED', 'checkpoint.config.workflowDelegations.' + nodeId,
68
+ 'This node has an explicit task source. Use ai.delegation.delegateTask so its admitted owner and context cannot be silently ignored.');
69
+ }
70
+ }
71
+
72
+ /** The canonical instance index owns context reuse; the Host still owns the Actor. */
73
+ function frozenAgentContext(checkpoint:AIWorkflowFlowRunCheckpoint,runtime:AIAgentProcessorRuntime,config:AIAgentEffectConfig):{name?:string;generation:number}|undefined {
74
+ const object=(value:FlowClosedValue|undefined):value is FlowClosedObject=>value!==undefined&&value!==null&&typeof value==='object'&&!Array.isArray(value);
75
+ const bindings=normalizeFlowClosedObject(checkpoint.config).workflowAgentContexts;
76
+ if(bindings===undefined)return;
77
+ if(!object(bindings))fail('AI_AGENT_CONTEXT_INVALID','checkpoint.config.workflowAgentContexts','Frozen Agent contexts must be an object.');
78
+ if(!Object.hasOwn(bindings,runtime.nodeId))return;
79
+ const choice=bindings[runtime.nodeId];
80
+ if(!object(choice)||choice.schemaVersion!=='workflow.agent-context/v1'
81
+ ||choice.agentDefinitionRef!==config.agentDefinitionRef||!['authored','shared','independent-per-generation'].includes(String(choice.sessionPolicy))
82
+ ||choice.nodeIdentity!==undefined&&choice.nodeIdentity!==true
83
+ ||Object.keys(choice).some(key=>!['schemaVersion','agentDefinitionRef','sessionPolicy','nodeIdentity'].includes(key)))
84
+ fail('AI_AGENT_CONTEXT_INVALID','checkpoint.config.workflowAgentContexts.'+runtime.nodeId,'Frozen Agent context must match the exact node task definition and supported policy.');
85
+ const generation=runtime.nodeContext?.generation??runtime.generation,scope=runtime.nodeContext?.scope??'';
86
+ if(generation===undefined||!Number.isSafeInteger(generation)||generation<0||typeof scope!=='string'||scope.normalize('NFC')!==scope)
87
+ fail('AI_AGENT_CONTEXT_IDENTITY_REQUIRED','runtime.nodeContext','A frozen Agent context requires its actual native generation and scope.');
88
+ if(choice.sessionPolicy==='authored')return {generation};
89
+ const name='workflow-agent-context:'+JSON.stringify([runtime.nodeId,scope,choice.sessionPolicy==='shared'?null:generation]);
90
+ if(config.instanceName!==undefined&&config.instanceName!==name)fail('AI_AGENT_CONTEXT_NAME_CONFLICT','config.instanceName','An explicit instance name cannot replace the frozen node context identity.');
91
+ return {name,generation};
92
+ }
93
+
61
94
  function semanticFacts(config: AIAgentEffectConfig, invocationMetadata?: Readonly<Record<string, FlowClosedValue>>): Readonly<Record<string, FlowClosedValue>> {
62
95
  return normalizeFlowClosedObject({
63
96
  materialRefs: config.materialRefs ?? [],
@@ -201,11 +234,16 @@ async function complete(runtime: AIAgentProcessorRuntime, checkpoint: AIWorkflow
201
234
 
202
235
  export async function runAgent<Input extends FlowClosedValue = FlowClosedValue, Output extends FlowClosedValue = FlowClosedValue>(runtime: AIAgentProcessorRuntime, input: Input, configInput: AIAgentEffectConfig): Promise<AIAgentRunResult<Output>> {
203
236
  const inputValue = normalizeFlowClosedValue(input, 'input') as Input;
204
- const config = normalizeAIAgentEffectConfig(configInput);
237
+ let config = normalizeAIAgentEffectConfig(configInput);
205
238
  assertTaskAuthority(runtime, config);
206
239
  let checkpoint = await loadCheckpoint(runtime);
240
+ assertNoTaskSourceOverride(checkpoint, runtime.nodeId);
241
+ const context=frozenAgentContext(checkpoint,runtime,config);
242
+ if(context){runtime={...runtime,generation:context.generation};if(context.name!==undefined)config={...config,instanceName:context.name}}
207
243
  let ai = profileAI(checkpoint);
208
244
  let receipt = ai.invocationsByKey[runtime.invocationKey];
245
+ if(context?.name!==undefined&&(receipt?.mode==='targeted'||!receipt&&ai.instanceIdByName[context.name]!==undefined))
246
+ return runTargetedAgent<Input,Output>(runtime,{byName:context.name},{kind:'ai.agent',payload:inputValue},config);
209
247
  if (receipt) {
210
248
  assertSameInvocation(receipt, runtime, 'new', inputValue, config);
211
249
  if (receipt.status === 'completed') return resultFromReceipt(ai, receipt) as AIAgentRunResult<Output>;
@@ -238,11 +276,15 @@ export async function runAgent<Input extends FlowClosedValue = FlowClosedValue,
238
276
  export async function runTargetedAgent<Input extends FlowClosedValue = FlowClosedValue, Output extends FlowClosedValue = FlowClosedValue>(runtime: AIAgentProcessorRuntime, selectorInput: AIAgentSelector, invocationInput: AIAgentInvocation<Input>, configInput: AIAgentEffectConfig): Promise<AIAgentRunResult<Output>> {
239
277
  const selector = normalizeAIAgentSelector(selectorInput);
240
278
  const invocation = normalizeAIAgentInvocation<Input>(invocationInput);
241
- const config = normalizeAIAgentEffectConfig(configInput);
279
+ let config = normalizeAIAgentEffectConfig(configInput);
242
280
  assertTaskAuthority(runtime, config);
243
281
  let checkpoint = await loadCheckpoint(runtime);
282
+ assertNoTaskSourceOverride(checkpoint, runtime.nodeId);
283
+ const context=frozenAgentContext(checkpoint,runtime,config);
284
+ if(context){runtime={...runtime,generation:context.generation};if(context.name!==undefined)config={...config,instanceName:context.name}}
244
285
  let ai = profileAI(checkpoint);
245
286
  const instance = resolveSelector(ai, selector, config);
287
+ if(context?.name!==undefined&&instance.instanceName!==context.name)fail('AI_AGENT_CONTEXT_TARGET_CONFLICT','selector','Target must be the original instance for this frozen node generation and scope.');
246
288
  let receipt = ai.invocationsByKey[runtime.invocationKey];
247
289
  if (receipt) {
248
290
  assertSameInvocation(receipt, runtime, 'targeted', invocation.payload, config, invocation.metadata);
@@ -280,3 +322,21 @@ export function bindAIAgentProcessors(runtime: AIAgentProcessorRuntime): AIWorkf
280
322
  runTargetedAgent: <Input extends FlowClosedValue, Output extends FlowClosedValue>(selector: AIAgentSelector, invocation: AIAgentInvocation<Input>, config: AIAgentEffectConfig) => runTargetedAgent<Input, Output>(runtime, selector, invocation, config),
281
323
  });
282
324
  }
325
+
326
+ /** Source selection is an immutable admission fact; authored input cannot select another owner. */
327
+ export function bindWorkflowNodeTasks(runtime: AIAgentProcessorRuntime, delegation?: { delegateTask(input: FlowClosedValue): Promise<FlowClosedValue> }): AIWorkflowNodeTasks {
328
+ return Object.freeze({ async invoke(input: FlowClosedValue, configInput: AIAgentEffectConfig): Promise<FlowClosedValue> {
329
+ const config=normalizeAIAgentEffectConfig(configInput),value=normalizeFlowClosedValue(input);
330
+ assertTaskAuthority(runtime,config);
331
+ const checkpoint=await loadCheckpoint(runtime),closed=normalizeFlowClosedObject(checkpoint.config),bindings=closed.workflowDelegations;
332
+ if(bindings!==undefined&&(bindings===null||typeof bindings!=='object'||Array.isArray(bindings)))
333
+ fail('AI_AGENT_TASK_SOURCES_INVALID','checkpoint.config.workflowDelegations','Frozen node task sources must be an object.');
334
+ if(bindings&&Object.hasOwn(bindings,runtime.nodeId)){
335
+ if(Object.keys(config).some(key=>key!=='agentDefinitionRef'))
336
+ fail('AI_AGENT_TASK_EFFECT_OVERRIDE_UNSUPPORTED','config','Delegated tasks use their frozen materials, model and context; Agent instance or effect overrides cannot be ignored.');
337
+ if(!delegation)fail('AI_AGENT_TASK_OWNER_REQUIRED','runtime.delegation','The admitted node task owner is unavailable.');
338
+ return delegation.delegateTask(value);
339
+ }
340
+ return (await runAgent(runtime,value,config)).output;
341
+ }});
342
+ }
@@ -25,6 +25,7 @@ import type {
25
25
  } from 'halfcode-compiler.xnl/resource-core';
26
26
  import { markAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
27
27
  import { AIWorkflowResourceProjectionError } from './errors.js';
28
+ import { projectAIAgentCodeResources } from './agent-code-projection.js';
28
29
 
29
30
  const AGENT_SUBDOMAINS = Object.freeze([
30
31
  'Messages',
@@ -43,6 +44,7 @@ export function projectAIWorkflowAgentResources(
43
44
  const diagnostics: AIWorkflowResourceProjectionDiagnostic[] = [];
44
45
  const resources = buildResourceIndex(registry, diagnostics);
45
46
  const edges: ResourceDependencyEdge[] = [];
47
+ const codeResources = projectAIAgentCodeResources(resources, edges);
46
48
 
47
49
  const materialPorts = recordsForKind(registry, 'MaterialPort')
48
50
  .map((resource) => projectMaterialPort(resource, resources, edges, diagnostics))
@@ -60,6 +62,7 @@ export function projectAIWorkflowAgentResources(
60
62
 
61
63
  if (diagnostics.length > 0) throwProjectionError(diagnostics);
62
64
  return markAIWorkflowAgentResourceProjection(Object.freeze({
65
+ codeResources,
63
66
  agentDefinitions: Object.freeze(agentDefinitions),
64
67
  materialPorts: Object.freeze(materialPorts),
65
68
  materialBindings: Object.freeze(materialBindings),
@@ -12,6 +12,7 @@ import type {
12
12
  LegacyAIWorkflowProfileDurableState,
13
13
  } from 'ai-workflow-contract';
14
14
  import { normalizeFlowClosedObject, normalizeFlowClosedValue } from './run-checkpoint.js';
15
+ import { normalizeWorkflowDelegations } from './delegation.js';
15
16
 
16
17
  export class AIAgentInvocationError extends Error {
17
18
  constructor(public readonly code: string, public readonly location: string, message: string) {
@@ -231,7 +232,7 @@ function normalizeReceipt(value: unknown, key: string, instances: Readonly<Recor
231
232
  }
232
233
 
233
234
  export function normalizeCanonicalAIAgentState(value: unknown, location = 'ai'): AIWorkflowProfileDurableState {
234
- const input = record(value, ['schemaVersion', 'instancesById', 'instanceIdByName', 'invocationsByKey'], [], location);
235
+ const input = record(value, ['schemaVersion', 'instancesById', 'instanceIdByName', 'invocationsByKey'], ['delegationsByKey'], location);
235
236
  if (input.schemaVersion !== 'depa.ai-agent-state/v1') fail('AI_AGENT_STATE_SCHEMA_UNSUPPORTED', `${location}.schemaVersion`, 'Unsupported Agent state schema.');
236
237
  const instanceValues = objectMap(input.instancesById, `${location}.instancesById`);
237
238
  const instancesById: Record<string, AIAgentInstanceReference> = {};
@@ -259,7 +260,7 @@ export function normalizeCanonicalAIAgentState(value: unknown, location = 'ai'):
259
260
  }
260
261
  invocationsByKey[key] = receipt;
261
262
  }
262
- return Object.freeze({ schemaVersion: 'depa.ai-agent-state/v1', instancesById: Object.freeze(instancesById), instanceIdByName: Object.freeze(instanceIdByName), invocationsByKey: Object.freeze(invocationsByKey) });
263
+ return Object.freeze({ schemaVersion: 'depa.ai-agent-state/v1', instancesById: Object.freeze(instancesById), instanceIdByName: Object.freeze(instanceIdByName), invocationsByKey: Object.freeze(invocationsByKey), ...(input.delegationsByKey === undefined ? {} : { delegationsByKey: normalizeWorkflowDelegations(input.delegationsByKey) }) });
263
264
  }
264
265
 
265
266
  export function upgradeLegacyAIWorkflowProfileDurableState(value: LegacyAIWorkflowProfileDurableState, agentDefinitionRefs: Readonly<Record<string, `resource://${string}`>>, location = 'legacyAI'): AIWorkflowProfileDurableState {
package/src/browser.ts CHANGED
@@ -1 +1 @@
1
- export * from './index.js';
1
+ export * from './core.js';
@@ -16,6 +16,7 @@ import type { WorkCtrlFlowSnapshot } from 'work-ctrl-flow-contract';
16
16
  import {
17
17
  createMaterializedWorkCtrlFlowEngineFromDirectory,
18
18
  loadFrozenCtrlDefinitionStepIds,
19
+ loadFrozenCtrlDefinitionMembership,
19
20
  loadMaterializedFlowInstance,
20
21
  materializeFlowInstance,
21
22
  type MaterializedWorkCtrlFlowDirectoryOptions,
@@ -293,7 +294,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
293
294
  if (!fs.existsSync(locations.runDir)) return undefined;
294
295
  assertExistingDirectory(locations.runDir, this.#realStoreRoot, 'flow-run-symlink-boundary');
295
296
  if (!fs.existsSync(locations.checkpointFile)) return undefined;
296
- const checkpoint = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
297
+ const checkpoint = this.#readStorage(locations.checkpointFile).checkpoint;
297
298
  this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
298
299
  this.#validateFrozenProfile(checkpoint);
299
300
  return checkpoint;
@@ -317,7 +318,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
317
318
  this.#assertRunPhysicalBoundary(locations);
318
319
  removeUncommittedStaging(locations.stagingFile, locations.runDir, this.#realStoreRoot);
319
320
  const currentStorage = fs.existsSync(locations.checkpointFile)
320
- ? readCheckpointStorage(locations.checkpointFile, this.#realStoreRoot)
321
+ ? this.#readStorage(locations.checkpointFile)
321
322
  : undefined;
322
323
  const current = currentStorage?.checkpoint;
323
324
  if (current) {
@@ -338,13 +339,15 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
338
339
  ));
339
340
  const encoded = encodeCheckpointSidecars(checkpoint, currentStorage?.tree, {
340
341
  definitionStepIds,
342
+ ...(checkpoint.profile.kind === 'AIDataWorkflow' ? {} : { definitionInvocationOwners: frozenAuthority?.definitionInvocationOwners
343
+ ?? loadFrozenCtrlDefinitionMembership(path.join(locations.instanceDir, 'definition'), checkpoint.profile.kind, this.#extensionCodecs).invocationOwners }),
341
344
  previousMaterialRecordDigests: currentStorage?.materialRecordDigests,
342
345
  });
343
346
  removeUnacceptedSidecarReceipt(locations.runDir, checkpoint.version, this.#realStoreRoot);
344
347
  persistCheckpointSidecars(locations.runDir, encoded, this.#durability);
345
348
  assertSidecarDirectories(locations.runDir, this.#realStoreRoot);
346
349
  writeCheckpointCandidate(locations.stagingFile, encoded.head, this.#realStoreRoot, this.#durability);
347
- const staged = readCheckpointFile(locations.stagingFile, this.#realStoreRoot);
350
+ const staged = this.#readStorage(locations.stagingFile).checkpoint;
348
351
  this.#assertCheckpointIdentity(staged, key, descriptor.definition);
349
352
  this.#validateFrozenProfile(staged);
350
353
  if (stableJson(staged) !== stableJson(checkpoint)) {
@@ -359,7 +362,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
359
362
  assertRegularFile(locations.checkpointFile, this.#realStoreRoot, 'flow-run-checkpoint-invalid');
360
363
  }
361
364
  renameDurably(locations.stagingFile, locations.checkpointFile, 'head', this.#durability);
362
- const accepted = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
365
+ const accepted = this.#readStorage(locations.checkpointFile).checkpoint;
363
366
  this.#assertCheckpointIdentity(accepted, key, descriptor.definition);
364
367
  this.#validateFrozenProfile(accepted);
365
368
  if (stableJson(accepted) !== stableJson(checkpoint)) {
@@ -452,7 +455,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
452
455
  try {
453
456
  this.#assertRunPhysicalBoundary(locations);
454
457
  const checkpoint = fs.existsSync(locations.checkpointFile)
455
- ? readCheckpointFile(locations.checkpointFile, this.#realStoreRoot)
458
+ ? this.#readStorage(locations.checkpointFile).checkpoint
456
459
  : undefined;
457
460
  if (!checkpoint) {
458
461
  throw new LegacyWorkCtrlImportError(
@@ -574,6 +577,13 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
574
577
  }, { requireAIDataAuthority: true });
575
578
  }
576
579
 
580
+ #readStorage(file: string) {
581
+ return readCheckpointStorage(file, this.#realStoreRoot, {
582
+ profileAuthority: this.#profileAuthority,
583
+ extensionCodecs: this.#extensionCodecs,
584
+ });
585
+ }
586
+
577
587
  #ensureRunDirectory(locations: FlowRunFilesystemLocations): void {
578
588
  ensureChildDirectory(locations.instanceDir, locations.runsDir, this.#realStoreRoot, this.#durability, 'head');
579
589
  ensureChildDirectory(locations.runsDir, locations.runDir, this.#realStoreRoot, this.#durability, 'head');
@@ -1018,13 +1028,10 @@ function ensureChildDirectory(
1018
1028
  assertExistingDirectory(child, realRoot, 'flow-run-boundary-invalid');
1019
1029
  }
1020
1030
 
1021
- function readCheckpointFile(file: string, realRoot: string): AIWorkflowFlowRunCheckpoint {
1022
- return readCheckpointStorage(file, realRoot).checkpoint;
1023
- }
1024
-
1025
1031
  function readCheckpointStorage(
1026
1032
  file: string,
1027
1033
  realRoot: string,
1034
+ runtime: { readonly profileAuthority?: import('./checkpoint-profile.js').FlowRunProfileAuthorityPort; readonly extensionCodecs: import('flow-step-space-contract').DefinitionStepExtensionCodecRegistryPort },
1028
1035
  ): {
1029
1036
  readonly checkpoint: AIWorkflowFlowRunCheckpoint;
1030
1037
  readonly tree?: StepMerkleTree;
@@ -1034,7 +1041,13 @@ function readCheckpointStorage(
1034
1041
  try {
1035
1042
  if (isCheckpointHead(value)) {
1036
1043
  assertSidecarDirectories(path.dirname(file), realRoot);
1037
- const loaded = readCheckpointHeadAndLogical(file);
1044
+ const loaded = readCheckpointHeadAndLogical(file,{ctrlMembership(head,kind){
1045
+ assertSafeSegment(head.instanceId);
1046
+ const authority=runtime.profileAuthority?.load({instanceId:head.instanceId,definition:head.definition,profileKind:kind});
1047
+ return authority?.definitionInvocationOwners
1048
+ ? {definitionStepIds:authority.definitionStepIds,invocationOwners:authority.definitionInvocationOwners}
1049
+ : loadFrozenCtrlDefinitionMembership(path.join(realRoot,'instances',head.instanceId,'definition'),kind,runtime.extensionCodecs);
1050
+ }});
1038
1051
  return {
1039
1052
  checkpoint: loaded.checkpoint,
1040
1053
  tree: loaded.tree,
@@ -18,6 +18,7 @@ export interface FrozenRunStepExtensionDefinition {
18
18
  export interface FrozenFlowRunProfileAuthority {
19
19
  readonly profileKind: FlowRunProfileState['kind'];
20
20
  readonly definitionStepIds: readonly string[];
21
+ readonly definitionInvocationOwners?: Readonly<Record<string, string>>;
21
22
  readonly binding?: FlowClosedObject;
22
23
  readonly extensionsByStepId: Readonly<
23
24
  Record<string, Readonly<Record<string, FrozenRunStepExtensionDefinition>>>
@@ -103,6 +104,8 @@ export function validateCheckpointAgainstFrozenProfile(
103
104
  if (ids.some((id) => typeof id !== 'string' || !id || id !== id.trim()) || new Set(ids).size !== ids.length) {
104
105
  throw new FlowRunCheckpointTransitionError('Frozen profile step membership is invalid.');
105
106
  }
107
+ if (Object.entries(authority.definitionInvocationOwners ?? {}).some(([key, owner]) => !key || !ids.includes(owner)))
108
+ throw new FlowRunCheckpointTransitionError('Frozen compiled action ownership is invalid.');
106
109
  for (const stepId of Object.keys(authority.extensionsByStepId)) {
107
110
  if (!ids.includes(stepId)) {
108
111
  throw new FlowRunCheckpointTransitionError(`Frozen extension step ${stepId} is not a definition member.`);
@@ -42,8 +42,15 @@ export interface FlowRunCheckpointSidecarReceipt {
42
42
  }
43
43
  export interface CheckpointSidecarEncodeConfig {
44
44
  readonly definitionStepIds?: readonly string[];
45
+ readonly definitionInvocationOwners?: Readonly<Record<string, string>>;
45
46
  readonly previousMaterialRecordDigests?: readonly DefinitionStepDigest[];
46
47
  }
48
+ export interface CheckpointSidecarReadConfig {
49
+ readonly ctrlMembership?: (head: FlowRunCheckpointHead, kind: 'WorkCtrlFlow' | 'AICtrlWorkflow') => {
50
+ readonly definitionStepIds: readonly string[];
51
+ readonly invocationOwners: Readonly<Record<string, string>>;
52
+ };
53
+ }
47
54
  type EncodedCheckpoint = {
48
55
  readonly head: FlowRunCheckpointHead;
49
56
  readonly controlRecords: readonly ImmutableStepRecord[];
@@ -123,12 +130,12 @@ function dataMembership(
123
130
  const ORCHESTRATION_FIELDS = ['attempts', 'deadlines', 'lanes', 'winners', 'cancellations', 'composites'] as const;
124
131
  type OrchestrationField = typeof ORCHESTRATION_FIELDS[number];
125
132
 
126
- function runtimeCtrlMembers(orchestrationValue: unknown, definitionStepIds: readonly string[]): readonly string[] {
133
+ function runtimeCtrlMembers(orchestrationValue: unknown, definitionStepIds: readonly string[], invocationOwners: Readonly<Record<string, string>> = {}, waitOwners:ReadonlyMap<string,string>=new Map()): readonly string[] {
127
134
  const orchestration = object(orchestrationValue, 'ctrl orchestration');
128
135
  const members = new Set<string>();
129
136
  for (const field of ORCHESTRATION_FIELDS) {
130
137
  for (const [identity] of entries(orchestration[field], `ctrl orchestration.${field}`)) {
131
- if (!definitionOwner(identity, definitionStepIds)) members.add(identity);
138
+ if (!definitionOwner(identity, definitionStepIds, invocationOwners)&&!waitOwners.has(identity)) members.add(identity);
132
139
  }
133
140
  }
134
141
  return [...members].sort(compare);
@@ -161,22 +168,55 @@ function createRunStepLeaf(
161
168
  });
162
169
  return { leaf, materialRecords: [coreRecord, ...extensionRecords.map(({ record }) => record)] as readonly ImmutableStepRecord[] };
163
170
  }
164
- function definitionOwner(invocationKey: string, definitionStepIds: readonly string[]): string | undefined {
171
+ function definitionOwner(invocationKey: string, definitionStepIds: readonly string[], invocationOwners: Readonly<Record<string, string>> = {}): string | undefined {
165
172
  const visit = invocationKey.lastIndexOf('#');
166
173
  const stablePath = visit >= 0 ? invocationKey.slice(0, visit) : invocationKey;
167
174
  const id = stablePath.slice(stablePath.lastIndexOf('/') + 1);
168
- return definitionStepIds.includes(id) ? id : undefined;
175
+ const owner = invocationOwners[id];
176
+ return definitionStepIds.includes(id) ? id : owner && definitionStepIds.includes(owner) ? owner : undefined;
169
177
  }
170
178
 
171
- function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: readonly string[]) {
179
+ /** Dynamic waits belong to the exact compiled action that created their native handle, never a prefix wildcard. */
180
+ function delegationWaitOwners(snapshotValue:unknown,definitionStepIds:readonly string[],invocationOwners:Readonly<Record<string,string>>,aiValue?:unknown):ReadonlyMap<string,string>{
181
+ const snapshot=object(snapshotValue,'ctrl snapshot'),owners=new Map<string,string>();
182
+ for(const field of ['closedWaitHandles','openWaitHandles']){
183
+ const handles=snapshot[field];if(!Array.isArray(handles))throw Error('Ctrl wait handles must be an array.');
184
+ for(const value of handles){
185
+ const handle=object(value,'ctrl wait handle');
186
+ if(!handle.metadata||typeof handle.metadata!=='object'||Array.isArray(handle.metadata))continue;
187
+ const metadata=object(handle.metadata,'ctrl wait metadata');
188
+ if(metadata.workflowDelegation===undefined)continue;
189
+ const delegated=object(metadata.workflowDelegation,'ctrl delegation wait');
190
+ const action=delegated.actionInvocationKey,invocation=delegated.invocationKey;
191
+ if(delegated.kind!=='workflow.task-delegation/v1'||typeof action!=='string'||typeof invocation!=='string'||typeof handle.invocationKey!=='string'||typeof handle.nodeKey!=='string'||handle.waitKind!=='external-job'||handle.signalKind!=='workflow.delegation.observe'||handle.signalKey!==action)throw Error('Ctrl delegation wait identity is invalid.');
192
+ const owner=definitionOwner(action,definitionStepIds,invocationOwners);
193
+ const actionPath=action.slice(0,action.lastIndexOf('#')),scope=actionPath.slice(0,actionPath.lastIndexOf('/')+1),node=actionPath.slice(actionPath.lastIndexOf('/')+1);
194
+ const prefix=`${node}:delegation:`;
195
+ if(!owner||definitionOwner(invocation,definitionStepIds,invocationOwners)!==owner||!handle.nodeKey.startsWith(prefix)||!/^(0|[1-9][0-9]*)$/.test(handle.nodeKey.slice(prefix.length))||!handle.invocationKey.startsWith(`${scope}${handle.nodeKey}#`)||!/^(0|[1-9][0-9]*)$/.test(handle.invocationKey.slice(`${scope}${handle.nodeKey}#`.length)))throw Error('Ctrl delegation wait is not owned by its frozen action.');
196
+ if(aiValue!==undefined){
197
+ const ai=object(aiValue,'ctrl AI state'),records=object(ai.delegationsByKey??{},'ctrl delegations'),record=object(records[invocation],'ctrl delegation record');
198
+ if(!Array.isArray(record.attempts)||record.attempts.length===0)throw Error('Ctrl delegation wait has no accepted invocation.');
199
+ const attempt=object(record.attempts.at(-1),'ctrl delegation attempt'),request=object(attempt.request,'ctrl delegation request'),lineage=object(request.lineage,'ctrl delegation lineage');
200
+ if(attempt.dispatchClaimed!==true||lineage.invocationKey!==invocation||lineage.nodeId!==owner)throw Error('Ctrl delegation wait receipt does not match its frozen action.');
201
+ }
202
+ if(owners.has(handle.invocationKey))throw Error('Ctrl delegation wait identity is duplicated.');
203
+ owners.set(handle.invocationKey,owner);
204
+ }
205
+ }
206
+ return owners;
207
+ }
208
+
209
+ function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: readonly string[], invocationOwners: Readonly<Record<string, string>> = {}) {
172
210
  if (!idsValue?.length || idsValue.some((id) => typeof id !== 'string' || !id) || new Set(idsValue).size !== idsValue.length) {
173
211
  throw new Error('Ctrl sidecar requires unique frozen definition step identities.');
174
212
  }
175
213
  if (checkpoint.profile.kind !== 'WorkCtrlFlow' && checkpoint.profile.kind !== 'AICtrlWorkflow') throw new Error('Ctrl profile expected.');
176
214
  const definitionStepIds = [...idsValue];
215
+ if (Object.entries(invocationOwners).some(([key, owner]) => !key || !definitionStepIds.includes(owner))) throw new Error('Ctrl compiled action ownership is invalid.');
177
216
  const snapshot = checkpoint.profile.snapshot as FlowClosedObject;
178
217
  const { branchDecisions, nodeResults, waitResults, orchestration, ...snapshotControl } = snapshot;
179
- const runtimeMembers = runtimeCtrlMembers(orchestration, definitionStepIds);
218
+ const waitOwners=delegationWaitOwners(snapshot,definitionStepIds,invocationOwners,checkpoint.profile.kind==='AICtrlWorkflow'?checkpoint.profile.ai:undefined);
219
+ const runtimeMembers = runtimeCtrlMembers(orchestration, definitionStepIds, invocationOwners,waitOwners);
180
220
  const memberIds = [...definitionStepIds.map((id) => `definition:${id}`), ...runtimeMembers.map((id) => `runtime:${id}`)].sort(compare);
181
221
  const facts = new Map(memberIds.map((id) => [id, {
182
222
  branchDecisions: {},
@@ -186,7 +226,7 @@ function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: reado
186
226
  }]));
187
227
  const runtimeSet = new Set(runtimeMembers);
188
228
  const ownerOf = (identity: string) => {
189
- const owner = definitionOwner(identity, definitionStepIds);
229
+ const owner = definitionOwner(identity, definitionStepIds, invocationOwners)??waitOwners.get(identity);
190
230
  return owner ? `definition:${owner}` : runtimeSet.has(identity) ? `runtime:${identity}` : undefined;
191
231
  };
192
232
  const place = (field: 'branchDecisions' | 'nodeResults' | 'waitResults', value: unknown) => {
@@ -223,6 +263,7 @@ function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: reado
223
263
  profile: {
224
264
  kind: checkpoint.profile.kind,
225
265
  definitionStepIds,
266
+ ...(Object.keys(invocationOwners).length ? { definitionInvocationOwners: invocationOwners } : {}),
226
267
  snapshot: {
227
268
  ...snapshotControl,
228
269
  orchestration: Object.fromEntries(ORCHESTRATION_FIELDS.map((field) => [field, {}])),
@@ -269,6 +310,7 @@ function reconstruct(
269
310
  records: readonly ImmutableStepRecord[],
270
311
  controlRecords: ReadonlyMap<DefinitionStepDigest, ImmutableStepRecord>,
271
312
  readMaterial: (digest: DefinitionStepDigest) => ImmutableStepRecord,
313
+ config: CheckpointSidecarReadConfig,
272
314
  ): AIWorkflowFlowRunCheckpoint {
273
315
  const control = object(head.control, 'checkpoint control');
274
316
  if (control.schemaVersion !== 'depa.flow-run-checkpoint-control/v1') throw new Error('Checkpoint control schema is invalid.');
@@ -339,7 +381,15 @@ function reconstruct(
339
381
  if (!Array.isArray(profileControl.definitionStepIds) || profileControl.definitionStepIds.some((id) => typeof id !== 'string' || !id)) throw new Error('Ctrl definition membership is invalid.');
340
382
  const definitionStepIds = profileControl.definitionStepIds as readonly string[];
341
383
  if (new Set(definitionStepIds).size !== definitionStepIds.length) throw new Error('Ctrl definition membership is duplicated.');
384
+ const savedOwners = object(profileControl.definitionInvocationOwners ?? {}, 'ctrl compiled action owners');
385
+ if (Object.values(savedOwners).some(owner => typeof owner !== 'string' || !definitionStepIds.includes(owner))) throw new Error('Ctrl compiled action owner is invalid.');
386
+ const membership = config.ctrlMembership?.(head, profileControl.kind);
387
+ if (membership && (canonicalJson([...definitionStepIds].sort(compare)) !== canonicalJson([...membership.definitionStepIds].sort(compare))
388
+ || profileControl.definitionInvocationOwners !== undefined && canonicalJson(savedOwners) !== canonicalJson(membership.invocationOwners)))
389
+ throw new Error('Ctrl sidecar ownership differs from the frozen compiled definition.');
390
+ const invocationOwners = membership?.invocationOwners ?? savedOwners as Readonly<Record<string, string>>;
342
391
  const snapshotControl = object(profileControl.snapshot, 'ctrl snapshot control');
392
+ const waitOwners=delegationWaitOwners(snapshotControl,definitionStepIds,invocationOwners,profileControl.kind==='AICtrlWorkflow'?profileControl.ai:undefined);
343
393
  const orchestrationOutput = Object.fromEntries(
344
394
  ORCHESTRATION_FIELDS.map((field) => [field, {}]),
345
395
  ) as Record<OrchestrationField, Record<string, FlowClosedValue>>;
@@ -365,7 +415,7 @@ function reconstruct(
365
415
  actual.push(value.memberId);
366
416
  for (const field of ['branchDecisions', 'nodeResults', 'waitResults'] as const) {
367
417
  for (const [key, fact] of entries(value[field], `ctrl ${field}`)) {
368
- const owner = definitionOwner(key, definitionStepIds);
418
+ const owner = definitionOwner(key, definitionStepIds, invocationOwners)??waitOwners.get(key);
369
419
  if (projectedDefinition !== null ? owner !== projectedDefinition : value.memberId !== `runtime:${key}`) throw new Error(`Ctrl ${field} member is invalid.`);
370
420
  if (Object.prototype.hasOwnProperty.call(output[field], key)) throw new Error(`Ctrl ${field} member is duplicated.`);
371
421
  output[field][key] = fact;
@@ -374,14 +424,14 @@ function reconstruct(
374
424
  const recordOrchestration = object(value.orchestration, 'ctrl record orchestration');
375
425
  for (const field of ORCHESTRATION_FIELDS) {
376
426
  for (const [key, fact] of entries(recordOrchestration[field], `ctrl orchestration.${field}`)) {
377
- const owner = definitionOwner(key, definitionStepIds);
427
+ const owner = definitionOwner(key, definitionStepIds, invocationOwners)??waitOwners.get(key);
378
428
  if (projectedDefinition !== null ? owner !== projectedDefinition : value.memberId !== `runtime:${key}`) throw new Error(`Ctrl orchestration.${field} member is invalid.`);
379
429
  if (Object.prototype.hasOwnProperty.call(orchestrationOutput[field], key)) throw new Error(`Ctrl orchestration.${field} member is duplicated.`);
380
430
  orchestrationOutput[field][key] = fact;
381
431
  }
382
432
  }
383
433
  }
384
- const runtimeMembers = runtimeCtrlMembers(orchestrationOutput, definitionStepIds);
434
+ const runtimeMembers = runtimeCtrlMembers(orchestrationOutput, definitionStepIds, invocationOwners,waitOwners);
385
435
  const expected = [...definitionStepIds.map((id) => `definition:${id}`), ...runtimeMembers.map((id) => `runtime:${id}`)].sort(compare);
386
436
  if (canonicalJson(expected) !== canonicalJson(actual.sort(compare))) throw new Error('Ctrl sidecar membership is invalid.');
387
437
  const snapshot: FlowClosedObject = { ...snapshotControl, ...output };
@@ -469,9 +519,20 @@ function runEvent(runtime: CheckpointDurabilityRuntime | undefined, event: Check
469
519
  runtime?.beforeEvent?.(event); operation(); runtime?.onEvent?.(event);
470
520
  }
471
521
  function syncDirectory(directory: string, role: CheckpointDurabilityRole, runtime?: CheckpointDurabilityRuntime): void {
472
- let fd: number | undefined;
473
- try { fd = fs.openSync(directory, fs.constants.O_RDONLY); runEvent(runtime, { operation: 'directory-fsync', role, path: directory }, () => fs.fsyncSync(fd!)); }
474
- finally { if (fd !== undefined) fs.closeSync(fd); }
522
+ runEvent(runtime, { operation: 'directory-fsync', role, path: directory }, () => {
523
+ let fd: number | undefined;
524
+ try {
525
+ fd = fs.openSync(directory, fs.constants.O_RDONLY);
526
+ fs.fsyncSync(fd);
527
+ } catch (error) {
528
+ // Windows may reject either opening or syncing a directory. File-data
529
+ // fsync and durability observers retain their original failure behavior.
530
+ const code = (error as NodeJS.ErrnoException).code;
531
+ if (process.platform !== 'win32' || !code || !['EPERM', 'EINVAL', 'ENOTSUP', 'EISDIR'].includes(code)) throw error;
532
+ } finally {
533
+ if (fd !== undefined) fs.closeSync(fd);
534
+ }
535
+ });
475
536
  }
476
537
  export function createDirectoryDurably(directory: string, role: CheckpointDurabilityRole, runtime?: CheckpointDurabilityRuntime): void {
477
538
  runEvent(runtime, { operation: 'directory-create', role, path: directory }, () => fs.mkdirSync(directory));
@@ -500,7 +561,7 @@ function writeImmutable(file: string, value: unknown, role: 'record' | 'tree' |
500
561
 
501
562
  export function encodeCheckpointSidecars(checkpointValue: AIWorkflowFlowRunCheckpoint, previousTree: StepMerkleTree | undefined, config: CheckpointSidecarEncodeConfig = {}): EncodedCheckpoint {
502
563
  const checkpoint = normalizeFlowRunCheckpoint(checkpointValue);
503
- const partitioned = checkpoint.profile.kind === 'AIDataWorkflow' ? partitionData(checkpoint, config.definitionStepIds) : partitionCtrl(checkpoint, config.definitionStepIds);
564
+ const partitioned = checkpoint.profile.kind === 'AIDataWorkflow' ? partitionData(checkpoint, config.definitionStepIds) : partitionCtrl(checkpoint, config.definitionStepIds, config.definitionInvocationOwners);
504
565
  const tree = buildStepMerkleTree(partitioned.records); const treeDiff = diffStepMerkleTrees(previousTree, tree);
505
566
  const previousRecords = new Set<string>(); Object.values(previousTree?.nodes ?? {}).forEach((node) => { if (node.kind === 'leaf') previousRecords.add(node.recordDigest); });
506
567
  const addedRecordDigests = partitioned.records.map((record) => record.digest).filter((value) => !previousRecords.has(value)).sort(compare);
@@ -525,7 +586,7 @@ function assertReceipt(root: string, head: FlowRunCheckpointHead): Record<string
525
586
  if (value.schemaVersion !== 'depa.flow-run-checkpoint-sidecar-receipt/v1' || value.version !== head.version || value.rootDigest !== head.runStepStateRootDigest || value.logicalDigest !== head.logicalDigest || (value.previousRootDigest !== null && (typeof value.previousRootDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value.previousRootDigest)))) throw new Error('Receipt does not bind head.');
526
587
  return value;
527
588
  }
528
- export function readCheckpointHeadAndLogical(file: string) {
589
+ export function readCheckpointHeadAndLogical(file: string, config: CheckpointSidecarReadConfig = {}) {
529
590
  const head = normalizeHead(readJson(file)); const root = path.join(path.dirname(file), 'step-space'); const receipt = assertReceipt(root, head);
530
591
  const controlRecords = new Map<DefinitionStepDigest, ImmutableStepRecord>();
531
592
  for (const value of receipt.controlRecordDigests as readonly DefinitionStepDigest[]) {
@@ -536,7 +597,7 @@ export function readCheckpointHeadAndLogical(file: string) {
536
597
  materialDigests.add(value);
537
598
  return readRecord(path.join(root, 'records'), value);
538
599
  };
539
- const checkpoint = reconstruct(head, collectRecords(root, head.runStepStateRootDigest), controlRecords, readMaterial);
600
+ const checkpoint = reconstruct(head, collectRecords(root, head.runStepStateRootDigest), controlRecords, readMaterial, config);
540
601
  if (digest(checkpoint) !== head.logicalDigest || checkpoint.instanceId !== head.instanceId || checkpoint.runId !== head.runId || checkpoint.version !== head.version || canonicalJson(checkpoint.definition) !== canonicalJson(head.definition)) throw new Error('Head and logical checkpoint differ.');
541
602
  const nodes: Record<string, StepMerkleNode> = {}; const leafByKey: Record<string, DefinitionStepDigest> = {}; const keyOrder: string[] = [];
542
603
  const visit = (value: DefinitionStepDigest): void => { if (nodes[value]) return; const node = readNode(path.join(root, 'trees'), value); nodes[value] = node; if (node.kind === 'leaf') { leafByKey[node.key] = value; keyOrder.push(node.key); } else { visit(node.leftDigest); visit(node.rightDigest); } };
@@ -34,22 +34,38 @@ export interface WorkCtrlFlowCheckpointStoreOptions {
34
34
  readonly projectOutput?: (snapshot: WorkCtrlFlowSnapshot) => FlowClosedValue;
35
35
  }
36
36
 
37
- /** Removes optional undefined object fields without relaxing the closed-data validator. */
37
+ /** Projects engine-owned faults and optional fields; business values remain subject to closed-data validation. */
38
38
  export function projectWorkCtrlSnapshotToCheckpointData(
39
39
  snapshot: WorkCtrlFlowSnapshot,
40
40
  ): WorkCtrlFlowSnapshot {
41
+ const projectState = (state: unknown): unknown => {
42
+ if (!state || typeof state !== 'object') return state;
43
+ const fault: unknown = Object.getOwnPropertyDescriptor(state,'fault')?.value;
44
+ if(!(fault instanceof Error))return state;
45
+ const code: unknown = Object.getOwnPropertyDescriptor(fault, 'code')?.value;
46
+ return { ...state, fault: { name: fault.name, message: fault.message, ...(typeof code === 'string' ? { code } : {}) } };
47
+ };
41
48
  const visit = (value: unknown): unknown => {
42
- if (Array.isArray(value)) return value.map(visit);
49
+ if (Array.isArray(value)) {
50
+ if(Reflect.ownKeys(value).length!==value.length+1)return value;
51
+ const items:unknown[]=[];
52
+ for(let index=0;index<value.length;index++){
53
+ const descriptor=Object.getOwnPropertyDescriptor(value,String(index));
54
+ if(!descriptor||!('value'in descriptor))return value;
55
+ items.push(visit(descriptor.value));
56
+ }
57
+ return items;
58
+ }
43
59
  if (value === null || typeof value !== 'object') return value;
44
60
  const prototype = Object.getPrototypeOf(value);
45
61
  if (prototype !== Object.prototype && prototype !== null) return value;
46
- return Object.fromEntries(
47
- Object.entries(value)
48
- .filter(([, nested]) => nested !== undefined)
49
- .map(([key, nested]) => [key, visit(nested)]),
50
- );
62
+ const descriptors=Object.getOwnPropertyDescriptors(value);
63
+ if(Reflect.ownKeys(value).some(key=>typeof key!=='string'||!('value'in descriptors[key]!)))return value;
64
+ return Object.fromEntries(Object.entries(descriptors).filter(([,descriptor])=>descriptor.value!==undefined).map(([key,descriptor])=>[key,visit(descriptor.value)]));
51
65
  };
52
- return visit(snapshot) as WorkCtrlFlowSnapshot;
66
+ return visit({ ...snapshot, vars: { ...snapshot.vars, __flow_execution__: projectState(snapshot.vars.__flow_execution__) }, ...(snapshot.orchestration ? { orchestration: { ...snapshot.orchestration,
67
+ lanes: Object.fromEntries(Object.entries(snapshot.orchestration.lanes).map(([key, lane]) => [key, { ...lane, ...(lane.flowState === undefined ? {} : { flowState: projectState(lane.flowState) }) }])),
68
+ } } : {}) }) as WorkCtrlFlowSnapshot;
53
69
  }
54
70
 
55
71
  function workCtrlControllerSidecars(snapshot: WorkCtrlFlowSnapshot): FlowClosedObject {
@@ -283,6 +299,9 @@ function validateAIDataProfileTransition(
283
299
  `AI Data node ${nodeId} generation cannot decrease from ${node.generation} to ${candidate.generation}.`,
284
300
  );
285
301
  }
302
+ const waitContext=node.result&&typeof node.result==='object'?Object.getOwnPropertyDescriptor(node.result,'waitContext')?.value:undefined;
303
+ const nextContext=candidate.result&&typeof candidate.result==='object'?Object.getOwnPropertyDescriptor(candidate.result,'waitContext')?.value:undefined;
304
+ if(node.generation===candidate.generation&&waitContext!==undefined&&!sameClosedValue(waitContext,nextContext))throw new FlowRunCheckpointTransitionError(`AI Data native wait context ${nodeId}@${node.generation} is immutable.`);
286
305
  }
287
306
  const appendedPatches = after.patchHistory.slice(before.patchHistory.length);
288
307
  const appendedInvalidations = after.invalidations.filter((fact) =>
@@ -469,6 +488,7 @@ function validateAgentStateTransition(
469
488
  || (next.profile.kind !== 'AICtrlWorkflow' && next.profile.kind !== 'AIDataWorkflow')) return;
470
489
  const before = current.profile.ai;
471
490
  const after = next.profile.ai;
491
+ validateWorkflowDelegationTransition(before.delegationsByKey ?? {}, after.delegationsByKey ?? {});
472
492
  for (const [instanceId, instance] of Object.entries(before.instancesById)) {
473
493
  if (!sameClosedValue(instance, after.instancesById[instanceId])) {
474
494
  throw new FlowRunCheckpointTransitionError(`Accepted Agent instance ${instanceId} is immutable and cannot disappear.`);
@@ -553,9 +573,38 @@ export function validateFlowRunCheckpointTransition(
553
573
  );
554
574
  }
555
575
  validateStepExtensionTransition(current, next);
576
+ validateFrozenCtrlExpansion(current, next);
556
577
  validateAgentStateTransition(current, next);
557
578
  }
558
579
 
580
+ function validateFrozenCtrlExpansion(current: AIWorkflowFlowRunCheckpoint, next: AIWorkflowFlowRunCheckpoint): void {
581
+ if (current.profile.kind === 'AIDataWorkflow' || next.profile.kind === 'AIDataWorkflow') return;
582
+ function object(value: FlowClosedValue | undefined): FlowClosedObject {
583
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new FlowRunCheckpointTransitionError('Ctrl orchestration facts are missing.');
584
+ return value as FlowClosedObject;
585
+ }
586
+ const before = object(current.profile.snapshot.orchestration);
587
+ const after = object(next.profile.snapshot.orchestration);
588
+ const composites = object(before.composites);
589
+ const nextComposites = object(after.composites);
590
+ const lanes = object(before.lanes), nextLanes = object(after.lanes);
591
+ for (const [key, value] of Object.entries(composites)) {
592
+ const fact = object(value);
593
+ if (!fact.expansion) continue;
594
+ const candidate = object(nextComposites[key]);
595
+ if (!sameClosedValue(fact.expansion, candidate.expansion)) throw new FlowRunCheckpointTransitionError(`ForEach ${key} cannot rewrite frozen expansion.`);
596
+ for (const [laneKey, value] of Object.entries(lanes)) {
597
+ const lane = object(value);
598
+ if (lane.invocationKey !== key) continue;
599
+ const candidateLane = object(nextLanes[laneKey]);
600
+ for (const field of ['invocationKey', 'laneId', 'bodyId', 'item']) {
601
+ if (!sameClosedValue(lane[field], candidateLane[field])) throw new FlowRunCheckpointTransitionError(`ForEach ${key} cannot rewrite frozen lane ${laneKey}.${field}.`);
602
+ }
603
+ if (lane.started === true && candidateLane.started !== true) throw new FlowRunCheckpointTransitionError(`ForEach ${key} cannot release an unsettled lane slot by rewriting its claim.`);
604
+ }
605
+ }
606
+ }
607
+
559
608
  function validateStepExtensionTransition(
560
609
  current: AIWorkflowFlowRunCheckpoint,
561
610
  next: AIWorkflowFlowRunCheckpoint,
@@ -656,3 +705,4 @@ export async function projectFlowRunCheckpoint(
656
705
  const checkpoint = await loadFlowRunCheckpoint(runtime, input);
657
706
  return checkpoint === undefined ? undefined : normalizeFlowRunCheckpoint(checkpoint);
658
707
  }
708
+ import { validateWorkflowDelegationTransition } from './delegation.js';