@pikku/core 0.12.150 → 0.12.151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/function/function-runner.js +23 -0
  2. package/dist/function/trace-queue-service.d.ts +26 -0
  3. package/dist/function/trace-queue-service.js +48 -0
  4. package/dist/mesh.d.ts +2 -2
  5. package/dist/mesh.js +3 -3
  6. package/dist/middleware/index.d.ts +1 -1
  7. package/dist/middleware/index.js +1 -1
  8. package/dist/middleware/mesh-auth.d.ts +3 -2
  9. package/dist/middleware/mesh-auth.js +7 -2
  10. package/dist/middleware/remote-jobs-secret.d.ts +1 -1
  11. package/dist/middleware/require-origin.d.ts +19 -0
  12. package/dist/middleware/require-origin.js +36 -2
  13. package/dist/services/deployment-service.d.ts +1 -1
  14. package/dist/services/incoming-webhook-service.d.ts +4 -2
  15. package/dist/services/incoming-webhook-service.js +4 -3
  16. package/dist/services/queue-webhook-service.js +7 -1
  17. package/dist/services/remote-jobs.d.ts +6 -1
  18. package/dist/services/scheduler-service.d.ts +3 -1
  19. package/dist/services/webhook-service.d.ts +2 -0
  20. package/dist/services/workflow-service.d.ts +4 -1
  21. package/dist/testing/index.d.ts +4 -0
  22. package/dist/testing/index.js +2 -0
  23. package/dist/testing/queue-identity-transport-tests.d.ts +18 -0
  24. package/dist/testing/queue-identity-transport-tests.js +162 -0
  25. package/dist/testing/queue-trace-conformance.d.ts +26 -0
  26. package/dist/testing/queue-trace-conformance.js +259 -0
  27. package/dist/testing/service-tests/channel-store-tests.js +11 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.d.ts +18 -1
  29. package/dist/testing/service-tests/queued-workflow-harness.js +19 -3
  30. package/dist/testing/service-tests/workflow-service-tests.js +12 -0
  31. package/dist/testing/trace-matrix/assertions.d.ts +3 -0
  32. package/dist/testing/trace-matrix/assertions.js +190 -0
  33. package/dist/testing/trace-matrix/chain-harness.d.ts +119 -0
  34. package/dist/testing/trace-matrix/chain-harness.js +777 -0
  35. package/dist/testing/trace-matrix/dimensions.d.ts +55 -0
  36. package/dist/testing/trace-matrix/dimensions.js +305 -0
  37. package/dist/testing/trace-matrix/pairwise.d.ts +20 -0
  38. package/dist/testing/trace-matrix/pairwise.js +75 -0
  39. package/dist/testing/trace-matrix/report.d.ts +1 -0
  40. package/dist/testing/trace-matrix/report.js +16 -0
  41. package/dist/testing/trace-matrix/transports.d.ts +19 -0
  42. package/dist/testing/trace-matrix/transports.js +27 -0
  43. package/dist/testing/trace-matrix/workflow-cases.d.ts +24 -0
  44. package/dist/testing/trace-matrix/workflow-cases.js +147 -0
  45. package/dist/trace.d.ts +66 -0
  46. package/dist/trace.js +97 -0
  47. package/dist/types/core.types.d.ts +6 -2
  48. package/dist/wirings/addon/addon-reach.d.ts +3 -0
  49. package/dist/wirings/addon/addon-reach.js +9 -0
  50. package/dist/wirings/addon/define-plugin.d.ts +6 -0
  51. package/dist/wirings/addon/wire-addon.js +5 -0
  52. package/dist/wirings/addon/wire-remote-addon.d.ts +11 -0
  53. package/dist/wirings/agent/agent-finalize.d.ts +2 -0
  54. package/dist/wirings/agent/agent-finalize.js +1 -1
  55. package/dist/wirings/agent/agent-prepare.d.ts +3 -0
  56. package/dist/wirings/agent/agent-prepare.js +24 -8
  57. package/dist/wirings/agent/agent-rpc.js +1 -0
  58. package/dist/wirings/agent/agent-runner.js +3 -0
  59. package/dist/wirings/agent/agent-stream.js +4 -0
  60. package/dist/wirings/agent/voice-input.d.ts +1 -1
  61. package/dist/wirings/agent/voice-output.d.ts +1 -1
  62. package/dist/wirings/agent-scorer/agent-scorer-live.d.ts +4 -2
  63. package/dist/wirings/agent-scorer/agent-scorer-live.js +2 -2
  64. package/dist/wirings/channel/channel-common.d.ts +4 -1
  65. package/dist/wirings/channel/channel-common.js +3 -1
  66. package/dist/wirings/channel/channel-handler.js +2 -0
  67. package/dist/wirings/channel/channel-host-rpc.d.ts +1 -1
  68. package/dist/wirings/channel/channel-host-rpc.js +2 -2
  69. package/dist/wirings/channel/channel-rpc-responder.d.ts +7 -1
  70. package/dist/wirings/channel/channel-rpc-responder.js +2 -2
  71. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  72. package/dist/wirings/channel/channel-rpc-service.js +2 -2
  73. package/dist/wirings/channel/channel-rpc.types.d.ts +5 -2
  74. package/dist/wirings/channel/channel-rpc.types.js +1 -1
  75. package/dist/wirings/channel/channel-store.d.ts +4 -2
  76. package/dist/wirings/channel/channel-store.js +2 -2
  77. package/dist/wirings/channel/channel.types.d.ts +3 -0
  78. package/dist/wirings/channel/local/local-channel-runner.js +16 -5
  79. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +3 -1
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +27 -4
  82. package/dist/wirings/gateway/gateway-runner.js +3 -1
  83. package/dist/wirings/http/http-runner.d.ts +4 -4
  84. package/dist/wirings/http/http-runner.js +14 -18
  85. package/dist/wirings/http/http.types.d.ts +3 -3
  86. package/dist/wirings/mcp/mcp-runner.js +15 -5
  87. package/dist/wirings/queue/in-memory-queue-service.d.ts +1 -1
  88. package/dist/wirings/queue/in-memory-queue-service.js +8 -2
  89. package/dist/wirings/queue/index.d.ts +1 -0
  90. package/dist/wirings/queue/index.js +1 -0
  91. package/dist/wirings/queue/queue-runner.d.ts +3 -4
  92. package/dist/wirings/queue/queue-runner.js +8 -7
  93. package/dist/wirings/queue/queue-trace.d.ts +18 -0
  94. package/dist/wirings/queue/queue-trace.js +42 -0
  95. package/dist/wirings/queue/queue.types.d.ts +7 -0
  96. package/dist/wirings/queue/remote-queue-job.d.ts +1 -1
  97. package/dist/wirings/queue/remote-queue-job.js +5 -3
  98. package/dist/wirings/rpc/rpc-runner.d.ts +7 -1
  99. package/dist/wirings/rpc/rpc-runner.js +43 -21
  100. package/dist/wirings/rpc/rpc-types.d.ts +2 -0
  101. package/dist/wirings/scheduler/remote-scheduled-job.d.ts +1 -1
  102. package/dist/wirings/scheduler/remote-scheduled-job.js +2 -2
  103. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -4
  104. package/dist/wirings/scheduler/scheduler-runner.js +7 -6
  105. package/dist/wirings/trigger/pikku-trigger-service.js +7 -4
  106. package/dist/wirings/trigger/webhook-source-runner.d.ts +7 -1
  107. package/dist/wirings/trigger/webhook-source-runner.js +11 -3
  108. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  109. package/dist/wirings/workflow/graph/graph-runner.js +49 -30
  110. package/dist/wirings/workflow/index.d.ts +3 -1
  111. package/dist/wirings/workflow/index.js +1 -0
  112. package/dist/wirings/workflow/pikku-scenario-service.js +2 -0
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +37 -40
  114. package/dist/wirings/workflow/pikku-workflow-service.js +122 -133
  115. package/dist/wirings/workflow/workflow-child-step.d.ts +2 -2
  116. package/dist/wirings/workflow/workflow-child-step.js +11 -4
  117. package/dist/wirings/workflow/workflow-compensation.d.ts +6 -0
  118. package/dist/wirings/workflow/workflow-compensation.js +15 -1
  119. package/dist/wirings/workflow/workflow-constants.d.ts +13 -0
  120. package/dist/wirings/workflow/workflow-constants.js +13 -0
  121. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +2 -0
  122. package/dist/wirings/workflow/workflow-dsl-pass.js +27 -11
  123. package/dist/wirings/workflow/workflow-recovery.d.ts +131 -3
  124. package/dist/wirings/workflow/workflow-recovery.js +233 -7
  125. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +1 -0
  126. package/dist/wirings/workflow/workflow-run-mirror.d.ts +4 -0
  127. package/dist/wirings/workflow/workflow-run-mirror.js +16 -0
  128. package/dist/wirings/workflow/workflow-run-status.d.ts +2 -0
  129. package/dist/wirings/workflow/workflow-run-status.js +13 -0
  130. package/dist/wirings/workflow/workflow-run-trace.d.ts +22 -0
  131. package/dist/wirings/workflow/workflow-run-trace.js +24 -0
  132. package/dist/wirings/workflow/workflow-step-claim.d.ts +18 -0
  133. package/dist/wirings/workflow/workflow-step-claim.js +47 -0
  134. package/dist/wirings/workflow/workflow-step-retry.d.ts +6 -2
  135. package/dist/wirings/workflow/workflow-step-retry.js +8 -3
  136. package/dist/wirings/workflow/workflow-wire.d.ts +12 -0
  137. package/dist/wirings/workflow/workflow-wire.js +33 -0
  138. package/dist/wirings/workflow/workflow.types.d.ts +16 -0
  139. package/package.json +2 -1
@@ -1,5 +1,6 @@
1
1
  import { getSingletonServices, pikkuState } from '../../pikku-state.js';
2
2
  import { getDurationInMilliseconds } from '../../time-utils.js';
3
+ import { buildWorkflowWire } from './workflow-wire.js';
3
4
  import { ChildWorkflowStartedException, continueGraph, executeGraphStep, runWorkflowGraph, stripInstanceOrdinal, } from './graph/graph-runner.js';
4
5
  import { isExpectedError } from '../../errors/error-handler.js';
5
6
  import { PikkuMissingMetaError } from '../../errors/errors.js';
@@ -7,8 +8,8 @@ import { RPCNotFoundError } from '../rpc/rpc-runner.js';
7
8
  import { deriveInvocationId } from './workflow-invocation-id.js';
8
9
  import { approvalDeciderFrom } from './workflow-approval-policy.js';
9
10
  import { buildRunTimeline, reconstructStateAt, } from './run-timeline.js';
10
- import { DEFAULT_STEP_RETRIES, RUN_LEASE_RETRY_MS, WORKFLOW_CHILD_POLL_MAX_MS, WORKFLOW_END_STATES, WORKFLOW_POLL_FACTOR, WORKFLOW_POLL_MIN_MS, WORKFLOW_TERMINAL_STATES, NO_COMPENSATE_STATE_PREFIX, forwardStepName, isCompensationStepName, milestoneStepName, isRunSettled, } from './workflow-constants.js';
11
- import { WorkflowAsyncException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotFoundError, WorkflowStepNameNotString, WorkflowStepSupersededError, } from './workflow-errors.js';
11
+ import { DEFAULT_STEP_RETRIES, RUN_LEASE_RETRY_MS, WORKFLOW_CHILD_POLL_MAX_MS, WORKFLOW_END_STATES, WORKFLOW_POLL_FACTOR, WORKFLOW_POLL_MIN_MS, WORKFLOW_TERMINAL_STATES, NO_COMPENSATE_STATE_PREFIX, forwardStepName, isCompensationStepName, isRunSettled, isRunStopped, } from './workflow-constants.js';
12
+ import { WorkflowAsyncException, WorkflowCancelledException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowStepNameNotString, WorkflowStepSupersededError, } from './workflow-errors.js';
12
13
  import { resolveWorkflowMeta } from './workflow-meta-resolver.js';
13
14
  import { storeWorkflowVersion } from './workflow-version-store.js';
14
15
  import { jobGroupFor, orchestratorQueueName, requireQueueService, resolveWorkflowConfig, stepDispatchTarget, stepJobOptions, stepWorkerQueueName, } from './workflow-queue-routing.js';
@@ -16,16 +17,18 @@ import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js';
16
17
  import { approvalStepNameFor, evaluateApprovalStep, recordApprovalDecision, } from './workflow-approval.js';
17
18
  import { auditApprovalDecision } from './workflow-approval-audit.js';
18
19
  import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js';
19
- import { claimStepByReadThenWrite } from './workflow-step-claim.js';
20
+ import { claimStepByReadThenWrite, takeBackLostInlineStep, warnStepAttemptsExhausted, warnStepRerun, } from './workflow-step-claim.js';
20
21
  import { holdWorkflowLease, isRunLeaseError, nullWhenStepHeld, } from './workflow-run-lease.js';
21
- import { summarizeRunStatus } from './workflow-run-status.js';
22
+ import { runOutput, summarizeRunStatus } from './workflow-run-status.js';
23
+ import { writeRunMirror } from './workflow-run-mirror.js';
22
24
  import { runningStepLease, startStepLeaseRefresh, stepLeaseMsForQueue, } from './workflow-step-lease.js';
23
25
  import { runInlineRetryLoop } from './workflow-step-retry.js';
24
26
  import { runInlineChildStep, startQueuedChildStep, } from './workflow-child-step.js';
25
27
  import { runDslWorkflowPass } from './workflow-dsl-pass.js';
26
- import { childRunFailure, failRunOrUnwind, isUnwound, compensatingForStep, cancelAndUnwind, createUnwindHost, driveUnwind, recordHasCompensation, isChildWorkflowRecord, } from './workflow-compensation.js';
28
+ import { runLogger, runTraceWire, withRunTrace } from './workflow-run-trace.js';
29
+ import { childRunFailure, settleStoppedRun, failRunOrUnwind, isUnwound, compensatingForStep, cancelAndUnwind, createUnwindHost, driveUnwind, recordHasCompensation, isChildWorkflowRecord, } from './workflow-compensation.js';
27
30
  import { runVersionMismatchFallback } from './workflow-version-fallback.js';
28
- import { RedispatchBackoff, sweepStalledRuns, sweepUndispatchedSteps, } from './workflow-recovery.js';
31
+ import { WorkflowRecovery, } from './workflow-recovery.js';
29
32
  /** The workflow engine; subclasses supply storage. Leases serialise runs and step claims over a shared store. */
30
33
  export class PikkuWorkflowService {
31
34
  runExtension;
@@ -68,6 +71,10 @@ export class PikkuWorkflowService {
68
71
  get logger() {
69
72
  return getSingletonServices()?.logger;
70
73
  }
74
+ lostStepLog = {
75
+ logger: () => this.logger,
76
+ traceparent: (runId) => this.getRunTraceparent(runId),
77
+ };
71
78
  mirror;
72
79
  /**
73
80
  * Serialises orchestration passes over a run and claims on a step. A service
@@ -91,19 +98,7 @@ export class PikkuWorkflowService {
91
98
  }
92
99
  async mirrored(write, mirror) {
93
100
  const written = await write();
94
- if (this.mirror) {
95
- try {
96
- await mirror(this.mirror, written);
97
- }
98
- catch (err) {
99
- try {
100
- this.logger?.warn?.(`[pikku] WorkflowRunMirror write failed: ${err?.message ?? err}`);
101
- }
102
- catch {
103
- // knowledge: decisions/internals/workflow-run-mirror-is-never-a-source-of-truth.md
104
- }
105
- }
106
- }
101
+ await writeRunMirror(this.mirror, () => this.logger, (target) => mirror(target, written));
107
102
  return written;
108
103
  }
109
104
  wireQueueWorkers() {
@@ -144,6 +139,7 @@ export class PikkuWorkflowService {
144
139
  }
145
140
  }
146
141
  async createRun(workflowName, input, inline, graphHash, wire, options) {
142
+ wire = withRunTrace(wire);
147
143
  await storeWorkflowVersion(this, workflowName, graphHash);
148
144
  return this.mirrored(() => this.createRunImpl(workflowName, input, inline, graphHash, wire, options), (mirror, runId) => mirror.createRun(runId, workflowName, input, inline, graphHash, wire, options));
149
145
  }
@@ -252,77 +248,89 @@ export class PikkuWorkflowService {
252
248
  await this.enqueueOrchestration(runId, workflowName);
253
249
  }
254
250
  async enqueueOrchestration(runId, workflowName, delay) {
255
- const queueService = requireQueueService();
256
- workflowName ??= (await this.getRun(runId))?.workflow;
257
- await queueService.add(this.getOrchestratorQueueName(workflowName), { runId }, {
251
+ await this.addOrchestratorJob(runId, workflowName, {
258
252
  ...this.resolveStepJobOptions(),
259
253
  ...(delay !== undefined ? { delay } : {}),
254
+ });
255
+ }
256
+ async addOrchestratorJob(runId, workflowName, options) {
257
+ const queueService = requireQueueService();
258
+ const run = await this.getRunIdentity(runId);
259
+ workflowName ??= run?.workflow;
260
+ await queueService.add(this.getOrchestratorQueueName(workflowName), { runId }, {
261
+ ...options,
260
262
  group: this.getJobGroup(workflowName),
263
+ traceparent: run?.wire?.traceparent,
261
264
  });
262
265
  }
263
266
  /**
264
- * Ids of runs that are stalled: still `running`, with no step in a state that
265
- * something is expected to complete (`scheduled`, `suspended`, or `running`
266
- * under a live lease), and no step activity since `before`.
267
+ * Ids of runs that are stalled: still `running`, with no step something is
268
+ * expected to complete (`scheduled`, `suspended`, `running` under a live
269
+ * lease, or parked on a child run), and no step activity since `before`.
267
270
  *
268
- * A `running` step whose lease has lapsed is the opposite of in flight — the
269
- * dispatch that claimed it is gone — so it does not hold its run out of the
270
- * sweep.
271
+ * A lapsed lease is a dispatch that died, so it holds nothing out of the
272
+ * sweep. Nor does a `running` step with no lease and no child run: that is
273
+ * work inside an orchestration pass, whose owner is the run's lease, which the
274
+ * sweep checks before it touches the run.
271
275
  *
272
- * Returns nothing by default so a store that cannot express the query keeps
273
- * working unchanged; a store that overrides it gains crash recovery through
274
- * `recoverStalledRuns`.
276
+ * Returns nothing by default, for a store that cannot express the query.
275
277
  */
276
278
  async findStalledRunIds(_before, _limit) {
277
279
  return [];
278
280
  }
279
281
  /**
280
- * Runs holding a step that has sat `pending` since before `before`, paired
281
- * with the step that flagged them.
282
- *
283
- * Returns nothing by default so a store that cannot express the query keeps
284
- * working unchanged — and, because it does not opt in, gains no re-dispatches
285
- * either. A store must have an atomic `claimStepForExecution` before
286
- * overriding this, or no concurrency for one to exclude: the relay makes
287
- * duplicate dispatch routine, and the claim is what keeps a duplicate from
288
- * becoming a second execution. Every `@pikku/kysely` dialect qualifies on its
289
- * status-guarded claim, `mongodb` on the same claim expressed as a
290
- * single-document update, `in-memory` on being inline and single-process —
291
- * none of them overrides this yet.
282
+ * Runs holding a step `pending` since before `before`. A store overriding it
283
+ * needs an atomic `claimStepForExecution` (see `sweepUndispatchedSteps`).
292
284
  */
293
285
  async findUndispatchedSteps(_before, _limit) {
294
286
  return [];
295
287
  }
296
- redispatchBackoff = new RedispatchBackoff();
297
- get sweepDeps() {
298
- return {
299
- resume: (runId) => this.resumeWorkflow(runId),
300
- logger: getSingletonServices()?.logger,
301
- };
302
- }
303
288
  /**
304
- * Re-drive runs whose next move was lost. Not self-starting — call it from a
305
- * scheduled task at whatever interval suits the workload.
289
+ * Steps parked on `scheduled` on a live run, oldest first, a page at a time.
290
+ * Returns nothing by default, for a store whose runs die with the process.
306
291
  */
307
- async recoverStalledRuns(options) {
308
- return sweepStalledRuns((before, limit) => this.findStalledRunIds(before, limit), this.redispatchBackoff, options, this.sweepDeps);
292
+ async findScheduledSteps(_limit, _offset) {
293
+ return [];
309
294
  }
310
- /**
311
- * Re-drive steps whose dispatch was lost. Not self-starting — call it from a
312
- * scheduled task; ~30s suits a queue whose `pending`→`running` latency is
313
- * well under that.
314
- */
315
- async relayUndispatchedSteps(options) {
316
- return sweepUndispatchedSteps((before, limit) => this.findUndispatchedSteps(before, limit), this.redispatchBackoff, options, this.sweepDeps);
295
+ recovery = new WorkflowRecovery({
296
+ leases: () => this.leaseService,
297
+ listStepStates: (runId) => this.listStepStates(runId),
298
+ withRunLease: (runId, fn) => this.withRunLease(runId, fn),
299
+ findStalledRunIds: (before, limit) => this.findStalledRunIds(before, limit),
300
+ findUndispatchedSteps: this.findUndispatchedSteps.bind(this),
301
+ findScheduledSteps: this.findScheduledSteps.bind(this),
302
+ resumeWorkflow: (runId) => this.resumeWorkflow(runId),
303
+ wakeSleep: this.executeWorkflowSleepCompleted.bind(this),
304
+ rearmSleep: this.scheduleSleep.bind(this),
305
+ queueStepWorker: (...args) => this.queueStepWorker(...args),
306
+ });
307
+ /** One recovery pass, for a host driving it from a scheduled task. */
308
+ recoverWorkflows(options) {
309
+ return this.recovery.recover(options);
310
+ }
311
+ /** Sweep now and on an interval, once the queue is consuming. */
312
+ startRecovery(options) {
313
+ this.recovery.start(options);
314
+ }
315
+ stopRecovery() {
316
+ this.recovery.stop();
317
+ }
318
+ /** Re-drive runs whose next move was lost. */
319
+ recoverStalledRuns(options) {
320
+ return this.recovery.recoverStalledRuns(options);
321
+ }
322
+ /** Re-drive steps whose dispatch was lost. Not self-starting. */
323
+ relayUndispatchedSteps(options) {
324
+ return this.recovery.relayUndispatchedSteps(options);
317
325
  }
318
326
  resolveStepJobOptions(stepOptions) {
319
327
  return stepJobOptions(stepOptions);
320
328
  }
321
329
  async queueStepWorker(runId, stepName, rpcName, data, stepOptions, fromStepName) {
322
- const queueService = requireQueueService();
323
- await queueService.add(this.getStepWorkerQueueName(rpcName), { runId, stepName, rpcName, data, fromStepName }, {
330
+ await requireQueueService().add(this.getStepWorkerQueueName(rpcName), { runId, stepName, rpcName, data, fromStepName }, {
324
331
  ...this.resolveStepJobOptions(stepOptions),
325
332
  group: this.getJobGroup(rpcName),
333
+ traceparent: await this.getRunTraceparent(runId),
326
334
  });
327
335
  }
328
336
  async executeWorkflowSleepCompleted(runId, stepId) {
@@ -330,17 +338,7 @@ export class PikkuWorkflowService {
330
338
  await this.resumeWorkflow(runId);
331
339
  }
332
340
  async scheduleOrchestratorRetry(runId, retryDelay, workflowName) {
333
- const queueService = requireQueueService();
334
- if (!workflowName) {
335
- const run = await this.getRun(runId);
336
- workflowName = run?.workflow;
337
- }
338
- await queueService.add(this.getOrchestratorQueueName(workflowName), { runId }, {
339
- ...(retryDelay
340
- ? { delay: getDurationInMilliseconds(retryDelay) }
341
- : undefined),
342
- group: this.getJobGroup(workflowName),
343
- });
341
+ await this.addOrchestratorJob(runId, workflowName, retryDelay ? { delay: getDurationInMilliseconds(retryDelay) } : {});
344
342
  }
345
343
  async dispatchStep(runId, stepName, rpcName, data, stepOptions, fromStepName) {
346
344
  const target = await stepDispatchTarget(rpcName, stepName, () => this.isInline(runId));
@@ -348,10 +346,7 @@ export class PikkuWorkflowService {
348
346
  return false;
349
347
  }
350
348
  try {
351
- await getSingletonServices().queueService.add(this.getStepWorkerQueueName(rpcName), { runId, stepName, rpcName, data, fromStepName }, {
352
- ...this.resolveStepJobOptions(stepOptions),
353
- group: this.getJobGroup(rpcName),
354
- });
349
+ await this.queueStepWorker(runId, stepName, rpcName, data, stepOptions, fromStepName);
355
350
  }
356
351
  catch (cause) {
357
352
  throw new WorkflowDispatchException(runId, stepName, { cause });
@@ -363,7 +358,7 @@ export class PikkuWorkflowService {
363
358
  !getSingletonServices()?.schedulerService) {
364
359
  return false;
365
360
  }
366
- await getSingletonServices().schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, { runId, stepId });
361
+ await getSingletonServices().schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, { runId, stepId }, undefined, await this.getRunTraceparent(runId));
367
362
  return true;
368
363
  }
369
364
  setRunExtension(create) {
@@ -372,6 +367,7 @@ export class PikkuWorkflowService {
372
367
  updateRunStatus: this.updateRunStatus.bind(this),
373
368
  onChildWorkflowFailed: this.onChildWorkflowFailed.bind(this),
374
369
  verifyStepName: this.verifyStepName.bind(this),
370
+ getRunTraceparent: this.getRunTraceparent.bind(this),
375
371
  };
376
372
  const extension = create(engine);
377
373
  this.runExtension = extension;
@@ -381,6 +377,10 @@ export class PikkuWorkflowService {
381
377
  return this.runExtension;
382
378
  }
383
379
  async startWorkflow(name, input, wire, rpcService, options) {
380
+ wire = {
381
+ ...wire,
382
+ traceparent: wire?.traceparent ?? rpcService?.traceparent,
383
+ };
384
384
  const resolved = resolveWorkflowMeta(name);
385
385
  const workflowMeta = resolved?.meta;
386
386
  const packageName = resolved?.packageName ?? null;
@@ -401,7 +401,7 @@ export class PikkuWorkflowService {
401
401
  throw new Error(`Workflow graph '${name}': '${options.startNode}' is not a declared entry node`);
402
402
  }
403
403
  const shouldInline = options?.inline || !getSingletonServices()?.queueService;
404
- return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire, workflowMeta);
404
+ return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire, workflowMeta, options?.onRunCreated);
405
405
  }
406
406
  if (!workflowMeta.graphHash) {
407
407
  throw new Error(`Missing workflow graphHash for '${name}'`);
@@ -434,7 +434,7 @@ export class PikkuWorkflowService {
434
434
  message: error.message,
435
435
  stack: error.stack,
436
436
  });
437
- getSingletonServices().logger.error(`Workflow ${name} (run ${runId}) failed:`, isExpectedError(error) ? error.message : error);
437
+ runLogger(getSingletonServices().logger, wire).error(`Workflow ${name} (run ${runId}) failed:`, isExpectedError(error) ? error.message : error);
438
438
  const settled = await this.getRun(runId);
439
439
  if (settled?.wire?.type === 'workflow' && isUnwound(settled.status)) {
440
440
  throw childRunFailure(settled) ?? error;
@@ -461,16 +461,7 @@ export class PikkuWorkflowService {
461
461
  }
462
462
  async runToCompletion(name, input, rpcService, options) {
463
463
  const { runId } = await this.startWorkflow(name, input, options?.wire ?? { type: 'internal' }, rpcService, { inline: true });
464
- const run = await this.awaitRunEnd(runId, options?.pollIntervalMs ?? 1000);
465
- if (run.status === 'failed' ||
466
- run.status === 'compensated' ||
467
- run.status === 'compensation_failed') {
468
- throw new WorkflowRunFailedError(run.error?.message);
469
- }
470
- if (run.status === 'cancelled') {
471
- throw new WorkflowRunCancelledError();
472
- }
473
- return run.output;
464
+ return runOutput(await this.awaitRunEnd(runId, options?.pollIntervalMs ?? 1000));
474
465
  }
475
466
  async awaitRunEnd(runId, maxIntervalMs) {
476
467
  let interval = Math.min(WORKFLOW_POLL_MIN_MS, maxIntervalMs);
@@ -547,6 +538,18 @@ export class PikkuWorkflowService {
547
538
  }
548
539
  return run;
549
540
  }
541
+ /** The trace every hop of the run stays on. */
542
+ async getRunTraceparent(runId) {
543
+ return (await this.getRunIdentity(runId))?.wire?.traceparent;
544
+ }
545
+ /** Warn that a step lost its worker and is running again, as `attempt`. */
546
+ logStepRerun(runId, stepName, attempt) {
547
+ return warnStepRerun(this.lostStepLog, runId, stepName, attempt);
548
+ }
549
+ /** Warn that a step lost its worker with no attempts left, and failed. */
550
+ async logStepAttemptsExhausted(runId, stepName, attempts) {
551
+ await warnStepAttemptsExhausted(this.lostStepLog, runId, stepName, attempts);
552
+ }
550
553
  lastStepName(runId) {
551
554
  return this.runContexts.get(runId)?.lastStep;
552
555
  }
@@ -616,6 +619,7 @@ export class PikkuWorkflowService {
616
619
  workflowWire: (addonNs) => this.createWorkflowWire(run.workflow, runId, rpcService, addonNs),
617
620
  extension: this.runExtension,
618
621
  updateRunStatus: (status, output, error) => this.updateRunStatus(runId, status, output, error),
622
+ settleIfStopped: () => this.settleStoppedRun(runId, rpcService),
619
623
  onChildCompleted: (result) => this.onChildWorkflowCompleted(run, result),
620
624
  onChildFailed: (error) => this.onChildWorkflowFailed(run, error),
621
625
  failOrUnwind: (error) => this.failOrUnwind(run, error, rpcService),
@@ -713,7 +717,7 @@ export class PikkuWorkflowService {
713
717
  startWorkflow: (...args) => this.startWorkflow(...args),
714
718
  setStepChildRunId: (...args) => this.setStepChildRunId(...args),
715
719
  getRun: (id) => this.getRun(id),
716
- }, runId, stepState.stepId, rpcName, data, rpcService, run.wire?.pikkuUserId);
720
+ }, runId, stepState.stepId, rpcName, data, rpcService, run.wire);
717
721
  }
718
722
  else {
719
723
  result = await this.invokeStepRpc(runId, stepName, stepState, rpcName, data, rpcService, run, isCompensationStepName(stepName)
@@ -792,6 +796,7 @@ export class PikkuWorkflowService {
792
796
  async invokeStepRpc(runId, stepName, stepState, rpcName, data, rpcService, knownRun, compensatingFor) {
793
797
  const run = knownRun ?? (await this.getRunIdentity(runId));
794
798
  return rpcService.rpcWithWire(rpcName, data, {
799
+ ...runTraceWire(run?.wire?.traceparent),
795
800
  ...(run?.wire?.pikkuUserId ? { pikkuUserId: run.wire.pikkuUserId } : {}),
796
801
  ...(compensatingFor
797
802
  ? {
@@ -855,7 +860,7 @@ export class PikkuWorkflowService {
855
860
  }
856
861
  const retries = resolvedStepOptions.retries ?? this.getConfig().retries;
857
862
  const retryDelay = resolvedStepOptions.retryDelay;
858
- return runInlineRetryLoop(this, stepState, retries, retryDelay, async (currentStepState) => {
863
+ return runInlineRetryLoop(this, await takeBackLostInlineStep(this, runId, stepName, stepState), retries, retryDelay, async (currentStepState) => {
859
864
  if (resolvedStepOptions.virtualUser) {
860
865
  return resolvedStepOptions.virtualUser.invoke(rpcName, data);
861
866
  }
@@ -864,7 +869,7 @@ export class PikkuWorkflowService {
864
869
  startWorkflow: (...args) => this.startWorkflow(...args),
865
870
  setStepChildRunId: (...args) => this.setStepChildRunId(...args),
866
871
  awaitRunEnd: (id) => this.awaitRunEnd(id, WORKFLOW_CHILD_POLL_MAX_MS),
867
- }, runId, currentStepState.stepId, rpcName, data, rpcService, (await this.getRunIdentity(runId))?.wire?.pikkuUserId);
872
+ }, runId, currentStepState.stepId, rpcName, data, rpcService, (await this.getRunIdentity(runId))?.wire);
868
873
  }
869
874
  return this.invokeStepRpc(runId, stepName, currentStepState, rpcName, data, rpcService);
870
875
  }, async (error) => {
@@ -875,7 +880,7 @@ export class PikkuWorkflowService {
875
880
  });
876
881
  throw error;
877
882
  }
878
- });
883
+ }, () => this.throwIfRunStopped(runId));
879
884
  }
880
885
  async inlineStep(runId, logicalStepName, fn, stepOptions, data = null, rpcName = null) {
881
886
  const fromStepName = this.lastStepName(runId);
@@ -887,7 +892,7 @@ export class PikkuWorkflowService {
887
892
  const retries = stepOptions?.retries ?? this.getConfig().retries;
888
893
  const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay;
889
894
  if (await this.isInline(runId)) {
890
- return runInlineRetryLoop(this, stepState, retries, retryDelay, () => fn());
895
+ return runInlineRetryLoop(this, stepState, retries, retryDelay, () => fn(), undefined, () => this.throwIfRunStopped(runId));
891
896
  }
892
897
  else {
893
898
  let currentStepState = stepState;
@@ -899,7 +904,7 @@ export class PikkuWorkflowService {
899
904
  }
900
905
  catch (error) {
901
906
  await this.setStepError(currentStepState.stepId, error);
902
- if (currentStepState.attemptCount < retries) {
907
+ if (currentStepState.attemptCount <= retries) {
903
908
  await this.createRetryAttempt(currentStepState.stepId, 'pending');
904
909
  await this.scheduleOrchestratorRetry(runId, retryDelay);
905
910
  throw new WorkflowAsyncException(runId, stepName);
@@ -971,41 +976,15 @@ export class PikkuWorkflowService {
971
976
  return await this.withStepLock(runId, approvalStepName, () => evaluateApprovalStep(this.approvalStore, runId, reason, approvalStepName, fromStepName, options));
972
977
  }
973
978
  createWorkflowWire(name, runId, rpcService, addonNamespace) {
974
- const workflowWire = {
975
- name,
976
- runId,
977
- getRun: async () => (await this.getRun(runId)),
978
- do: async (stepName, rpcNameOrFn, dataOrOptions, options) => {
979
- this.verifyStepName(stepName);
980
- if (typeof rpcNameOrFn === 'string') {
981
- const resolvedRpcName = addonNamespace && !rpcNameOrFn.includes(':')
982
- ? `${addonNamespace}:${rpcNameOrFn}`
983
- : rpcNameOrFn;
984
- return await this.rpcStep(runId, stepName, resolvedRpcName, dataOrOptions, rpcService, options);
985
- }
986
- else {
987
- return await this.inlineStep(runId, stepName, rpcNameOrFn, dataOrOptions);
988
- }
989
- },
990
- sleep: async (stepName, duration) => {
991
- this.verifyStepName(stepName);
992
- await this.sleepStep(runId, stepName, getDurationInMilliseconds(duration));
993
- },
994
- suspend: async (reason) => {
995
- this.verifyStepName(reason);
996
- await this.suspendStep(runId, reason);
997
- },
998
- milestone: async (name) => {
999
- this.verifyStepName(name);
1000
- await this.inlineStep(runId, milestoneStepName(name), async () => ({
1001
- name,
1002
- }));
1003
- },
1004
- approval: (async (reason, options) => {
1005
- this.verifyStepName(reason);
1006
- return await this.approvalStep(runId, reason, options);
1007
- }),
1008
- };
979
+ const workflowWire = buildWorkflowWire(name, runId, addonNamespace, {
980
+ getRun: () => this.getRun(runId),
981
+ verifyStepName: (stepName) => this.verifyStepName(stepName),
982
+ rpcStep: (stepName, rpcName, data, options) => this.rpcStep(runId, stepName, rpcName, data, rpcService, options),
983
+ inlineStep: (stepName, fn, options) => this.inlineStep(runId, stepName, fn, options),
984
+ sleepStep: (stepName, ms) => this.sleepStep(runId, stepName, ms),
985
+ suspendStep: (reason) => this.suspendStep(runId, reason),
986
+ approvalStep: (reason, options) => this.approvalStep(runId, reason, options),
987
+ });
1009
988
  this.runExtension?.decorateWorkflowWire(workflowWire, {
1010
989
  name,
1011
990
  runId,
@@ -1038,13 +1017,23 @@ export class PikkuWorkflowService {
1038
1017
  const api = this.compensationApi(rpcService);
1039
1018
  return failRunOrUnwind(api, run, error, () => this.onChildWorkflowFailed(run, error));
1040
1019
  }
1020
+ /** Ends a run's body at its next inline attempt once the run has been stopped. */
1021
+ async throwIfRunStopped(runId) {
1022
+ if (isRunStopped((await this.getRun(runId))?.status ?? '')) {
1023
+ throw new WorkflowCancelledException(runId);
1024
+ }
1025
+ }
1026
+ settleStoppedRun(runId, rpcService) {
1027
+ return settleStoppedRun(this.compensationApi(rpcService), runId);
1028
+ }
1041
1029
  /** Stops an unfinished run, children first, and unwinds what it completed. */
1042
1030
  async cancelRun(runId, rpcService, reason) {
1043
1031
  const api = this.compensationApi(rpcService);
1044
1032
  return cancelAndUnwind(api, createUnwindHost(api), runId, reason);
1045
1033
  }
1046
1034
  async markFailedUnlessUnwound(runId, error) {
1047
- if (isUnwound((await this.getRun(runId))?.status))
1035
+ const status = (await this.getRun(runId))?.status;
1036
+ if (status === 'cancelled' || isUnwound(status))
1048
1037
  return;
1049
1038
  await this.updateRunStatus(runId, 'failed', undefined, error);
1050
1039
  }
@@ -10,8 +10,8 @@ interface ChildStepApi {
10
10
  }
11
11
  export declare const runInlineChildStep: (api: ChildStepApi & {
12
12
  awaitRunEnd(childRunId: string): Promise<WorkflowRun>;
13
- }, runId: string, stepId: string, workflowName: string, data: unknown, rpcService: PikkuRPC, pikkuUserId: string | undefined) => Promise<unknown>;
13
+ }, runId: string, stepId: string, workflowName: string, data: unknown, rpcService: PikkuRPC, parentWire: WorkflowRunWire | undefined) => Promise<unknown>;
14
14
  export declare const startQueuedChildStep: (api: ChildStepApi & {
15
15
  getRun(id: string): Promise<WorkflowRun | null>;
16
- }, runId: string, stepId: string, workflowName: string, data: unknown, rpcService: PikkuRPC, pikkuUserId: string | undefined) => Promise<unknown>;
16
+ }, runId: string, stepId: string, workflowName: string, data: unknown, rpcService: PikkuRPC, parentWire: WorkflowRunWire | undefined) => Promise<unknown>;
17
17
  export {};
@@ -1,8 +1,14 @@
1
1
  import { getSingletonServices } from '../../pikku-state.js';
2
2
  import { ChildWorkflowStartedException } from './graph/graph-runner.js';
3
3
  import { childRunFailure } from './workflow-compensation.js';
4
- export const runInlineChildStep = async (api, runId, stepId, workflowName, data, rpcService, pikkuUserId) => {
5
- const { runId: childRunId } = await api.startWorkflow(workflowName, data, { type: 'workflow', id: workflowName, parentRunId: runId, pikkuUserId }, rpcService, { inline: true });
4
+ export const runInlineChildStep = async (api, runId, stepId, workflowName, data, rpcService, parentWire) => {
5
+ const { runId: childRunId } = await api.startWorkflow(workflowName, data, {
6
+ type: 'workflow',
7
+ id: workflowName,
8
+ parentRunId: runId,
9
+ pikkuUserId: parentWire?.pikkuUserId,
10
+ traceparent: parentWire?.traceparent,
11
+ }, rpcService, { inline: true });
6
12
  await api.setStepChildRunId(stepId, childRunId);
7
13
  const childRun = await api.awaitRunEnd(childRunId);
8
14
  const failure = childRunFailure(childRun);
@@ -10,14 +16,15 @@ export const runInlineChildStep = async (api, runId, stepId, workflowName, data,
10
16
  throw failure;
11
17
  return childRun.output;
12
18
  };
13
- export const startQueuedChildStep = async (api, runId, stepId, workflowName, data, rpcService, pikkuUserId) => {
19
+ export const startQueuedChildStep = async (api, runId, stepId, workflowName, data, rpcService, parentWire) => {
14
20
  const shouldInline = !getSingletonServices()?.queueService;
15
21
  const { runId: childRunId } = await api.startWorkflow(workflowName, data, {
16
22
  type: 'workflow',
17
23
  id: workflowName,
18
24
  parentRunId: runId,
19
25
  parentStepId: stepId,
20
- pikkuUserId,
26
+ pikkuUserId: parentWire?.pikkuUserId,
27
+ traceparent: parentWire?.traceparent,
21
28
  }, rpcService, { inline: shouldInline });
22
29
  await api.setStepChildRunId(stepId, childRunId);
23
30
  if (!shouldInline) {
@@ -57,6 +57,12 @@ export type UnwindCause = SerializedError & {
57
57
  export declare const childRunFailure: (childRun: WorkflowRun) => Error | null;
58
58
  export declare const beginUnwind: (api: CompensationApi, host: UnwindHost, run: WorkflowRun, cause: UnwindCause) => Promise<'unwinding' | 'finished' | 'not-needed'>;
59
59
  export declare const isUnwound: (status: string | undefined) => boolean;
60
+ /**
61
+ * The status of a run that can no longer take an outcome from a pass of its
62
+ * own. An unwind that stopped to wait on the steps in flight is finished here:
63
+ * those steps were this pass's, and nothing else will wake an inline run.
64
+ */
65
+ export declare const settleStoppedRun: (api: CompensationApi, runId: string) => Promise<WorkflowStatus | undefined>;
60
66
  export declare const failRunOrUnwind: (api: CompensationApi, run: WorkflowRun, error: Error & {
61
67
  code?: string;
62
68
  childCompensation?: ChildCompensationInfo;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, WORKFLOW_TERMINAL_STATES, compensationStepName, } from './workflow-constants.js';
1
+ import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, WORKFLOW_TERMINAL_STATES, isRunStopped, compensationStepName, } from './workflow-constants.js';
2
2
  import { pikkuState } from '../../pikku-state.js';
3
3
  import { resolveNamespace } from '../rpc/rpc-runner.js';
4
4
  import { runningStepLease } from './workflow-step-lease.js';
@@ -239,6 +239,20 @@ export const beginUnwind = async (api, host, run, cause) => {
239
239
  export const isUnwound = (status) => status === 'compensating' ||
240
240
  status === 'compensated' ||
241
241
  status === 'compensation_failed';
242
+ /**
243
+ * The status of a run that can no longer take an outcome from a pass of its
244
+ * own. An unwind that stopped to wait on the steps in flight is finished here:
245
+ * those steps were this pass's, and nothing else will wake an inline run.
246
+ */
247
+ export const settleStoppedRun = async (api, runId) => {
248
+ const status = (await api.getRun(runId))?.status;
249
+ if (!status || !isRunStopped(status))
250
+ return undefined;
251
+ if (status === 'compensating') {
252
+ await driveUnwind(createUnwindHost(api), runId);
253
+ }
254
+ return status;
255
+ };
242
256
  export const failRunOrUnwind = async (api, run, error, onFailed) => {
243
257
  const cause = {
244
258
  message: error.message,
@@ -30,6 +30,11 @@ export declare const isMilestoneStepName: (stepName: string) => boolean;
30
30
  * not the run, which resumes when its approval or signal arrives.
31
31
  */
32
32
  export declare const isRunSettled: (status: string) => boolean;
33
+ /**
34
+ * True for a run its own pass must no longer write an outcome for. That is a
35
+ * settled run, or one unwinding, which `cancelRun` moves there on its own.
36
+ */
37
+ export declare const isRunStopped: (status: string) => boolean;
33
38
  export declare const WORKFLOW_POLL_MIN_MS = 10;
34
39
  export declare const WORKFLOW_POLL_FACTOR = 1.6;
35
40
  export declare const WORKFLOW_CHILD_POLL_MAX_MS = 500;
@@ -45,6 +50,14 @@ export declare const DEFAULT_STALLED_RUN_LIMIT = 100;
45
50
  export declare const DEFAULT_UNDISPATCHED_STEP_MS = 30000;
46
51
  /** Steps re-driven per `relayUndispatchedSteps` call, so one tick is bounded. */
47
52
  export declare const DEFAULT_UNDISPATCHED_STEP_LIMIT = 100;
53
+ /** How far past its wake time a sleep sits before a sweep decides its timer is lost. */
54
+ export declare const SLEEP_WAKE_GRACE_MS = 5000;
55
+ /** `scheduled` steps read per query, so a sweep pages rather than loads them all. */
56
+ export declare const SCHEDULED_STEP_PAGE_SIZE = 200;
57
+ /** How often a long-running host sweeps for lost work, after its startup pass. */
58
+ export declare const DEFAULT_RECOVERY_INTERVAL_MS = 30000;
59
+ /** Idle window `startRecovery` treats as stalled; the run's lease is what makes it safe. */
60
+ export declare const DEFAULT_RECOVERY_STALLED_MS = 30000;
48
61
  /** First wait before a step already re-dispatched once is re-dispatched again. */
49
62
  export declare const REDISPATCH_BACKOFF_MS = 30000;
50
63
  /** Ceiling on the doubling backoff, so a permanently stuck step still gets swept. */
@@ -46,6 +46,11 @@ export const isMilestoneStepName = (stepName) => stepName.startsWith(MILESTONE_S
46
46
  * not the run, which resumes when its approval or signal arrives.
47
47
  */
48
48
  export const isRunSettled = (status) => WORKFLOW_TERMINAL_STATES.has(status);
49
+ /**
50
+ * True for a run its own pass must no longer write an outcome for. That is a
51
+ * settled run, or one unwinding, which `cancelRun` moves there on its own.
52
+ */
53
+ export const isRunStopped = (status) => isRunSettled(status) || status === 'compensating';
49
54
  export const WORKFLOW_POLL_MIN_MS = 10;
50
55
  export const WORKFLOW_POLL_FACTOR = 1.6;
51
56
  export const WORKFLOW_CHILD_POLL_MAX_MS = 500;
@@ -61,6 +66,14 @@ export const DEFAULT_STALLED_RUN_LIMIT = 100;
61
66
  export const DEFAULT_UNDISPATCHED_STEP_MS = 30_000;
62
67
  /** Steps re-driven per `relayUndispatchedSteps` call, so one tick is bounded. */
63
68
  export const DEFAULT_UNDISPATCHED_STEP_LIMIT = 100;
69
+ /** How far past its wake time a sleep sits before a sweep decides its timer is lost. */
70
+ export const SLEEP_WAKE_GRACE_MS = 5_000;
71
+ /** `scheduled` steps read per query, so a sweep pages rather than loads them all. */
72
+ export const SCHEDULED_STEP_PAGE_SIZE = 200;
73
+ /** How often a long-running host sweeps for lost work, after its startup pass. */
74
+ export const DEFAULT_RECOVERY_INTERVAL_MS = 30_000;
75
+ /** Idle window `startRecovery` treats as stalled; the run's lease is what makes it safe. */
76
+ export const DEFAULT_RECOVERY_STALLED_MS = 30_000;
64
77
  /** First wait before a step already re-dispatched once is re-dispatched again. */
65
78
  export const REDISPATCH_BACKOFF_MS = 30_000;
66
79
  /** Ceiling on the doubling backoff, so a permanently stuck step still gets swept. */
@@ -5,6 +5,8 @@ export interface DslPassDeps {
5
5
  workflowWire: (addonNamespace: string | null) => PikkuWorkflowWire;
6
6
  extension: WorkflowRunExtension | undefined;
7
7
  updateRunStatus: (status: WorkflowStatus, output?: unknown, error?: SerializedError) => Promise<void>;
8
+ /** The run's status if something has ended it, finishing any unwind that waited on this pass. */
9
+ settleIfStopped: () => Promise<WorkflowStatus | undefined>;
8
10
  onChildCompleted: (result: unknown) => Promise<void>;
9
11
  onChildFailed: (error: Error) => Promise<void>;
10
12
  failOrUnwind: (error: Error) => Promise<'unwinding' | 'finished' | 'failed'>;