@sixb/pipeline-worker 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.ts CHANGED
@@ -1,25 +1,9 @@
1
1
  import type { DatasetDefinition, PipelineDefinition } from "@sixb/core"
2
+ import { createSixbError, isSixbError, summarizeErrorMessage } from "@sixb/core/internal/errors"
2
3
  import type { DatasetVersion } from "@sixb/core/lake-storage"
3
- import type { PipelineRunFailure, PipelineRunStatus } from "@sixb/core/storage"
4
+ import type { PipelineRunStatus } from "@sixb/core/storage"
4
5
  import type { PipelineJob } from "./types"
5
6
 
6
- export class PipelineWorkerError extends Error {
7
- readonly name = "PipelineWorkerError"
8
- }
9
-
10
- export function toPipelineRunFailure(error: unknown): PipelineRunFailure {
11
- if (error instanceof Error) {
12
- return {
13
- name: error.name,
14
- message: error.message,
15
- }
16
- }
17
-
18
- return {
19
- message: String(error),
20
- }
21
- }
22
-
23
7
  export function throwIfAborted(signal: AbortSignal): void {
24
8
  if (signal.aborted) {
25
9
  throw createAbortError()
@@ -33,39 +17,94 @@ export function statusForFailure(
33
17
  return signal.aborted || isAbortError(error) ? "cancelled" : "failed"
34
18
  }
35
19
 
36
- export function requireFinishedAt(runId: string, finishedAt: Date | undefined): Date {
37
- if (finishedAt) {
38
- return finishedAt
20
+ export function requireFinishedAt(input: {
21
+ readonly pipelineId: string
22
+ readonly runId: string
23
+ readonly finishedAt: Date | undefined
24
+ }): Date {
25
+ if (input.finishedAt) {
26
+ return input.finishedAt
39
27
  }
40
28
 
41
- throw new PipelineWorkerError(
42
- `[SixbPipelineWorker] Pipeline run '${runId}' finished without a finishedAt timestamp.`
29
+ throw createSixbError(
30
+ "internal.unexpected",
31
+ `[SixbPipelineWorker] Pipeline run '${input.runId}' finished without a finishedAt timestamp.`,
32
+ { details: { pipelineId: input.pipelineId, runId: input.runId } }
43
33
  )
44
34
  }
45
35
 
46
36
  export function createStepBookkeepingError(options: {
47
37
  readonly pipelineId: string
38
+ readonly pipelineRunId: string
48
39
  readonly stepId: string
49
- readonly runId: string
40
+ readonly stepRunId: string
50
41
  readonly version: DatasetVersion
51
42
  readonly cause: unknown
52
43
  }): Error {
53
44
  // The dataset version is already durable; retrying the whole pipeline could duplicate appends.
54
- return new PipelineWorkerError(
55
- `[SixbPipelineWorker] Pipeline '${options.pipelineId}' step '${options.stepId}' committed dataset version '${options.version.versionId}', but failed to finalize step run '${options.runId}'. The dataset commit may already have succeeded and the step run record may need repair.`,
56
- { cause: options.cause }
45
+ return createSixbError(
46
+ "internal.unexpected",
47
+ `[SixbPipelineWorker] Pipeline '${options.pipelineId}' step '${options.stepId}' committed dataset version '${options.version.versionId}', but failed to finalize step run '${options.stepRunId}'. The dataset commit may already have succeeded and the step run record may need repair.`,
48
+ {
49
+ cause: options.cause,
50
+ details: {
51
+ pipelineId: options.pipelineId,
52
+ pipelineRunId: options.pipelineRunId,
53
+ stepId: options.stepId,
54
+ stepRunId: options.stepRunId,
55
+ datasetId: options.version.datasetId,
56
+ versionId: options.version.versionId,
57
+ },
58
+ }
57
59
  )
58
60
  }
59
61
 
62
+ export function createPipelineStepFailure(options: {
63
+ readonly pipelineId: string
64
+ readonly pipelineRunId: string
65
+ readonly stepId: string
66
+ readonly stepRunId?: string
67
+ readonly cause: unknown
68
+ }) {
69
+ return createSixbError(
70
+ "pipeline.step_failed",
71
+ summarizeErrorMessage(options.cause, "Pipeline step execution failed."),
72
+ {
73
+ cause: options.cause,
74
+ details: {
75
+ pipelineId: options.pipelineId,
76
+ pipelineRunId: options.pipelineRunId,
77
+ stepId: options.stepId,
78
+ ...(options.stepRunId ? { stepRunId: options.stepRunId } : {}),
79
+ },
80
+ }
81
+ )
82
+ }
83
+
84
+ export function unwrapPipelineStepFailure(error: unknown): unknown {
85
+ return isSixbError(error) && error.code === "pipeline.step_failed" && error.cause !== undefined
86
+ ? error.cause
87
+ : error
88
+ }
89
+
60
90
  export function createPipelineBookkeepingError(options: {
61
91
  readonly pipelineId: string
62
92
  readonly runId: string
63
93
  readonly version: DatasetVersion
64
94
  readonly cause: unknown
65
95
  }): Error {
66
- return new PipelineWorkerError(
96
+ return createSixbError(
97
+ "internal.unexpected",
67
98
  `[SixbPipelineWorker] Pipeline '${options.pipelineId}' committed final dataset version '${options.version.versionId}', but failed to finalize pipeline run '${options.runId}'. The dataset commit may already have succeeded and the pipeline run record may need repair.`,
68
- { cause: options.cause }
99
+ {
100
+ cause: options.cause,
101
+ details: {
102
+ pipelineId: options.pipelineId,
103
+ runId: options.runId,
104
+ datasetId: options.version.datasetId,
105
+ versionId: options.version.versionId,
106
+ },
107
+ }
69
108
  )
70
109
  }
71
110
 
@@ -74,7 +113,11 @@ export function requirePipeline(
74
113
  job: PipelineJob
75
114
  ): PipelineDefinition {
76
115
  if (!pipeline) {
77
- throw new PipelineWorkerError(`[SixbPipelineWorker] Unknown pipeline '${job.pipelineId}'.`)
116
+ throw createSixbError(
117
+ "internal.unexpected",
118
+ `[SixbPipelineWorker] Unknown pipeline '${job.pipelineId}'.`,
119
+ { details: { pipelineId: job.pipelineId, runId: job.id } }
120
+ )
78
121
  }
79
122
 
80
123
  return pipeline
@@ -83,6 +126,7 @@ export function requirePipeline(
83
126
  export function requireRegisteredDataset(options: {
84
127
  readonly dataset: DatasetDefinition | null
85
128
  readonly pipelineId: string
129
+ readonly pipelineRunId: string
86
130
  readonly stepId: string
87
131
  readonly role: "input" | "output"
88
132
  readonly name?: string
@@ -94,8 +138,17 @@ export function requireRegisteredDataset(options: {
94
138
 
95
139
  const namedRole = options.role === "input" ? `input '${options.name ?? "unknown"}'` : "output"
96
140
 
97
- throw new PipelineWorkerError(
98
- `[SixbPipelineWorker] Pipeline '${options.pipelineId}' step '${options.stepId}' ${namedRole} references unknown dataset '${options.datasetId}'.`
141
+ throw createSixbError(
142
+ "internal.unexpected",
143
+ `[SixbPipelineWorker] Pipeline '${options.pipelineId}' step '${options.stepId}' ${namedRole} references unknown dataset '${options.datasetId}'.`,
144
+ {
145
+ details: {
146
+ pipelineId: options.pipelineId,
147
+ pipelineRunId: options.pipelineRunId,
148
+ stepId: options.stepId,
149
+ datasetId: options.datasetId,
150
+ },
151
+ }
99
152
  )
100
153
  }
101
154
 
package/src/events.ts CHANGED
@@ -10,8 +10,12 @@ const SOURCE = "SixbPipelineWorker"
10
10
 
11
11
  export async function emitPipelineRunStarted(
12
12
  events: DomainEventLog | undefined,
13
- run: Pick<PipelineRunRecord, "id" | "pipelineId" | "startedAt">
13
+ run: Pick<PipelineRunRecord, "id" | "pipelineId" | "startedAt">,
14
+ correlationId: string
14
15
  ): Promise<void> {
16
+ if (!run.startedAt) {
17
+ throw new Error(`[SixbPipelineWorker] Pipeline run '${run.id}' started without a timestamp.`)
18
+ }
15
19
  await events?.emit(
16
20
  {
17
21
  events: [
@@ -24,6 +28,7 @@ export async function emitPipelineRunStarted(
24
28
  },
25
29
  },
26
30
  ],
31
+ correlationId,
27
32
  },
28
33
  { source: SOURCE }
29
34
  )
@@ -35,7 +40,8 @@ export async function emitPipelineRunStepStarted(
35
40
  PipelineStepRunRecord,
36
41
  "id" | "pipelineRunId" | "pipelineId" | "stepId" | "datasetId" | "startedAt"
37
42
  >,
38
- context: PipelineStepLifecycleContext
43
+ context: PipelineStepLifecycleContext,
44
+ correlationId: string
39
45
  ): Promise<void> {
40
46
  await events?.emit(
41
47
  {
@@ -54,6 +60,7 @@ export async function emitPipelineRunStepStarted(
54
60
  },
55
61
  },
56
62
  ],
63
+ correlationId,
57
64
  },
58
65
  { source: SOURCE }
59
66
  )
@@ -74,7 +81,8 @@ export async function emitPipelineRunStepFinished(
74
81
  | "rowsWritten"
75
82
  | "error"
76
83
  >,
77
- context: PipelineStepLifecycleContext
84
+ context: PipelineStepLifecycleContext,
85
+ correlationId: string
78
86
  ): Promise<void> {
79
87
  await events?.emit(
80
88
  {
@@ -93,10 +101,11 @@ export async function emitPipelineRunStepFinished(
93
101
  finishedAt: requireFinishedAt(step.id, step.finishedAt).toISOString(),
94
102
  ...(step.output ? { versionId: step.output.versionId } : {}),
95
103
  ...(step.rowsWritten !== undefined ? { rowsWritten: step.rowsWritten } : {}),
96
- ...(step.error ? { error: step.error.message } : {}),
104
+ ...(step.error ? { error: step.error } : {}),
97
105
  },
98
106
  },
99
107
  ],
108
+ correlationId,
100
109
  },
101
110
  { source: SOURCE }
102
111
  )
@@ -105,7 +114,8 @@ export async function emitPipelineRunStepFinished(
105
114
  export async function emitDatasetVersionCommitted(
106
115
  events: DomainEventLog | undefined,
107
116
  job: PipelineJob,
108
- step: PipelineStepRunResult
117
+ step: PipelineStepRunResult,
118
+ correlationId: string
109
119
  ): Promise<void> {
110
120
  if (!step.versionCreated) return
111
121
 
@@ -128,6 +138,7 @@ export async function emitDatasetVersionCommitted(
128
138
  },
129
139
  },
130
140
  ],
141
+ correlationId,
131
142
  },
132
143
  { source: SOURCE }
133
144
  )
@@ -135,37 +146,32 @@ export async function emitDatasetVersionCommitted(
135
146
 
136
147
  export async function emitPipelineRunFinished(
137
148
  events: DomainEventLog | undefined,
138
- job: {
139
- readonly id: string
140
- readonly pipelineId: string
141
- readonly status: "succeeded" | "failed" | "cancelled"
142
- readonly datasetId?: string
143
- readonly versionId?: string
144
- }
149
+ run: Pick<PipelineRunRecord, "id" | "pipelineId" | "status" | "output" | "error">,
150
+ correlationId: string
145
151
  ): Promise<void> {
146
152
  await events?.emit(
147
- { events: [{ type: "pipeline.run.finished", payload: buildPipelineRunFinishedPayload(job) }] },
153
+ {
154
+ events: [{ type: "pipeline.run.finished", payload: buildPipelineRunFinishedPayload(run) }],
155
+ correlationId,
156
+ },
148
157
  { source: SOURCE }
149
158
  )
150
159
  }
151
160
 
152
- function buildPipelineRunFinishedPayload(job: {
153
- readonly id: string
154
- readonly pipelineId: string
155
- readonly status: "succeeded" | "failed" | "cancelled"
156
- readonly datasetId?: string
157
- readonly versionId?: string
158
- }) {
161
+ function buildPipelineRunFinishedPayload(
162
+ run: Pick<PipelineRunRecord, "id" | "pipelineId" | "status" | "output" | "error">
163
+ ) {
159
164
  return {
160
- pipelineId: job.pipelineId,
161
- runId: job.id,
162
- status: job.status,
163
- ...(job.datasetId && job.versionId
165
+ pipelineId: run.pipelineId,
166
+ runId: run.id,
167
+ status: requireTerminalStatus(run.status, `Pipeline run '${run.id}'`),
168
+ ...(run.output
164
169
  ? {
165
- datasetId: job.datasetId,
166
- versionId: job.versionId,
170
+ datasetId: run.output.datasetId,
171
+ versionId: run.output.versionId,
167
172
  }
168
173
  : {}),
174
+ ...(run.error ? { error: run.error } : {}),
169
175
  }
170
176
  }
171
177
 
@@ -180,8 +186,8 @@ function requireFinishedAt(stepRunId: string, finishedAt: Date | undefined): Dat
180
186
  function requireTerminalStatus(
181
187
  status: PipelineRunStatus,
182
188
  context: string
183
- ): Exclude<PipelineRunStatus, "running"> {
184
- if (status === "running") {
189
+ ): Exclude<PipelineRunStatus, "queued" | "running"> {
190
+ if (status === "queued" || status === "running") {
185
191
  throw new Error(`[SixbPipelineWorker] ${context} is still running.`)
186
192
  }
187
193
 
@@ -5,8 +5,9 @@ import type {
5
5
  PipelineStepDefinition,
6
6
  PipelineStepRunContext,
7
7
  } from "@sixb/core"
8
+ import { createSixbError } from "@sixb/core/internal/errors"
8
9
  import type { DatasetVersion } from "@sixb/core/lake-storage"
9
- import { PipelineWorkerError, throwIfAborted } from "./errors"
10
+ import { throwIfAborted } from "./errors"
10
11
  import { createStepInputs, type ResolvedStepInput } from "./step-inputs"
11
12
  import type { PipelineJob, PipelineLogSession, PipelineWorkerContext } from "./types"
12
13
 
@@ -27,8 +28,17 @@ export async function executeRunStep(input: {
27
28
  const { runtime, pipeline, step, job, signal, outputDataset, resolvedInputs } = input
28
29
 
29
30
  if (step.executor.kind !== "run") {
30
- throw new PipelineWorkerError(
31
- `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses SQL execution, which is not supported by the run step executor.`
31
+ throw createSixbError(
32
+ "internal.unexpected",
33
+ `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses SQL execution, which is not supported by the run step executor.`,
34
+ {
35
+ details: {
36
+ pipelineId: pipeline.id,
37
+ pipelineRunId: job.id,
38
+ stepId: step.id,
39
+ datasetId: outputDataset.id,
40
+ },
41
+ }
32
42
  )
33
43
  }
34
44
 
@@ -5,12 +5,13 @@ import type {
5
5
  PipelineDefinition,
6
6
  PipelineStepDefinition,
7
7
  } from "@sixb/core"
8
+ import { createSixbError } from "@sixb/core/internal/errors"
8
9
  import type {
9
10
  DatasetVersion,
10
11
  DatasetWriteMode,
11
12
  LakeSqlTransformCapabilities,
12
13
  } from "@sixb/core/lake-storage"
13
- import { PipelineWorkerError, throwIfAborted } from "./errors"
14
+ import { throwIfAborted } from "./errors"
14
15
  import type { ResolvedStepInput } from "./step-inputs"
15
16
  import type { PipelineJob, PipelineWorkerContext } from "./types"
16
17
 
@@ -27,7 +28,9 @@ function assertSqlModeSupported(input: {
27
28
  readonly dialect: string
28
29
  readonly mode: DatasetWriteMode
29
30
  readonly pipelineId: string
31
+ readonly pipelineRunId: string
30
32
  readonly stepId: string
33
+ readonly datasetId: string
31
34
  }): void {
32
35
  const supported =
33
36
  input.mode === "append"
@@ -35,9 +38,18 @@ function assertSqlModeSupported(input: {
35
38
  : input.capabilities.supportsSnapshot
36
39
  if (supported) return
37
40
 
38
- throw new PipelineWorkerError(
41
+ throw createSixbError(
42
+ "internal.unexpected",
39
43
  `[SixbPipelineWorker] Pipeline '${input.pipelineId}' step '${input.stepId}' writes in ` +
40
- `'${input.mode}' mode, which the ${input.dialect} SQL executor does not support.`
44
+ `'${input.mode}' mode, which the ${input.dialect} SQL executor does not support.`,
45
+ {
46
+ details: {
47
+ pipelineId: input.pipelineId,
48
+ pipelineRunId: input.pipelineRunId,
49
+ stepId: input.stepId,
50
+ datasetId: input.datasetId,
51
+ },
52
+ }
41
53
  )
42
54
  }
43
55
 
@@ -57,14 +69,32 @@ export async function executeSqlStep(input: {
57
69
  const { runtime, pipeline, step, job, signal, outputDataset, resolvedInputs } = input
58
70
 
59
71
  if (step.executor.kind !== "sql") {
60
- throw new PipelineWorkerError(
61
- `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses run execution, which is not supported by the SQL step executor.`
72
+ throw createSixbError(
73
+ "internal.unexpected",
74
+ `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses run execution, which is not supported by the SQL step executor.`,
75
+ {
76
+ details: {
77
+ pipelineId: pipeline.id,
78
+ pipelineRunId: job.id,
79
+ stepId: step.id,
80
+ datasetId: outputDataset.id,
81
+ },
82
+ }
62
83
  )
63
84
  }
64
85
 
65
86
  if (!hasSqlExecutor(runtime.lakeStorage)) {
66
- throw new PipelineWorkerError(
67
- `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' requires SQL transform support, but lake storage does not provide lakeStorage.sql.execute(...).`
87
+ throw createSixbError(
88
+ "internal.unexpected",
89
+ `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' requires SQL transform support, but lake storage does not provide lakeStorage.sql.execute(...).`,
90
+ {
91
+ details: {
92
+ pipelineId: pipeline.id,
93
+ pipelineRunId: job.id,
94
+ stepId: step.id,
95
+ datasetId: outputDataset.id,
96
+ },
97
+ }
68
98
  )
69
99
  }
70
100
 
@@ -77,7 +107,9 @@ export async function executeSqlStep(input: {
77
107
  dialect: runtime.lakeStorage.sql.dialect,
78
108
  mode: step.mode,
79
109
  pipelineId: pipeline.id,
110
+ pipelineRunId: job.id,
80
111
  stepId: step.id,
112
+ datasetId: outputDataset.id,
81
113
  })
82
114
 
83
115
  throwIfAborted(signal)
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { PipelineWorkerSixb } from "./types"
1
+ export type { PipelineWorkerHost } from "./types"
2
2
  export { PipelineWorker } from "./worker"
@@ -1,16 +1,22 @@
1
- import { resolveLogsRuntime } from "@sixb/core/internal/logging"
1
+ import { captureSixbFailure } from "@sixb/core/internal/errors"
2
+ import { resolveLoggingService } from "@sixb/core/internal/logging"
2
3
  import type { DatasetVersion } from "@sixb/core/lake-storage"
3
- import type { PipelineRunRecord } from "@sixb/core/storage"
4
+ import { PIPELINE_RUN_FAILURE_CODES, type PipelineRunRecord } from "@sixb/core/storage"
4
5
  import {
5
6
  createPipelineBookkeepingError,
6
7
  requireFinishedAt,
7
8
  requirePipeline,
8
9
  statusForFailure,
9
10
  throwIfAborted,
10
- toPipelineRunFailure,
11
+ unwrapPipelineStepFailure,
11
12
  } from "./errors"
12
13
  import { runStep } from "./run-step"
13
- import type { PipelineRunResult, PipelineStepRunResult, RunPipelineJobInput } from "./types"
14
+ import type {
15
+ PipelineRunFinishedHandler,
16
+ PipelineRunResult,
17
+ PipelineStepRunResult,
18
+ RunPipelineJobInput,
19
+ } from "./types"
14
20
 
15
21
  export class PipelineRunAlreadyStartedError extends Error {
16
22
  override readonly name = "PipelineRunAlreadyStartedError"
@@ -21,10 +27,15 @@ export class PipelineRunAlreadyStartedError extends Error {
21
27
  }
22
28
 
23
29
  export async function runPipelineJob(input: RunPipelineJobInput): Promise<PipelineRunResult> {
24
- const { runtime, job } = input
30
+ const { runtime, run: durableRun } = input
31
+ const job = { id: durableRun.id, pipelineId: durableRun.pipelineId }
25
32
  const signal = input.signal ?? new AbortController().signal
26
- const pipeline = requirePipeline(runtime.getPipelineById(job.pipelineId), job)
27
- const logSession = resolveLogsRuntime(runtime.id, runtime.logs).startExecution({
33
+ if (durableRun.projectId !== runtime.id) {
34
+ throw new Error(
35
+ `[SixbPipelineWorker] Pipeline run '${durableRun.id}' belongs to project '${durableRun.projectId}', not '${runtime.id}'.`
36
+ )
37
+ }
38
+ const logSession = resolveLoggingService(runtime.id, runtime.logging).startExecution({
28
39
  kind: "pipeline",
29
40
  id: job.id,
30
41
  })
@@ -40,20 +51,24 @@ export async function runPipelineJob(input: RunPipelineJobInput): Promise<Pipeli
40
51
  startedRun = await runtime.pipelineRunsStorage.start({
41
52
  projectId: runtime.id,
42
53
  id: job.id,
43
- pipelineId: pipeline.id,
44
54
  })
45
55
  } catch (error) {
46
56
  const existing = await runtime.pipelineRunsStorage.getById({
47
57
  projectId: runtime.id,
48
58
  id: job.id,
49
59
  })
50
- if (existing?.pipelineId === pipeline.id) {
60
+ if (
61
+ existing?.pipelineId === durableRun.pipelineId &&
62
+ existing.executionId === durableRun.executionId
63
+ ) {
51
64
  throw new PipelineRunAlreadyStartedError(existing)
52
65
  }
53
66
  throw error
54
67
  }
55
68
  await input.onRunStarted?.(startedRun)
56
69
 
70
+ const pipeline = requirePipeline(runtime.pipelines.getById(job.pipelineId), job)
71
+
57
72
  for (const [stepIndex, node] of pipeline.graph.nodes.entries()) {
58
73
  const stepResult = await runStep({
59
74
  runtime,
@@ -97,34 +112,62 @@ export async function runPipelineJob(input: RunPipelineJobInput): Promise<Pipeli
97
112
  }
98
113
  throw error
99
114
  }
100
- finished = true
115
+ const finishedRun = {
116
+ ...run,
117
+ finishedAt: requireFinishedAt({
118
+ pipelineId: pipeline.id,
119
+ runId: job.id,
120
+ finishedAt: run.finishedAt,
121
+ }),
122
+ }
101
123
 
124
+ finished = true
125
+ await notifyRunFinished(input.onRunFinished, finishedRun)
102
126
  return {
103
- run: {
104
- ...run,
105
- finishedAt: requireFinishedAt(job.id, run.finishedAt),
106
- },
127
+ run: finishedRun,
107
128
  steps,
108
129
  version: finalVersion,
109
130
  }
110
131
  } catch (error) {
132
+ const reportedError = unwrapPipelineStepFailure(error)
111
133
  if (startedRun && !finished) {
112
134
  const status = statusForFailure(signal, error)
113
135
  try {
136
+ const failure = captureSixbFailure(error, {
137
+ allowedCodes: PIPELINE_RUN_FAILURE_CODES,
138
+ defaultCode: status === "cancelled" ? "runtime.cancelled" : "internal.unexpected",
139
+ details: { pipelineId: durableRun.pipelineId, runId: job.id },
140
+ })
114
141
  const run = await runtime.pipelineRunsStorage.finish({
115
142
  projectId: runtime.id,
116
143
  id: job.id,
117
144
  status,
118
- error: toPipelineRunFailure(error),
145
+ error: failure,
119
146
  })
120
- if (status === "failed" && run.status === "failed") input.onRunFailed?.(error, run)
147
+ if (status === "failed" && run.status === "failed") {
148
+ input.onRunFailed?.(reportedError, run, failure)
149
+ }
150
+ await notifyRunFinished(input.onRunFinished, run)
121
151
  } catch {
122
152
  // The run did not transition to the requested terminal status.
123
153
  }
124
154
  }
125
155
 
126
- throw error
156
+ throw reportedError
127
157
  } finally {
128
158
  await logSession.flush()
129
159
  }
130
160
  }
161
+
162
+ async function notifyRunFinished(
163
+ handler: PipelineRunFinishedHandler | undefined,
164
+ run: PipelineRunRecord
165
+ ): Promise<void> {
166
+ try {
167
+ await handler?.(run)
168
+ } catch (error) {
169
+ // The built-in event log reports lost batches itself. Anything reaching here is a broken
170
+ // invariant in a custom lifecycle handler and must not change an already durable outcome.
171
+ console.error("[SixbPipelineWorker] Pipeline run lifecycle handler failed:", error)
172
+ }
173
+ }