@bastani/atomic 0.9.19 → 0.9.20-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2304,6 +2304,13 @@ function isTopLevelWorkflowRun(run) {
2304
2304
  }
2305
2305
 
2306
2306
  // dist/builtin/workflows/src/shared/timing.ts
2307
+ function stageTimingFields(stage = {}) {
2308
+ return {
2309
+ ...stage.startedAt !== undefined ? { startedAt: stage.startedAt } : {},
2310
+ ...stage.endedAt !== undefined ? { endedAt: stage.endedAt } : {},
2311
+ ...stage.durationMs !== undefined ? { durationMs: stage.durationMs } : {}
2312
+ };
2313
+ }
2307
2314
  function nonNegative(ms) {
2308
2315
  return Math.max(0, ms);
2309
2316
  }
@@ -2329,6 +2336,8 @@ function elapsedStageMs(stage, now = Date.now()) {
2329
2336
  return nonNegative(stage.durationMs);
2330
2337
  if (stage.startedAt === undefined)
2331
2338
  return;
2339
+ if (stage.replayed && stage.endedAt === undefined)
2340
+ return;
2332
2341
  const effectiveNow = stage.endedAt ?? now;
2333
2342
  return elapsedFromStart(stage.startedAt, effectiveNow, stage.pausedDurationMs, stage.pausedAt);
2334
2343
  }
@@ -5994,6 +6003,7 @@ function cachedStageId(runId, replayKey) {
5994
6003
  function stageMetadataCheckpointId(replayKey, stage) {
5995
6004
  return `${stableCheckpointId("stage-meta", replayKey)}:${durableHash({
5996
6005
  stageId: stage.id,
6006
+ ...stage.replayed === true ? { replayed: true } : {},
5997
6007
  status: stage.status,
5998
6008
  endedAt: stage.endedAt ?? 0,
5999
6009
  durationMs: stage.durationMs ?? 0,
@@ -6001,11 +6011,9 @@ function stageMetadataCheckpointId(replayKey, stage) {
6001
6011
  })}`;
6002
6012
  }
6003
6013
  function recordCachedStageIntoStore(store, runId, name, replayKey, output, completedStageReplayKeys, parentIds, checkpoint) {
6004
- const now = Date.now();
6005
6014
  const sourceStageId = checkpoint?.topology?.run?.runId === runId ? checkpoint.topology.stageId : undefined;
6006
6015
  const stageId = sourceStageId ?? cachedStageId(runId, replayKey);
6007
6016
  const result = checkpoint?.result ?? (typeof output === "string" ? output : JSON.stringify(output));
6008
- const endedAt = checkpoint?.endedAt ?? checkpoint?.completedAt ?? now;
6009
6017
  const hasCurrentIdentity = checkpoint?.topology?.sourceOrder !== undefined || checkpoint?.topology?.status !== undefined || checkpoint?.topology?.occurrenceKey !== undefined || checkpoint?.topology?.boundary !== undefined;
6010
6018
  const childResult = parseWorkflowChildResult(output) ?? (hasCurrentIdentity ? undefined : parseLegacyWorkflowChildResult(output));
6011
6019
  const workflowChild = childResult === undefined ? undefined : workflowChildSnapshotFromResult(childResult);
@@ -6015,9 +6023,7 @@ function recordCachedStageIntoStore(store, runId, name, replayKey, output, compl
6015
6023
  name,
6016
6024
  status: "completed",
6017
6025
  parentIds: parentIds !== undefined ? Object.freeze([...parentIds]) : [],
6018
- startedAt: checkpoint?.startedAt ?? endedAt,
6019
- endedAt,
6020
- durationMs: checkpoint?.durationMs ?? 0,
6026
+ ...stageTimingFields(checkpoint),
6021
6027
  result,
6022
6028
  replayKey,
6023
6029
  replayed: true,
@@ -8908,7 +8914,7 @@ function appendStageStart(api, payload) {
8908
8914
  ...payload.replayKey !== undefined ? { replayKey: payload.replayKey } : {},
8909
8915
  ...payload.replayedFromStageId !== undefined ? { replayedFromStageId: payload.replayedFromStageId } : {},
8910
8916
  ...payload.replayed !== undefined ? { replayed: payload.replayed } : {},
8911
- ts: payload.ts
8917
+ ...payload.ts !== undefined ? { ts: payload.ts } : {}
8912
8918
  });
8913
8919
  }
8914
8920
  function appendStageEnd(api, payload, opts) {
@@ -8919,6 +8925,7 @@ function appendStageEnd(api, payload, opts) {
8919
8925
  stageId: payload.stageId,
8920
8926
  status: payload.status,
8921
8927
  ...payload.durationMs !== undefined ? { durationMs: payload.durationMs } : {},
8928
+ ...payload.endedAt !== undefined ? { endedAt: payload.endedAt } : {},
8922
8929
  ...payload.summary !== undefined ? { summary: payload.summary } : {},
8923
8930
  ...payload.error !== undefined ? { error: payload.error } : {},
8924
8931
  ...payload.failureKind !== undefined ? { failureKind: payload.failureKind } : {},
@@ -9622,6 +9629,26 @@ function createContinuationReplayIndex(continuation, sourceToContinuationNodeIds
9622
9629
  }
9623
9630
 
9624
9631
  // dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts
9632
+ function continuationPromptAnswer(store, sourceRun, sourceStage) {
9633
+ let run = sourceRun;
9634
+ let stage = sourceStage;
9635
+ const visited = new Set;
9636
+ while (!visited.has(stage.id)) {
9637
+ visited.add(stage.id);
9638
+ const answer = store.getStagePromptAnswer(run.id, stage.id);
9639
+ if (answer !== undefined)
9640
+ return answer;
9641
+ if (!stage.replayed || stage.replayedFromStageId === undefined || run.resumedFromRunId === undefined)
9642
+ break;
9643
+ const parentRun = store.runs().find((candidate) => candidate.id === run.resumedFromRunId);
9644
+ const parentStage = parentRun?.stages.find((candidate) => candidate.id === stage.replayedFromStageId);
9645
+ if (parentRun === undefined || parentStage === undefined)
9646
+ break;
9647
+ run = parentRun;
9648
+ stage = parentStage;
9649
+ }
9650
+ return;
9651
+ }
9625
9652
  function buildPromptNodeUiAdapter(input) {
9626
9653
  const ask = async (descriptor, durableReplay) => {
9627
9654
  input.throwIfWorkflowExitSelected();
@@ -9636,6 +9663,9 @@ function buildPromptNodeUiAdapter(input) {
9636
9663
  const prompt = makePrompt(descriptor);
9637
9664
  const replayKey = promptReplayKey(descriptor);
9638
9665
  const durableTopology = input.durableTopologyForReplayKey?.(replayKey);
9666
+ if (durableTopology?.status === "completed" && durableReplay === undefined) {
9667
+ throw new Error(`insufficient_state: missing durable UI answer for completed prompt ${durableTopology.stageId}`);
9668
+ }
9639
9669
  const stageId = durableTopology?.stageId ?? crypto.randomUUID();
9640
9670
  const provisionalParentIds = input.tracker.onSpawn(stageId, descriptor.kind);
9641
9671
  const replayDecision = input.replayIndex.decide({
@@ -9649,7 +9679,7 @@ function buildPromptNodeUiAdapter(input) {
9649
9679
  if (!sameStringSet(parentIds, provisionalParentIds))
9650
9680
  input.tracker.replaceParents(stageId, parentIds);
9651
9681
  const replaySource = replayDecision.source;
9652
- const continuationAnswer = replayDecision.kind === "replay" ? input.activeStore.getStagePromptAnswer(input.opts.continuation.source.id, replayDecision.source.id) : undefined;
9682
+ const continuationAnswer = replayDecision.kind === "replay" ? continuationPromptAnswer(input.activeStore, input.opts.continuation.source, replayDecision.source) : undefined;
9653
9683
  const replayAnswer = durableReplay === undefined ? continuationAnswer : { value: durableReplay.response };
9654
9684
  const shouldReplay = replayAnswer !== undefined;
9655
9685
  if (shouldReplay && durableReplay === undefined)
@@ -9662,13 +9692,11 @@ function buildPromptNodeUiAdapter(input) {
9662
9692
  replayKey,
9663
9693
  status: shouldReplay ? "completed" : "running",
9664
9694
  parentIds: Object.freeze(parentIds),
9665
- startedAt: prompt.createdAt,
9695
+ ...shouldReplay ? stageTimingFields(durableReplay === undefined ? replaySource : input.durableTimingForStageId?.(stageId)) : { startedAt: prompt.createdAt },
9666
9696
  promptFootprint: { ...prompt },
9667
9697
  toolEvents: [],
9668
9698
  attachable: !shouldReplay,
9669
9699
  ...shouldReplay ? {
9670
- endedAt: prompt.createdAt,
9671
- durationMs: 0,
9672
9700
  promptAnswerState: promptAnswerStatus,
9673
9701
  replayedFromStageId: replaySourceId,
9674
9702
  replayed: true
@@ -9698,8 +9726,10 @@ function buildPromptNodeUiAdapter(input) {
9698
9726
  pauseGate = undefined;
9699
9727
  currentPauseGate?.resolve();
9700
9728
  stageSnapshot.status = status;
9701
- stageSnapshot.endedAt = Date.now();
9702
- stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
9729
+ if (!shouldReplay) {
9730
+ stageSnapshot.endedAt = Date.now();
9731
+ stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
9732
+ }
9703
9733
  input.activeStore.recordStageAttachable(input.runId, stageId, false);
9704
9734
  input.activeStore.recordStageEnd(input.runId, stageSnapshot);
9705
9735
  await input.opts.onStageEnd?.(input.runId, stageSnapshot);
@@ -9709,6 +9739,7 @@ function buildPromptNodeUiAdapter(input) {
9709
9739
  stageId,
9710
9740
  status: stageSnapshot.status,
9711
9741
  durationMs: stageSnapshot.durationMs,
9742
+ endedAt: stageSnapshot.endedAt,
9712
9743
  ...stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {},
9713
9744
  ...stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {},
9714
9745
  ...stageSnapshot.failureCode !== undefined ? { failureCode: stageSnapshot.failureCode } : {},
@@ -9775,7 +9806,7 @@ function buildPromptNodeUiAdapter(input) {
9775
9806
  name: stageSnapshot.name,
9776
9807
  parentIds: stageSnapshot.parentIds,
9777
9808
  ...stageReplayFields(stageSnapshot),
9778
- ts: prompt.createdAt
9809
+ ts: stageSnapshot.startedAt
9779
9810
  });
9780
9811
  }
9781
9812
  if (shouldReplay) {
@@ -11312,7 +11343,7 @@ function wrapUiWithDurable(base, deps) {
11312
11343
  // dist/builtin/workflows/src/engine/primitives/ui.ts
11313
11344
  function buildExitGatedUiContext(input) {
11314
11345
  const base = input.opts.usePromptNodesForUi === true ? input.baseFromPromptNodes() : input.opts.executionMode === "non_interactive" && input.opts.ui === undefined ? makeHeadlessUnavailableUIContext() : normalizeUIContext(input.opts.ui);
11315
- const promptNodeReplay = input.opts.usePromptNodesForUi === true && input.opts.continuation !== undefined;
11346
+ const promptNodeReplay = input.opts.usePromptNodesForUi === true && input.opts.continuation !== undefined && input.opts.continuation.source.id !== input.durableUi?.workflowId;
11316
11347
  const durableBase = input.durableUi !== undefined && !promptNodeReplay ? wrapUiWithDurable(base, input.durableUi) : base;
11317
11348
  const invoke = (call) => {
11318
11349
  input.throwIfWorkflowExitSelected();
@@ -11935,7 +11966,7 @@ function mergeStageDraft(existing, checkpoint, sequence) {
11935
11966
  ...valueOrExisting("thinkingLevel", checkpoint, existing),
11936
11967
  ...valueOrExisting("attemptedModels", checkpoint, existing),
11937
11968
  ...valueOrExisting("modelAttempts", checkpoint, existing),
11938
- ...checkpoint.topology !== undefined ? { topology: checkpoint.topology } : existing?.topology !== undefined ? { topology: existing.topology } : {}
11969
+ ...existing?.topology?.run !== undefined && checkpoint.topology?.run === undefined ? { topology: existing.topology } : checkpoint.topology !== undefined ? { topology: checkpoint.topology } : existing?.topology !== undefined ? { topology: existing.topology } : {}
11939
11970
  };
11940
11971
  }
11941
11972
  function valueOrExisting(key, checkpoint, existing) {
@@ -13226,13 +13257,11 @@ function createWorkflowBoundaryFactory(input) {
13226
13257
  replayKey,
13227
13258
  status: replayedChild !== undefined ? "completed" : "running",
13228
13259
  parentIds: Object.freeze([...parentIds]),
13229
- startedAt,
13260
+ ...replayedChild === undefined ? { startedAt } : stageTimingFields(replaySource),
13230
13261
  toolEvents: [],
13231
13262
  attachable: false,
13232
13263
  ...replaySource !== undefined ? { replayedFromStageId: replaySource.id, replayed: replayedChild !== undefined } : {},
13233
13264
  ...replayedChild !== undefined && replayChildSnapshot !== undefined ? {
13234
- endedAt: startedAt,
13235
- durationMs: 0,
13236
13265
  ...replayDecision.kind === "replay" && replayDecision.source.result !== undefined ? { result: replayDecision.source.result } : {},
13237
13266
  workflowChild: cloneWorkflowChildReplaySnapshot(replayChildSnapshot)
13238
13267
  } : {}
@@ -13249,7 +13278,7 @@ function createWorkflowBoundaryFactory(input) {
13249
13278
  name,
13250
13279
  parentIds: stageSnapshot.parentIds,
13251
13280
  ...stageReplayFields(stageSnapshot),
13252
- ts: startedAt
13281
+ ts: stageSnapshot.startedAt
13253
13282
  });
13254
13283
  };
13255
13284
  const appendStageEndForSnapshot = () => {
@@ -13260,6 +13289,7 @@ function createWorkflowBoundaryFactory(input) {
13260
13289
  stageId,
13261
13290
  status: stageSnapshot.status,
13262
13291
  durationMs: stageSnapshot.durationMs,
13292
+ endedAt: stageSnapshot.endedAt,
13263
13293
  ...stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {},
13264
13294
  ...stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {},
13265
13295
  ...stageSnapshot.failureCode !== undefined ? { failureCode: stageSnapshot.failureCode } : {},
@@ -13296,8 +13326,10 @@ function createWorkflowBoundaryFactory(input) {
13296
13326
  clearBoundaryChildMetadata();
13297
13327
  applyFailureToStage(stageSnapshot, input.classifyExecutorFailure(failureError));
13298
13328
  }
13299
- stageSnapshot.endedAt = Date.now();
13300
- stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
13329
+ if (replayedChild === undefined) {
13330
+ stageSnapshot.endedAt = Date.now();
13331
+ stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
13332
+ }
13301
13333
  input.activeStore.recordStageEnd(input.runId, stageSnapshot);
13302
13334
  input.opts.onStageEnd?.(input.runId, stageSnapshot);
13303
13335
  appendStageEndForSnapshot();
@@ -13365,6 +13397,12 @@ function createWorkflowBoundaryFactory(input) {
13365
13397
  // dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts
13366
13398
  import { runCallback as runCallback3, runSynchronousCallback } from "@bastani/atomic";
13367
13399
 
13400
+ // dist/builtin/workflows/src/shared/pending-stage-route-readiness.ts
13401
+ var owners = new WeakMap;
13402
+ function workflowPendingStageRouteReady(store, runId) {
13403
+ return owners.get(store)?.ready(runId);
13404
+ }
13405
+
13368
13406
  // dist/builtin/workflows/src/runs/foreground/executor-queued-user-message.ts
13369
13407
  function removedMessages(before, after) {
13370
13408
  const remaining = [...after];
@@ -14259,7 +14297,7 @@ function createReplayStageContext(input) {
14259
14297
  name,
14260
14298
  parentIds: stageSnapshot.parentIds,
14261
14299
  ...stageReplayFields(stageSnapshot),
14262
- ts: stageSnapshot.startedAt ?? Date.now()
14300
+ ts: stageSnapshot.startedAt
14263
14301
  });
14264
14302
  };
14265
14303
  const appendReplayStageEnd = () => {
@@ -14269,7 +14307,8 @@ function createReplayStageContext(input) {
14269
14307
  runId,
14270
14308
  stageId,
14271
14309
  status: stageSnapshot.status,
14272
- durationMs: stageSnapshot.durationMs ?? 0,
14310
+ durationMs: stageSnapshot.durationMs,
14311
+ endedAt: stageSnapshot.endedAt,
14273
14312
  ...stageSnapshot.status === "completed" && stageSnapshot.result !== undefined ? { summary: stageSnapshot.result } : {},
14274
14313
  ...stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {},
14275
14314
  ...stageSnapshot.sessionId !== undefined ? { sessionId: stageSnapshot.sessionId } : {},
@@ -14287,8 +14326,6 @@ function createReplayStageContext(input) {
14287
14326
  delete stageSnapshot.result;
14288
14327
  stageSnapshot.skippedReason = input.workflowExitSkippedReason(reason);
14289
14328
  }
14290
- stageSnapshot.endedAt = Date.now();
14291
- stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
14292
14329
  input.activeStore.recordStageEnd(runId, stageSnapshot);
14293
14330
  input.opts.onStageEnd?.(runId, stageSnapshot);
14294
14331
  appendReplayStageEnd();
@@ -15673,6 +15710,21 @@ class StageSessionBindingCleanupFailure extends AggregateError {
15673
15710
  this.name = "StageSessionBindingCleanupFailure";
15674
15711
  }
15675
15712
  }
15713
+ async function cleanupFailedStageSessionBinding(current, bindingError) {
15714
+ const cleanupErrors = [];
15715
+ try {
15716
+ await shutdownStageSession(current);
15717
+ } catch (error) {
15718
+ cleanupErrors.push(error);
15719
+ }
15720
+ try {
15721
+ await current.dispose();
15722
+ } catch (error) {
15723
+ cleanupErrors.push(error);
15724
+ }
15725
+ if (cleanupErrors.length > 0)
15726
+ throw new StageSessionBindingCleanupFailure(bindingError, cleanupErrors);
15727
+ }
15676
15728
  async function disposeStageSession(current) {
15677
15729
  if (!current)
15678
15730
  return;
@@ -15993,6 +16045,27 @@ function terminatingToolCallId(event) {
15993
16045
  return typeof callId === "string" && callId.length > 0 ? callId : undefined;
15994
16046
  }
15995
16047
 
16048
+ // dist/builtin/workflows/src/runs/foreground/stage-startup-wait.ts
16049
+ function waitForStageStartup(operation, signal) {
16050
+ return new Promise((resolve, reject) => {
16051
+ const onAbort = () => {
16052
+ signal.removeEventListener("abort", onAbort);
16053
+ reject(signal.reason ?? new DOMException("Stage startup cancelled", "AbortError"));
16054
+ };
16055
+ operation.then((value) => {
16056
+ signal.removeEventListener("abort", onAbort);
16057
+ resolve(value);
16058
+ }, (error) => {
16059
+ signal.removeEventListener("abort", onAbort);
16060
+ reject(error);
16061
+ });
16062
+ if (signal.aborted)
16063
+ onAbort();
16064
+ else
16065
+ signal.addEventListener("abort", onAbort, { once: true });
16066
+ });
16067
+ }
16068
+
15996
16069
  // dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts
15997
16070
  function hasMeaningfulUsage(usage) {
15998
16071
  if (usage === undefined)
@@ -16073,6 +16146,9 @@ class StageSessionController {
16073
16146
  activeCreation;
16074
16147
  ownedCreationPromise;
16075
16148
  abortGeneration = 0;
16149
+ routeAuthorityWait;
16150
+ startupWait = new AbortController;
16151
+ startup;
16076
16152
  abortReason;
16077
16153
  abortReasonGeneration = 0;
16078
16154
  sessionPromise;
@@ -16232,25 +16308,28 @@ class StageSessionController {
16232
16308
  }
16233
16309
  if (this.disposed)
16234
16310
  throw new Error(`atomic-workflows: stage "${this.opts.stageName}" session has been disposed`);
16235
- if (this.session !== undefined)
16311
+ this.opts.signal?.throwIfAborted();
16312
+ if (this.session !== undefined && this.activeCreation === undefined)
16236
16313
  return this.session;
16237
16314
  if (!this.sessionPromise) {
16315
+ this.beginStartup();
16238
16316
  const pending = this.createInitialSession(consumer);
16239
16317
  this.sessionPromise = pending;
16240
16318
  this.ownedCreationPromise = pending;
16241
- const release = () => {
16319
+ const release = (failed = false) => {
16242
16320
  if (this.ownedCreationPromise === pending)
16243
16321
  this.ownedCreationPromise = undefined;
16322
+ this.settleStartup(failed);
16244
16323
  };
16245
- pending.then(release, () => {
16246
- release();
16324
+ pending.then(() => release(), () => {
16325
+ release(true);
16247
16326
  if (this.sessionPromise === pending) {
16248
16327
  this.sessionPromise = undefined;
16249
16328
  this.activeCandidateIndex = undefined;
16250
16329
  }
16251
16330
  });
16252
16331
  }
16253
- return this.sessionPromise;
16332
+ return waitForStageStartup(this.sessionPromise, this.startupWait.signal);
16254
16333
  }
16255
16334
  async ensureSessionFromFile(sessionFile, consumer = "prompt") {
16256
16335
  if (!this.sessionShutdownPromise && !this.sessionPromise && !this.session)
@@ -16298,6 +16377,10 @@ class StageSessionController {
16298
16377
  this.artifactCapture.close();
16299
16378
  }
16300
16379
  async promptWithFallback(text, sdkOptions, consumer = "prompt") {
16380
+ if (this.session !== undefined && this.activeCreation === undefined && this.ownedCreationPromise === undefined && this.startupWait.signal.aborted) {
16381
+ this.opts.signal?.throwIfAborted();
16382
+ this.startupWait = new AbortController;
16383
+ }
16301
16384
  if (!this.hasExplicitModelFallbackConfig) {
16302
16385
  try {
16303
16386
  const activeSession = await this.ensureSession(consumer);
@@ -16320,7 +16403,13 @@ class StageSessionController {
16320
16403
  }
16321
16404
  return;
16322
16405
  }
16323
- const candidates = await this.modelCandidates();
16406
+ const readinessOwner = this.ownedCreationPromise ?? this.activeCreation;
16407
+ if (readinessOwner !== undefined)
16408
+ await waitForStageStartup(readinessOwner, this.startupWait.signal);
16409
+ if (this.session === undefined)
16410
+ this.beginStartup();
16411
+ const startupSignal = this.startupWait.signal;
16412
+ const candidates = await waitForStageStartup(this.modelCandidates(), startupSignal);
16324
16413
  if (candidates.length === 0) {
16325
16414
  try {
16326
16415
  const activeSession = await this.ensureSession(consumer);
@@ -16336,14 +16425,9 @@ class StageSessionController {
16336
16425
  }
16337
16426
  return;
16338
16427
  }
16339
- if (this.session === undefined && this.sessionPromise !== undefined) {
16340
- try {
16341
- await this.sessionPromise;
16342
- } catch (error) {
16343
- if (error instanceof StageSessionCreationCancelled)
16344
- return;
16345
- }
16346
- }
16428
+ const creationOwner = this.ownedCreationPromise ?? this.activeCreation;
16429
+ if (creationOwner !== undefined)
16430
+ await waitForStageStartup(creationOwner, startupSignal);
16347
16431
  const resumedText = this.pendingCreationResumeMessage;
16348
16432
  this.pendingCreationResumeMessage = undefined;
16349
16433
  let promptText = resumedText ?? text;
@@ -16351,9 +16435,9 @@ class StageSessionController {
16351
16435
  return;
16352
16436
  let index = this.activeCandidateIndex ?? 0;
16353
16437
  while (index < candidates.length) {
16354
- if (this.session === undefined && this.ownedCreationPromise !== undefined) {
16438
+ if (this.ownedCreationPromise !== undefined || this.activeCreation !== undefined) {
16355
16439
  try {
16356
- await this.ownedCreationPromise;
16440
+ await waitForStageStartup(this.ownedCreationPromise ?? this.activeCreation, startupSignal);
16357
16441
  } catch (error) {
16358
16442
  if (error instanceof StageSessionCreationCancelled)
16359
16443
  return;
@@ -16365,7 +16449,7 @@ class StageSessionController {
16365
16449
  }
16366
16450
  const candidate = candidates[index];
16367
16451
  try {
16368
- const created = this.session && this.activeCandidateIndex === index ? this.session : await this.createSessionWithThrownErrorRetry(candidate, consumer);
16452
+ const created = this.session && this.activeCandidateIndex === index ? this.session : await waitForStageStartup(this.createSessionWithThrownErrorRetry(candidate, consumer), startupSignal);
16369
16453
  if (isSessionCreationPauseResult(created)) {
16370
16454
  if (created.resumeMessage === undefined)
16371
16455
  return;
@@ -16392,8 +16476,10 @@ class StageSessionController {
16392
16476
  const failure = await this.handleCandidateFailure(err, candidate, candidates, index);
16393
16477
  if (failure === "handled")
16394
16478
  return;
16395
- if (failure === "throw")
16479
+ if (failure === "throw") {
16480
+ this.settleStartup(true);
16396
16481
  throw err;
16482
+ }
16397
16483
  index += 1;
16398
16484
  }
16399
16485
  }
@@ -16436,7 +16522,8 @@ class StageSessionController {
16436
16522
  this.messageAdmission.dispose();
16437
16523
  this.deliveryActivity.dispose();
16438
16524
  await this.replacement.dispose();
16439
- await disposeStageSession(this.session);
16525
+ if (this.activeCreation === undefined)
16526
+ await disposeStageSession(this.session);
16440
16527
  }
16441
16528
  async drainPendingDisposal() {
16442
16529
  if (!this.pendingDisposal)
@@ -16510,6 +16597,12 @@ class StageSessionController {
16510
16597
  this.abortGeneration += 1;
16511
16598
  this.abortReason = reason;
16512
16599
  this.abortReasonGeneration = this.abortGeneration;
16600
+ this.routeAuthorityWait?.abort(reason);
16601
+ this.startupWait.abort(reason);
16602
+ if (this.startup?.state === "active") {
16603
+ this.startup = { ...this.startup, state: "cancelled" };
16604
+ this.opts.onStartupChange?.(this.startup);
16605
+ }
16513
16606
  this.abortThrownErrorRetries(reason);
16514
16607
  }
16515
16608
  pauseThrownErrorRetries(resume) {
@@ -16750,10 +16843,13 @@ class StageSessionController {
16750
16843
  return this.candidatesPromise;
16751
16844
  }
16752
16845
  async createInitialSession(consumer) {
16846
+ const generation = this.abortGeneration;
16753
16847
  if (!this.hasExplicitModelFallbackConfig) {
16754
16848
  return this.createSessionObservingPause(undefined, consumer).catch((error) => this.createInitialSessionWithRetry(undefined, consumer, { error }));
16755
16849
  }
16756
16850
  const candidates = await this.modelCandidates();
16851
+ if (this.abortGeneration !== generation || this.opts.signal?.aborted || this.disposed)
16852
+ throw this.staleCreationReason(generation);
16757
16853
  const initialIndex = this.activeCandidateIndex ?? 0;
16758
16854
  const first = candidates[initialIndex];
16759
16855
  if (first === undefined) {
@@ -16858,7 +16954,7 @@ class StageSessionController {
16858
16954
  if (errorSettingsManager !== undefined)
16859
16955
  this.sessionSettingsManager = errorSettingsManager;
16860
16956
  const decision = isWorkflowPendingStageDeliveryFailure(error) ? undefined : nextRetryDecision(this.retrySettings(), retryAttempt, isRetryableSameModelFailure(error));
16861
- if (decision === undefined || this.disposed || this.opts.signal?.aborted === true || this.capturedStructuredOutputForAttempt()) {
16957
+ if (decision === undefined || this.disposed || this.opts.signal?.aborted === true || this.startupWait.signal.aborted || this.capturedStructuredOutputForAttempt()) {
16862
16958
  throw error;
16863
16959
  }
16864
16960
  retryAttempt = decision.attempt;
@@ -16901,16 +16997,72 @@ class StageSessionController {
16901
16997
  return this.activeCreation;
16902
16998
  if (this.session !== undefined)
16903
16999
  return Promise.resolve(this.session);
17000
+ this.startupWait.signal.throwIfAborted();
16904
17001
  const creation = this.createSessionAttempt(candidate, consumer, resumeOptions);
16905
17002
  this.activeCreation = creation;
16906
17003
  creation.finally(() => {
16907
17004
  if (this.activeCreation === creation)
16908
17005
  this.activeCreation = undefined;
17006
+ this.settleStartup();
16909
17007
  }).catch(() => {});
16910
17008
  return creation;
16911
17009
  }
17010
+ settleStartup(failed = false) {
17011
+ if (this.activeCreation !== undefined || this.ownedCreationPromise !== undefined || this.startup === undefined)
17012
+ return;
17013
+ if (this.startup.state === "dispatched" || this.startup.phase === "ready")
17014
+ return;
17015
+ if (this.startup.state === "active" && !failed && this.bindingCleanupFailure === undefined)
17016
+ return;
17017
+ this.startup = {
17018
+ ...this.startup,
17019
+ state: this.startup.state === "active" ? "failed" : this.startup.state,
17020
+ ownershipPending: this.bindingCleanupFailure !== undefined,
17021
+ ...this.bindingCleanupFailure === undefined ? { settledAt: Date.now() } : {}
17022
+ };
17023
+ this.opts.onStartupChange?.(this.startup);
17024
+ }
17025
+ beginStartup() {
17026
+ if (this.activeCreation !== undefined || this.ownedCreationPromise !== undefined || this.sessionPromise !== undefined)
17027
+ return;
17028
+ this.opts.signal?.throwIfAborted();
17029
+ if (this.startupWait.signal.aborted)
17030
+ this.startupWait = new AbortController;
17031
+ this.reportStartupPhase("model-resolution", true);
17032
+ }
17033
+ reportStartupPhase(phase, reset = false) {
17034
+ if (!reset && this.startup !== undefined && this.startup.state !== "active")
17035
+ return;
17036
+ const now = Date.now();
17037
+ this.startup = {
17038
+ phase,
17039
+ startedAt: reset ? now : this.startup?.startedAt ?? now,
17040
+ phaseStartedAt: now,
17041
+ state: phase === "first-dispatch" ? "dispatched" : "active",
17042
+ ownershipPending: phase !== "first-dispatch" && phase !== "ready"
17043
+ };
17044
+ this.opts.onStartupChange?.(this.startup);
17045
+ }
16912
17046
  async createSessionAttempt(candidate, consumer, resumeOptions) {
16913
17047
  const startGeneration = this.abortGeneration;
17048
+ if (this.disposed || this.opts.signal?.aborted)
17049
+ throw this.staleCreationReason(startGeneration);
17050
+ this.reportStartupPhase("route-authority");
17051
+ const authority = this.opts.routeAuthorityReady?.();
17052
+ if (authority !== undefined) {
17053
+ const wait = new AbortController;
17054
+ this.routeAuthorityWait = wait;
17055
+ try {
17056
+ await raceAbort2(authority.completion, wait.signal);
17057
+ authority.assertCurrent();
17058
+ if (this.disposed || this.opts.signal?.aborted || this.abortGeneration !== startGeneration)
17059
+ throw this.staleCreationReason(startGeneration);
17060
+ } finally {
17061
+ if (this.routeAuthorityWait === wait)
17062
+ this.routeAuthorityWait = undefined;
17063
+ }
17064
+ }
17065
+ this.reportStartupPhase("resource-preparation");
16914
17066
  this.applyCandidateThinking(candidate);
16915
17067
  const stageOptions = buildStageSessionOptions({
16916
17068
  effectiveStageOptions: this.effectiveStageOptions,
@@ -16923,6 +17075,11 @@ class StageSessionController {
16923
17075
  try {
16924
17076
  created = this.opts.adapters.agentSession ? await this.opts.adapters.agentSession.create(stripWorkflowOnlyOptions(stageOptions, this.opts.defaultSessionDir, this.meta, this.opts.pendingStageDelivery), {
16925
17077
  ...this.meta,
17078
+ startupSignal: this.startupWait.signal,
17079
+ onStartupPhase: (phase) => {
17080
+ if (this.abortGeneration === startGeneration)
17081
+ this.reportStartupPhase(phase);
17082
+ },
16926
17083
  stageOptions,
16927
17084
  ...this.sharedOrchestrationContext !== undefined ? { orchestrationContext: this.sharedOrchestrationContext } : {}
16928
17085
  }) : missingAdapter(consumer);
@@ -16941,20 +17098,28 @@ class StageSessionController {
16941
17098
  throw new Error(`atomic-workflows: stage "${this.opts.stageName}" session has been disposed`);
16942
17099
  throw this.staleCreationReason(startGeneration);
16943
17100
  }
17101
+ this.reportStartupPhase("session-attachment");
16944
17102
  const session = attachCreatedStageSession(created, this.disposed, this.opts.stageName, (result) => this.attachSession(result));
16945
17103
  const attachedSession = session instanceof Promise ? await session : session;
16946
- const pendingStageDeliveryReady = this.sharedOrchestrationContext?.pendingStageDelivery?.ready();
16947
- if (pendingStageDeliveryReady !== undefined) {
17104
+ this.reportStartupPhase("delivery-readiness");
17105
+ try {
17106
+ await this.sharedOrchestrationContext?.pendingStageDelivery?.ready();
17107
+ await this.opts.onSessionReady?.();
17108
+ if (this.disposed || this.opts.signal?.aborted || this.abortGeneration !== startGeneration)
17109
+ throw this.staleCreationReason(startGeneration);
17110
+ } catch (reason) {
17111
+ if (this.session === attachedSession)
17112
+ this.session = undefined;
16948
17113
  try {
16949
- await pendingStageDeliveryReady;
17114
+ await cleanupFailedStageSessionBinding(attachedSession, reason);
16950
17115
  } catch (error) {
16951
- if (this.session === attachedSession)
16952
- this.session = undefined;
16953
- await disposeStageSession(attachedSession).catch(() => {});
17116
+ if (error instanceof StageSessionBindingCleanupFailure)
17117
+ this.bindingCleanupFailure = error;
16954
17118
  throw error;
16955
17119
  }
17120
+ throw reason;
16956
17121
  }
16957
- await this.opts.onSessionReady?.();
17122
+ this.reportStartupPhase("ready");
16958
17123
  return attachedSession;
16959
17124
  }
16960
17125
  attachSession(created) {
@@ -17040,6 +17205,7 @@ class StageSessionController {
17040
17205
  this.lastPromptStartIndex = promptStartIndex;
17041
17206
  this.unresolvedContextOverflowMessage = undefined;
17042
17207
  try {
17208
+ this.reportStartupPhase("first-dispatch");
17043
17209
  await activeSession.prompt(nextText, sdkOptions);
17044
17210
  const pendingPauseAfterPrompt = this.pauseControl.currentResume();
17045
17211
  if (pendingPauseAfterPrompt) {
@@ -17154,7 +17320,7 @@ class StageSessionController {
17154
17320
  ...usage === undefined ? {} : { usage },
17155
17321
  error: message
17156
17322
  });
17157
- if (this.opts.signal?.aborted || terminalStageDelivery || !isRetryableModelFailure(err) || index === candidates.length - 1) {
17323
+ if (this.opts.signal?.aborted || this.startupWait.signal.aborted || terminalStageDelivery || !isRetryableModelFailure(err) || index === candidates.length - 1) {
17158
17324
  this.modelWarnings.push(...this.pendingFallbackWarnings);
17159
17325
  this.pendingFallbackWarnings.length = 0;
17160
17326
  this.notifyModelFallbackMetaChange();
@@ -17803,9 +17969,7 @@ function createWorkflowStageFactory(input) {
17803
17969
  toolEvents: [],
17804
17970
  pendingStageDeliveryAvailable,
17805
17971
  ...shouldReplay ? {
17806
- startedAt: Date.now(),
17807
- endedAt: Date.now(),
17808
- durationMs: 0,
17972
+ ...stageTimingFields(replaySource),
17809
17973
  ...replaySource.result !== undefined ? { result: replaySource.result } : {},
17810
17974
  ...replaySource.sessionId !== undefined ? { sessionId: replaySource.sessionId } : {},
17811
17975
  ...replaySource.sessionFile !== undefined ? { sessionFile: replaySource.sessionFile } : {},
@@ -17868,11 +18032,19 @@ function createWorkflowStageFactory(input) {
17868
18032
  signal: input.signal,
17869
18033
  stageOptions: stageOptionsForContext,
17870
18034
  ...pendingStageDeliveryAvailable ? {
18035
+ routeAuthorityReady: () => workflowPendingStageRouteReady(input.activeStore, input.runId),
17871
18036
  pendingStageDelivery: createWorkflowPendingStageDelivery(input.activeStore, input.runId, stageId, name)
17872
18037
  } : {},
17873
18038
  models: input.opts.models,
17874
18039
  executionMode: input.opts.executionMode,
17875
18040
  defaultSessionDir: input.opts.defaultSessionDir,
18041
+ onStartupChange(startup) {
18042
+ const current = input.activeStore.runs().find((run) => run.id === input.runId);
18043
+ if (!current?.stages.includes(stageSnapshot))
18044
+ return;
18045
+ stageSnapshot.startup = startup;
18046
+ input.activeStore.recordStageStart(input.runId, stageSnapshot);
18047
+ },
17876
18048
  onModelFallbackMetaChange(meta) {
17877
18049
  applyModelFallbackMeta(meta);
17878
18050
  if (stageSnapshot.status === "running")
@@ -18035,6 +18207,7 @@ function createWorkflowStageFactory(input) {
18035
18207
  stageId,
18036
18208
  status: stageSnapshot.status,
18037
18209
  durationMs: stageSnapshot.durationMs,
18210
+ endedAt: stageSnapshot.endedAt,
18038
18211
  ...stageSnapshot.error !== undefined ? { error: stageSnapshot.error } : {},
18039
18212
  ...stageSnapshot.failureKind !== undefined ? { failureKind: stageSnapshot.failureKind } : {},
18040
18213
  ...stageSnapshot.failureCode !== undefined ? { failureCode: stageSnapshot.failureCode } : {},
@@ -18640,6 +18813,7 @@ async function run(def, inputs, opts = {}) {
18640
18813
  };
18641
18814
  };
18642
18815
  const resolvePromptNodeTopology = createDurableStageTopologyResolver(durableBackend, runId);
18816
+ const priorPromptStageCheckpoints = durableBackend.listCheckpoints(runId).filter((checkpoint) => checkpoint.kind === "stage");
18643
18817
  let promptNodeUi;
18644
18818
  const getPromptNodeUi = () => {
18645
18819
  promptNodeUi ??= buildPromptNodeUiAdapter({
@@ -18656,6 +18830,7 @@ async function run(def, inputs, opts = {}) {
18656
18830
  workflowExitSkippedReason: exit.workflowExitSkippedReason,
18657
18831
  preserveWorkflowExitSkippedReason: exit.preserveWorkflowExitSkippedReason,
18658
18832
  durableTopologyForReplayKey: resolvePromptNodeTopology,
18833
+ durableTimingForStageId: (stageId) => priorPromptStageCheckpoints.find((checkpoint) => checkpoint.topology?.stageId === stageId && checkpoint.topology.status === "completed" && (checkpoint.topology.run === undefined || checkpoint.topology.run.runId === runId)),
18659
18834
  onPendingStage: async (pendingRunId, snapshot) => pendingRunId === runId ? void await recordDurableActiveStage(durableStageDeps, snapshot) : undefined
18660
18835
  });
18661
18836
  return promptNodeUi;
@@ -112,6 +112,8 @@ Completion creates a shaded notification card in the owning chat without dependi
112
112
 
113
113
  The parent model also receives the result context. The internal receipt stays in structured message details, rather than becoming raw JSON in chat. The same persisted completion identity handles delivery retries without relaunching the child. Workflow completions remain in their owning stage chat, not the main conversation.
114
114
 
115
+ Supervisor progress messages are labelled **Historical supervisor update** with their original `Sent:` timestamp. They describe the child's state when sent, not its current status, and can arrive after completion. Prefer a later correction or final result over an earlier progress hypothesis; use `/tasks` for the current task state.
116
+
115
117
  Stopping a subagent through `/tasks` (`x`, then `y`) also delivers a **stopped** card and stop context to the parent model. The notice arrives after termination is confirmed, not while the task is merely **Stopping**, and does not require a final response from the child. Cancelling queued work notifies without starting it. Repeated stop requests or late child results do not duplicate the notice or overwrite the recorded outcome; a task that finished before cancellation keeps its actual result. Closing the owning session or workflow stage still suppresses late notices.
116
118
 
117
119
  Restored completions may have only an outcome and task identity if the original live task or transcript is unavailable. Atomic does not invent missing output. Excerpts are bounded; inspect retained history for more detail.