@pikku/core 0.12.148 → 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 +16 -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 +12 -2
  56. package/dist/wirings/agent/agent-prepare.js +38 -14
  57. package/dist/wirings/agent/agent-rpc.js +2 -1
  58. package/dist/wirings/agent/agent-runner.js +5 -6
  59. package/dist/wirings/agent/agent-stream.js +6 -6
  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
@@ -6,9 +6,9 @@ import { ScheduledTaskNotFoundError, runScheduledTask, } from './scheduler-runne
6
6
  *
7
7
  * @throws {BadRequestError} When the named task is not wired in this unit.
8
8
  */
9
- export async function pikkuRemoteScheduledJobFunc({ logger }, { taskName }) {
9
+ export async function pikkuRemoteScheduledJobFunc({ logger }, { taskName, traceparent }) {
10
10
  try {
11
- await runScheduledTask({ name: taskName, traceId: `cron-${taskName}` });
11
+ await runScheduledTask({ name: taskName, traceparent });
12
12
  }
13
13
  catch (error) {
14
14
  if (error instanceof ScheduledTaskNotFoundError) {
@@ -2,11 +2,11 @@ import type { CoreUserSession } from '../../types/core.types.js';
2
2
  import type { CoreScheduledTask } from './scheduler.types.js';
3
3
  import { PikkuError } from '../../errors/error-handler.js';
4
4
  import type { CorePikkuFunctionConfig, CorePikkuFunctionSessionless } from '../../function/functions.types.js';
5
- /** Parameters for {@link runScheduledTask}: the task's name, an optional session to run it with, and an optional trace ID. */
5
+ /** Parameters for {@link runScheduledTask}: the task's name, an optional session to run it with, and an optional W3C trace to continue. */
6
6
  export type RunScheduledTasksParams = {
7
7
  name: string;
8
8
  session?: CoreUserSession;
9
- traceId?: string;
9
+ traceparent?: string;
10
10
  };
11
11
  /**
12
12
  * Registers a sessionless function to run on a cron schedule.
@@ -35,10 +35,10 @@ export declare class ScheduledTaskNotFoundError extends PikkuError {
35
35
  * When `session` is given it is placed on the wire, so the function and its
36
36
  * middleware see that user; otherwise the task runs without one.
37
37
  *
38
- * @param params - See {@link RunScheduledTasksParams}. `traceId` defaults to `cron-<name>-<timestamp>`.
38
+ * @param params - See {@link RunScheduledTasksParams}. A missing or invalid `traceparent` starts a new trace.
39
39
  * @throws {ScheduledTaskNotFoundError} When the task or its metadata is missing.
40
40
  * @throws The error the function threw, including the one `scheduledTask.skip()` raises, so the caller can tell a skip from a failure.
41
41
  */
42
- export declare function runScheduledTask({ name, session, traceId, }: RunScheduledTasksParams): Promise<void>;
42
+ export declare function runScheduledTask({ name, session, traceparent, }: RunScheduledTasksParams): Promise<void>;
43
43
  /** Every wired scheduled task, keyed by name. Scheduler services read this to know what to schedule. */
44
44
  export declare const getScheduledTasks: () => Map<string, CoreScheduledTask>;
@@ -1,6 +1,7 @@
1
1
  import { getErrorResponse, PikkuError } from '../../errors/error-handler.js';
2
2
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
3
3
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
4
+ import { continueTrace } from '../../trace.js';
4
5
  import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
5
6
  /**
6
7
  * Registers a sessionless function to run on a cron schedule.
@@ -56,16 +57,15 @@ class ScheduledTaskSkippedError extends PikkuError {
56
57
  * When `session` is given it is placed on the wire, so the function and its
57
58
  * middleware see that user; otherwise the task runs without one.
58
59
  *
59
- * @param params - See {@link RunScheduledTasksParams}. `traceId` defaults to `cron-<name>-<timestamp>`.
60
+ * @param params - See {@link RunScheduledTasksParams}. A missing or invalid `traceparent` starts a new trace.
60
61
  * @throws {ScheduledTaskNotFoundError} When the task or its metadata is missing.
61
62
  * @throws The error the function threw, including the one `scheduledTask.skip()` raises, so the caller can tell a skip from a failure.
62
63
  */
63
- export async function runScheduledTask({ name, session, traceId, }) {
64
+ export async function runScheduledTask({ name, session, traceparent, }) {
64
65
  const singletonServices = getSingletonServices();
65
66
  const createWireServices = getCreateWireServices();
66
- const resolvedTraceId = traceId ?? `cron-${name}-${Date.now()}`;
67
- const scopedLogger = singletonServices.logger.scope?.(resolvedTraceId) ??
68
- singletonServices.logger;
67
+ const trace = continueTrace(traceparent);
68
+ const scopedLogger = singletonServices.logger.scope?.(trace.traceId) ?? singletonServices.logger;
69
69
  const task = pikkuState(null, 'scheduler', 'tasks').get(name);
70
70
  const meta = pikkuState(null, 'scheduler', 'meta')[name];
71
71
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
@@ -79,7 +79,8 @@ export async function runScheduledTask({ name, session, traceId, }) {
79
79
  throw new ScheduledTaskNotFoundError(`Scheduled task meta not found: ${name}`);
80
80
  }
81
81
  const wire = {
82
- traceId: resolvedTraceId,
82
+ traceId: trace.traceId,
83
+ trace,
83
84
  scheduledTask: {
84
85
  name,
85
86
  schedule: task.schedule,
@@ -1,6 +1,7 @@
1
1
  import { rpcService } from '../rpc/rpc-runner.js';
2
2
  import { setupTrigger } from './trigger-runner.js';
3
3
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
4
+ import { traceWire, tracedLogger } from '../../trace.js';
4
5
  /**
5
6
  * Base class for a {@link TriggerService}: pairs registered trigger sources with
6
7
  * the triggers listening to them and owns teardown. Subclasses decide where and
@@ -54,9 +55,11 @@ export class PikkuTriggerService {
54
55
  // The user rides on the wire, where the function runner resolves the
55
56
  // target's session and credentials from it and a started workflow records
56
57
  // it on the run.
57
- const wire = options?.pikkuUserId
58
- ? { pikkuUserId: options.pikkuUserId }
59
- : {};
58
+ // One trace per event, shared by every target it fires.
59
+ const wire = {
60
+ ...traceWire(),
61
+ ...(options?.pikkuUserId ? { pikkuUserId: options.pikkuUserId } : {}),
62
+ };
60
63
  const wireServices = await createWireServices?.(singletonServices, wire);
61
64
  const services = { ...singletonServices, ...wireServices };
62
65
  const rpc = rpcService.getContextRPCService(services, wire, false);
@@ -74,7 +77,7 @@ export class PikkuTriggerService {
74
77
  }
75
78
  }
76
79
  catch (error) {
77
- singletonServices.logger.error(`Error invoking ${target.targetType} '${target.targetName}' from trigger '${triggerName}': ${error}`);
80
+ tracedLogger(singletonServices.logger, wire.traceId).error(`Error invoking ${target.targetType} '${target.targetName}' from trigger '${triggerName}': ${error}`);
78
81
  }
79
82
  }
80
83
  }
@@ -3,6 +3,7 @@ import type { CoreSingletonServices } from '../../types/core.types.js';
3
3
  import type { PikkuHTTP } from '../http/http.types.js';
4
4
  import type { DeclaredTriggerSource } from '../../services/trigger-source-store.js';
5
5
  import type { CoreTriggerWebhookSource, WebhookCheckResult, WebhookSetupResult, WebhookSourceJob, WebhookSourceState, WebhookTeardownResult } from './webhook-source.types.js';
6
+ import { type TraceContext } from '../../trace.js';
6
7
  /**
7
8
  * Registers a webhook trigger source: Pikku mounts its route (by default
8
9
  * `POST /webhooks/<name>`), runs `receive` to turn each request into events,
@@ -33,11 +34,16 @@ export declare const subscribedWebhookEvents: (source: string) => string[];
33
34
  */
34
35
  export declare const receiveWebhookSourceRequest: (sourceName: string, wire: {
35
36
  http?: PikkuHTTP;
37
+ trace?: TraceContext;
36
38
  }) => Promise<{
37
39
  received: number;
38
40
  } | void>;
39
41
  /** The `pikku-incoming-webhooks` worker: runs the trigger an event was queued for. Throws so the queue retries. */
40
- export declare const dispatchWebhookSourceJob: (job: WebhookSourceJob) => Promise<void>;
42
+ export declare const dispatchWebhookSourceJob: (job: WebhookSourceJob,
43
+ /** The worker's wire, whose trace the trigger continues. */
44
+ wire?: {
45
+ trace?: TraceContext;
46
+ }) => Promise<void>;
41
47
  /** What one lifecycle pass (`check`, `setup` or `teardown`) did for one webhook source. */
42
48
  export type WebhookSourceOutcome = {
43
49
  source: string;
@@ -4,6 +4,7 @@ import { parseJson } from '../../utils.js';
4
4
  import { PikkuMissingMetaError, UnauthorizedError, } from '../../errors/errors.js';
5
5
  import { timingSafeStringEqual, verifyHmacSignature, verifyPublicKeySignature, } from '../../utils/hmac.js';
6
6
  import { webhookSecretCredentialName } from './webhook-source.types.js';
7
+ import { childTraceWire, outgoingTraceparent, } from '../../trace.js';
7
8
  const LIFECYCLE = ['receive', 'check', 'setup', 'teardown'];
8
9
  /**
9
10
  * Registers a webhook trigger source: Pikku mounts its route (by default
@@ -195,11 +196,18 @@ export const receiveWebhookSourceRequest = async (sourceName, wire) => {
195
196
  throw new Error(`Webhook source '${sourceName}' received events but no incomingWebhookService is configured to queue them.`);
196
197
  }
197
198
  return {
198
- received: await service.accept({ source: sourceName, request, events }),
199
+ received: await service.accept({
200
+ source: sourceName,
201
+ request,
202
+ events,
203
+ traceparent: outgoingTraceparent(wire),
204
+ }),
199
205
  };
200
206
  };
201
207
  /** The `pikku-incoming-webhooks` worker: runs the trigger an event was queued for. Throws so the queue retries. */
202
- export const dispatchWebhookSourceJob = async (job) => {
208
+ export const dispatchWebhookSourceJob = async (job,
209
+ /** The worker's wire, whose trace the trigger continues. */
210
+ wire = {}) => {
203
211
  const singletonServices = getSingletonServices();
204
212
  const name = triggerName(job.source, job.event.name);
205
213
  const trigger = pikkuState(null, 'trigger', 'meta')[name];
@@ -213,7 +221,7 @@ export const dispatchWebhookSourceJob = async (job) => {
213
221
  singletonServices,
214
222
  auth: false,
215
223
  data: () => job.event.data,
216
- wire: {},
224
+ wire: childTraceWire(wire.trace),
217
225
  });
218
226
  }
219
227
  catch (e) {
@@ -11,6 +11,6 @@ export declare function stripInstanceOrdinal(name: string): string;
11
11
  export declare function continueGraph(workflowService: PikkuWorkflowService, runId: string, graphName: string, overrideMeta?: WorkflowRuntimeMeta, rpcService?: any): Promise<void>;
12
12
  export declare function executeGraphStep(workflowService: PikkuWorkflowService, rpcService: any, runId: string, stepId: string, nodeId: string, rpcName: string, data: any, graphName: string): Promise<any>;
13
13
  export declare function runFromMeta(workflowService: PikkuWorkflowService, runId: string, meta: WorkflowRuntimeMeta, _rpcService: any): Promise<void>;
14
- export declare function runWorkflowGraph(workflowService: PikkuWorkflowService, graphName: string, triggerInput: any, rpcService?: any, inline?: boolean, startNode?: string, wire?: WorkflowRunWire, overrideMeta?: WorkflowRuntimeMeta): Promise<{
14
+ export declare function runWorkflowGraph(workflowService: PikkuWorkflowService, graphName: string, triggerInput: any, rpcService?: any, inline?: boolean, startNode?: string, wire?: WorkflowRunWire, overrideMeta?: WorkflowRuntimeMeta, onRunCreated?: (runId: string) => void): Promise<{
15
15
  runId: string;
16
16
  }>;
@@ -1,9 +1,11 @@
1
- import { WorkflowAsyncException, WorkflowSuspendedException, } from '../workflow-errors.js';
2
- import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, } from '../workflow-constants.js';
1
+ import { WorkflowAsyncException, WorkflowCancelledException, WorkflowSuspendedException, } from '../workflow-errors.js';
2
+ import { runInlineRetryLoop } from '../workflow-step-retry.js';
3
+ import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, isRunStopped, } from '../workflow-constants.js';
3
4
  import { childRunFailure } from '../workflow-compensation.js';
4
5
  import { runningStepLease } from '../workflow-step-lease.js';
5
6
  import { pikkuState, getSingletonServices } from '../../../pikku-state.js';
6
7
  import { RPCNotFoundError } from '../../rpc/rpc-runner.js';
8
+ import { runTraceWire } from '../workflow-run-trace.js';
7
9
  export class ChildWorkflowStartedException extends Error {
8
10
  parentRunId;
9
11
  stepId;
@@ -610,6 +612,10 @@ async function failGraphRun(workflowService, runId, failedNode, rpcService) {
610
612
  await failGraphRunWith(workflowService, runId, new Error(`Graph node '${failedNode}' failed after exhausting retries`), rpcService);
611
613
  }
612
614
  async function failGraphRunWith(workflowService, runId, cause, rpcService) {
615
+ if (rpcService &&
616
+ (await workflowService.settleStoppedRun(runId, rpcService))) {
617
+ return;
618
+ }
613
619
  const error = Object.assign(cause, { code: 'GRAPH_NODE_FAILED' });
614
620
  const run = await workflowService.getRun(runId);
615
621
  if (run && rpcService) {
@@ -664,7 +670,8 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
664
670
  return;
665
671
  }
666
672
  const currentRun = await workflowService.getRun(runId);
667
- if (currentRun?.status === 'suspended') {
673
+ if (currentRun?.status === 'suspended' ||
674
+ (currentRun && isRunStopped(currentRun.status))) {
668
675
  return;
669
676
  }
670
677
  const instances = await workflowService.getStepInstances(runId);
@@ -726,6 +733,7 @@ async function invokeGraphNodeRpc(workflowService, rpcService, runId, stepId, no
726
733
  recoveringFrom: (await workflowService.getRunState(runId))[`${RECOVERING_FROM_STATE_PREFIX}${nodeId}`],
727
734
  };
728
735
  const result = await rpcService.rpcWithWire(rpcName, input, {
736
+ ...runTraceWire(await workflowService.getRunTraceparent(runId)),
729
737
  graph: graphWire,
730
738
  workflow: workflowService.createWorkflowWire(graphName, runId, rpcService),
731
739
  });
@@ -747,6 +755,9 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
747
755
  id: rpcName,
748
756
  parentRunId: runId,
749
757
  parentStepId: stepId,
758
+ pikkuUserId: (await workflowService.getRunIdentity(runId))?.wire
759
+ ?.pikkuUserId,
760
+ traceparent: await workflowService.getRunTraceparent(runId),
750
761
  };
751
762
  const shouldInline = !getSingletonServices()?.queueService;
752
763
  const { runId: childRunId } = await workflowService.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
@@ -798,9 +809,9 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
798
809
  return;
799
810
  const rpcName = node.rpcName;
800
811
  const stepState = await workflowService.insertStepState(runId, instanceKey, rpcName, input, { retries: node.retries ?? 0, retryDelay: node.retryDelay }, fromStepName);
801
- await workflowService.setStepRunning(stepState.stepId);
802
- try {
803
- let result;
812
+ let attempt = stepState;
813
+ const runNode = async (current) => {
814
+ attempt = current;
804
815
  const subWorkflowMeta = getWorkflowMeta(rpcName);
805
816
  const agentMeta = subWorkflowMeta
806
817
  ? undefined
@@ -810,42 +821,48 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
810
821
  type: 'workflow',
811
822
  id: rpcName,
812
823
  parentRunId: runId,
813
- parentStepId: stepState.stepId,
824
+ parentStepId: attempt.stepId,
825
+ pikkuUserId: (await workflowService.getRunIdentity(runId))?.wire
826
+ ?.pikkuUserId,
827
+ traceparent: await workflowService.getRunTraceparent(runId),
814
828
  };
815
829
  const { runId: childRunId } = await workflowService.startWorkflow(rpcName, input, childWire, rpcService, { inline: true });
816
- await workflowService.setStepChildRunId(stepState.stepId, childRunId);
830
+ await workflowService.setStepChildRunId(attempt.stepId, childRunId);
817
831
  const childRun = await workflowService.getRun(childRunId);
818
832
  const failure = childRun && childRunFailure(childRun);
819
833
  if (failure)
820
834
  throw failure;
821
- result = childRun?.output;
835
+ return childRun?.output;
822
836
  }
823
- else if (agentMeta) {
837
+ if (agentMeta) {
824
838
  const agentRun = await rpcService.agent.run(rpcName, input);
825
- result = agentRun.result;
839
+ return agentRun.result;
826
840
  }
827
- else {
828
- result = await invokeGraphNodeRpc(workflowService, rpcService, runId, stepState.stepId, nodeId, rpcName, input, graphName);
829
- }
830
- await workflowService.setStepResult(stepState.stepId, result);
841
+ return invokeGraphNodeRpc(workflowService, rpcService, runId, attempt.stepId, nodeId, rpcName, input, graphName);
842
+ };
843
+ try {
844
+ await runInlineRetryLoop(workflowService, stepState, node.retries ?? 0, node.retryDelay, runNode, async (error) => {
845
+ if (error instanceof WorkflowAsyncException ||
846
+ error instanceof WorkflowSuspendedException) {
847
+ throw error;
848
+ }
849
+ if (error instanceof RPCNotFoundError) {
850
+ await workflowService.setStepError(attempt.stepId, error);
851
+ await workflowService.updateRunStatus(runId, 'suspended', undefined, {
852
+ message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
853
+ code: 'RPC_NOT_FOUND',
854
+ });
855
+ throw new WorkflowSuspendedException(runId, 'RPC_NOT_FOUND');
856
+ }
857
+ }, () => workflowService.throwIfRunStopped(runId));
831
858
  }
832
859
  catch (error) {
833
860
  if (error instanceof WorkflowAsyncException ||
834
- error instanceof WorkflowSuspendedException) {
861
+ error instanceof WorkflowSuspendedException ||
862
+ error instanceof WorkflowCancelledException ||
863
+ !recoversFailure(node)) {
835
864
  throw error;
836
865
  }
837
- if (error instanceof RPCNotFoundError) {
838
- await workflowService.setStepError(stepState.stepId, error);
839
- await workflowService.updateRunStatus(runId, 'suspended', undefined, {
840
- message: `RPC '${rpcName}' not found. Deploy the missing function and resume.`,
841
- code: 'RPC_NOT_FOUND',
842
- });
843
- throw new WorkflowSuspendedException(runId, 'RPC_NOT_FOUND');
844
- }
845
- await workflowService.setStepError(stepState.stepId, error);
846
- if (recoversFailure(node))
847
- return;
848
- throw error;
849
866
  }
850
867
  }
851
868
  async function continueGraphInline(workflowService, rpcService, runId, graphName, nodes, triggerInput, entryNodeIds) {
@@ -859,7 +876,8 @@ async function continueGraphInline(workflowService, rpcService, runId, graphName
859
876
  return;
860
877
  }
861
878
  const run = await workflowService.getRun(runId);
862
- if (run?.status === 'suspended') {
879
+ if (run?.status === 'suspended' ||
880
+ (await workflowService.settleStoppedRun(runId, rpcService))) {
863
881
  return;
864
882
  }
865
883
  const instances = await workflowService.getStepInstances(runId);
@@ -916,7 +934,7 @@ function planEntryFirings(graphName, nodeId, node, triggerNodeResults, triggerIn
916
934
  }
917
935
  return firings;
918
936
  }
919
- export async function runWorkflowGraph(workflowService, graphName, triggerInput, rpcService, inline, startNode, wire, overrideMeta) {
937
+ export async function runWorkflowGraph(workflowService, graphName, triggerInput, rpcService, inline, startNode, wire, overrideMeta, onRunCreated) {
920
938
  const meta = overrideMeta ?? getWorkflowMeta(graphName);
921
939
  if (!meta?.nodes) {
922
940
  throw new Error(`Workflow graph '${graphName}' not found`);
@@ -946,6 +964,7 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
946
964
  deterministic: meta.deterministic,
947
965
  plannedSteps: meta.plannedSteps,
948
966
  });
967
+ onRunCreated?.(runId);
949
968
  if (inline) {
950
969
  workflowService.registerInlineRun(runId);
951
970
  }
@@ -7,6 +7,7 @@ export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js';
7
7
  export { streamWorkflowRunStatus, type WorkflowStatusStreamParams, } from './workflow-status-stream.js';
8
8
  export type { WorkflowRunEngine, WorkflowRunExtension, } from './workflow-run-engine.types.js';
9
9
  export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js';
10
+ export { runTraceWire } from './workflow-run-trace.js';
10
11
  export { isRef } from './graph/workflow-graph.types.js';
11
12
  export type { RefValue } from './graph/workflow-graph.types.js';
12
13
  export { buildRunTimeline, reconstructStateAt, reconstructFinalState, } from './run-timeline.js';
@@ -15,7 +16,8 @@ export { addWorkflow } from './dsl/workflow-runner.js';
15
16
  export { template, type TemplateString } from './graph/template.js';
16
17
  export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult, } from './graph/wire-workflow-graph.js';
17
18
  export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
18
- export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
19
+ export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceOptions, WorkflowRecoveryOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
19
20
  export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, MilestoneStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
20
21
  export { createGraph } from './graph/graph-node.js';
21
22
  export type { GraphNodeConfig, ForEachConfig, ForEachMode, ItemFn, TemplateFn, } from './graph/workflow-graph.types.js';
23
+ export type { ScheduledStep, StalledSweepOptions, UndispatchedSweepOptions, WorkflowRecoveryPass, } from './workflow-recovery.js';
@@ -6,6 +6,7 @@ export { assertWorkflowRunOwner, WorkflowRunForbiddenError, } from './workflow-r
6
6
  export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js';
7
7
  export { streamWorkflowRunStatus, } from './workflow-status-stream.js';
8
8
  export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js';
9
+ export { runTraceWire } from './workflow-run-trace.js';
9
10
  export { isRef } from './graph/workflow-graph.types.js';
10
11
  export { buildRunTimeline, reconstructStateAt, reconstructFinalState, } from './run-timeline.js';
11
12
  export { addWorkflow } from './dsl/workflow-runner.js';
@@ -1,4 +1,5 @@
1
1
  import { runPikkuFunc } from '../../function/function-runner.js';
2
+ import { runTraceWire } from './workflow-run-trace.js';
2
3
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
3
4
  import { getDurationInMilliseconds } from '../../time-utils.js';
4
5
  import { closeWireServices } from '../../utils.js';
@@ -667,6 +668,7 @@ export class PikkuScenarioService {
667
668
  const runOnSurface = async (surface) => {
668
669
  // No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
669
670
  const wire = {
671
+ ...runTraceWire(await this.engine.getRunTraceparent(runId)),
670
672
  workflow: workflowWire,
671
673
  scenario: workflowWire,
672
674
  pikkuUserId: workflowWire.pikkuUserId,
@@ -1,13 +1,14 @@
1
1
  import type { CoreUserSession } from '../../types/core.types.js';
2
2
  import type { SerializedError } from '../../errors/serialized-error.js';
3
3
  import type { GroupConcurrencyConfig, JobGroup, JobOptions } from '../queue/queue.types.js';
4
- import type { PikkuWorkflowWire, StepState, StepStatus, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowQueueOptions, WorkflowStepOptions } from './workflow.types.js';
4
+ import type { PikkuWorkflowWire, StepState, StepStatus, WorkflowPlannedStep, WorkflowRun, WorkflowRunMirror, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowQueueOptions, WorkflowRecoveryOptions, WorkflowStepOptions } from './workflow.types.js';
5
5
  import type { WorkflowService } from '../../services/workflow-service.js';
6
6
  import type { ScenarioVirtualUsers } from '../../services/virtual-users-service.js';
7
7
  import type { PikkuRPC } from '../rpc/rpc-types.js';
8
8
  import { type RunTimeline, type ReconstructedRunState } from './run-timeline.js';
9
9
  import type { WorkflowRunEngine, WorkflowRunExtension } from './workflow-run-engine.types.js';
10
10
  import type { LeaseService } from '../../services/lease-service.js';
11
+ import { type ScheduledStep, type StalledSweepOptions, type UndispatchedSweepOptions, type WorkflowRecoveryPass } from './workflow-recovery.js';
11
12
  /** The workflow engine; subclasses supply storage. Leases serialise runs and step claims over a shared store. */
12
13
  export declare abstract class PikkuWorkflowService implements WorkflowService {
13
14
  private runExtension?;
@@ -17,6 +18,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
17
18
  private enterExecution;
18
19
  private exitExecution;
19
20
  protected get logger(): import("../../services/logger.js").Logger;
21
+ private readonly lostStepLog;
20
22
  protected mirror?: WorkflowRunMirror;
21
23
  /**
22
24
  * Serialises orchestration passes over a run and claims on a step. A service
@@ -129,59 +131,45 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
129
131
  } | null>;
130
132
  resumeWorkflow(runId: string, workflowName?: string): Promise<void>;
131
133
  private enqueueOrchestration;
134
+ private addOrchestratorJob;
132
135
  /**
133
- * Ids of runs that are stalled: still `running`, with no step in a state that
134
- * something is expected to complete (`scheduled`, `suspended`, or `running`
135
- * under a live lease), and no step activity since `before`.
136
+ * Ids of runs that are stalled: still `running`, with no step something is
137
+ * expected to complete (`scheduled`, `suspended`, `running` under a live
138
+ * lease, or parked on a child run), and no step activity since `before`.
136
139
  *
137
- * A `running` step whose lease has lapsed is the opposite of in flight — the
138
- * dispatch that claimed it is gone — so it does not hold its run out of the
139
- * sweep.
140
+ * A lapsed lease is a dispatch that died, so it holds nothing out of the
141
+ * sweep. Nor does a `running` step with no lease and no child run: that is
142
+ * work inside an orchestration pass, whose owner is the run's lease, which the
143
+ * sweep checks before it touches the run.
140
144
  *
141
- * Returns nothing by default so a store that cannot express the query keeps
142
- * working unchanged; a store that overrides it gains crash recovery through
143
- * `recoverStalledRuns`.
145
+ * Returns nothing by default, for a store that cannot express the query.
144
146
  */
145
147
  protected findStalledRunIds(_before: Date, _limit: number): Promise<string[]>;
146
148
  /**
147
- * Runs holding a step that has sat `pending` since before `before`, paired
148
- * with the step that flagged them.
149
- *
150
- * Returns nothing by default so a store that cannot express the query keeps
151
- * working unchanged — and, because it does not opt in, gains no re-dispatches
152
- * either. A store must have an atomic `claimStepForExecution` before
153
- * overriding this, or no concurrency for one to exclude: the relay makes
154
- * duplicate dispatch routine, and the claim is what keeps a duplicate from
155
- * becoming a second execution. Every `@pikku/kysely` dialect qualifies on its
156
- * status-guarded claim, `mongodb` on the same claim expressed as a
157
- * single-document update, `in-memory` on being inline and single-process —
158
- * none of them overrides this yet.
149
+ * Runs holding a step `pending` since before `before`. A store overriding it
150
+ * needs an atomic `claimStepForExecution` (see `sweepUndispatchedSteps`).
159
151
  */
160
152
  protected findUndispatchedSteps(_before: Date, _limit: number): Promise<Array<{
161
153
  runId: string;
162
154
  stepId: string;
163
155
  }>>;
164
- private readonly redispatchBackoff;
165
- private get sweepDeps();
166
156
  /**
167
- * Re-drive runs whose next move was lost. Not self-starting — call it from a
168
- * scheduled task at whatever interval suits the workload.
157
+ * Steps parked on `scheduled` on a live run, oldest first, a page at a time.
158
+ * Returns nothing by default, for a store whose runs die with the process.
169
159
  */
170
- recoverStalledRuns(options?: {
171
- stalledAfterMs?: number;
172
- limit?: number;
173
- }): Promise<{
160
+ protected findScheduledSteps(_limit: number, _offset: number): Promise<ScheduledStep[]>;
161
+ private readonly recovery;
162
+ /** One recovery pass, for a host driving it from a scheduled task. */
163
+ recoverWorkflows(options?: WorkflowRecoveryPass): Promise<void>;
164
+ /** Sweep now and on an interval, once the queue is consuming. */
165
+ startRecovery(options?: WorkflowRecoveryOptions | false): void;
166
+ stopRecovery(): void;
167
+ /** Re-drive runs whose next move was lost. */
168
+ recoverStalledRuns(options?: StalledSweepOptions): Promise<{
174
169
  resumed: string[];
175
170
  }>;
176
- /**
177
- * Re-drive steps whose dispatch was lost. Not self-starting — call it from a
178
- * scheduled task; ~30s suits a queue whose `pending`→`running` latency is
179
- * well under that.
180
- */
181
- relayUndispatchedSteps(options?: {
182
- undispatchedAfterMs?: number;
183
- limit?: number;
184
- }): Promise<{
171
+ /** Re-drive steps whose dispatch was lost. Not self-starting. */
172
+ relayUndispatchedSteps(options?: UndispatchedSweepOptions): Promise<{
185
173
  redispatched: string[];
186
174
  }>;
187
175
  protected resolveStepJobOptions(stepOptions?: WorkflowStepOptions): JobOptions;
@@ -212,7 +200,13 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
212
200
  private beginReplay;
213
201
  private endReplay;
214
202
  private loadOrCreateStep;
215
- private getRunIdentity;
203
+ getRunIdentity(runId: string): Promise<WorkflowRun | null>;
204
+ /** The trace every hop of the run stays on. */
205
+ getRunTraceparent(runId: string): Promise<string | undefined>;
206
+ /** Warn that a step lost its worker and is running again, as `attempt`. */
207
+ logStepRerun(runId: string, stepName: string, attempt: number): Promise<void>;
208
+ /** Warn that a step lost its worker with no attempts left, and failed. */
209
+ logStepAttemptsExhausted(runId: string, stepName: string, attempts: number): Promise<void>;
216
210
  private lastStepName;
217
211
  private nextStepKey;
218
212
  runWorkflowJob(runId: string, rpcService: PikkuRPC): Promise<void>;
@@ -256,6 +250,9 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
256
250
  failOrUnwind(run: WorkflowRun, error: Error & {
257
251
  code?: string;
258
252
  }, rpcService: PikkuRPC): Promise<'unwinding' | 'finished' | 'failed'>;
253
+ /** Ends a run's body at its next inline attempt once the run has been stopped. */
254
+ throwIfRunStopped(runId: string): Promise<void>;
255
+ settleStoppedRun(runId: string, rpcService: PikkuRPC): Promise<WorkflowStatus | undefined>;
259
256
  /** Stops an unfinished run, children first, and unwinds what it completed. */
260
257
  cancelRun(runId: string, rpcService: PikkuRPC, reason?: string): Promise<WorkflowStatus | undefined>;
261
258
  protected markFailedUnlessUnwound(runId: string, error: SerializedError): Promise<void>;