@shipfox/api-workflows 12.7.0 → 13.1.0

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 (122) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +24 -0
  3. package/dist/core/errors.d.ts +9 -1
  4. package/dist/core/errors.d.ts.map +1 -1
  5. package/dist/core/errors.js +5 -1
  6. package/dist/core/errors.js.map +1 -1
  7. package/dist/core/job-execution.d.ts.map +1 -1
  8. package/dist/core/job-execution.js +12 -2
  9. package/dist/core/job-execution.js.map +1 -1
  10. package/dist/core/step-config/agent.d.ts.map +1 -1
  11. package/dist/core/step-config/agent.js +9 -1
  12. package/dist/core/step-config/agent.js.map +1 -1
  13. package/dist/db/index.d.ts +1 -1
  14. package/dist/db/index.d.ts.map +1 -1
  15. package/dist/db/index.js +1 -1
  16. package/dist/db/index.js.map +1 -1
  17. package/dist/db/job-listeners.d.ts.map +1 -1
  18. package/dist/db/job-listeners.js +17 -1
  19. package/dist/db/job-listeners.js.map +1 -1
  20. package/dist/db/workflow-runs/job-executions.d.ts +2 -3
  21. package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
  22. package/dist/db/workflow-runs/job-executions.js +48 -20
  23. package/dist/db/workflow-runs/job-executions.js.map +1 -1
  24. package/dist/db/workflow-runs/outbox.d.ts +15 -0
  25. package/dist/db/workflow-runs/outbox.d.ts.map +1 -1
  26. package/dist/db/workflow-runs/outbox.js +35 -1
  27. package/dist/db/workflow-runs/outbox.js.map +1 -1
  28. package/dist/db/workflow-runs/run-graph.d.ts.map +1 -1
  29. package/dist/db/workflow-runs/run-graph.js +12 -0
  30. package/dist/db/workflow-runs/run-graph.js.map +1 -1
  31. package/dist/db/workflow-runs/run-status.d.ts.map +1 -1
  32. package/dist/db/workflow-runs/run-status.js +13 -1
  33. package/dist/db/workflow-runs/run-status.js.map +1 -1
  34. package/dist/db/workflow-runs.d.ts +2 -2
  35. package/dist/db/workflow-runs.d.ts.map +1 -1
  36. package/dist/db/workflow-runs.js +2 -2
  37. package/dist/db/workflow-runs.js.map +1 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/presentation/dto/step.d.ts.map +1 -1
  42. package/dist/presentation/dto/step.js +14 -0
  43. package/dist/presentation/dto/step.js.map +1 -1
  44. package/dist/presentation/index.d.ts +1 -1
  45. package/dist/presentation/index.d.ts.map +1 -1
  46. package/dist/presentation/index.js +1 -1
  47. package/dist/presentation/index.js.map +1 -1
  48. package/dist/presentation/routes/agent-runtime-config.d.ts.map +1 -1
  49. package/dist/presentation/routes/agent-runtime-config.js +22 -0
  50. package/dist/presentation/routes/agent-runtime-config.js.map +1 -1
  51. package/dist/presentation/subscribers/index.d.ts +0 -1
  52. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  53. package/dist/presentation/subscribers/index.js +0 -1
  54. package/dist/presentation/subscribers/index.js.map +1 -1
  55. package/dist/temporal/activities/index.d.ts +2 -18
  56. package/dist/temporal/activities/index.d.ts.map +1 -1
  57. package/dist/temporal/activities/index.js +3 -5
  58. package/dist/temporal/activities/index.js.map +1 -1
  59. package/dist/temporal/activities/orchestration-activities.d.ts +5 -14
  60. package/dist/temporal/activities/orchestration-activities.d.ts.map +1 -1
  61. package/dist/temporal/activities/orchestration-activities.js +13 -34
  62. package/dist/temporal/activities/orchestration-activities.js.map +1 -1
  63. package/dist/temporal/workflows/index.bundle.js +20 -68
  64. package/dist/temporal/workflows/job-execution-orchestration.d.ts +0 -3
  65. package/dist/temporal/workflows/job-execution-orchestration.d.ts.map +1 -1
  66. package/dist/temporal/workflows/job-execution-orchestration.js +17 -41
  67. package/dist/temporal/workflows/job-execution-orchestration.js.map +1 -1
  68. package/dist/temporal/workflows/job-listener-orchestration.d.ts +0 -3
  69. package/dist/temporal/workflows/job-listener-orchestration.d.ts.map +1 -1
  70. package/dist/temporal/workflows/job-listener-orchestration.js +1 -9
  71. package/dist/temporal/workflows/job-listener-orchestration.js.map +1 -1
  72. package/dist/temporal/workflows/run-orchestration.d.ts.map +1 -1
  73. package/dist/temporal/workflows/run-orchestration.js +2 -18
  74. package/dist/temporal/workflows/run-orchestration.js.map +1 -1
  75. package/dist/temporal/workflows/test-env.d.ts +4 -6
  76. package/dist/temporal/workflows/test-env.d.ts.map +1 -1
  77. package/dist/temporal/workflows/test-env.js +13 -24
  78. package/dist/temporal/workflows/test-env.js.map +1 -1
  79. package/dist/tsconfig.test.tsbuildinfo +1 -1
  80. package/package.json +4 -4
  81. package/src/core/errors.ts +17 -2
  82. package/src/core/job-execution.test.ts +54 -0
  83. package/src/core/job-execution.ts +6 -1
  84. package/src/core/step-config/agent.ts +8 -1
  85. package/src/core/step-config/complete-step-dispatch-config.test.ts +37 -0
  86. package/src/db/index.ts +1 -1
  87. package/src/db/job-listeners.test.ts +21 -0
  88. package/src/db/job-listeners.ts +18 -0
  89. package/src/db/workflow-runs/job-executions.test.ts +110 -12
  90. package/src/db/workflow-runs/job-executions.ts +54 -29
  91. package/src/db/workflow-runs/outbox.ts +63 -0
  92. package/src/db/workflow-runs/run-graph.ts +17 -0
  93. package/src/db/workflow-runs/run-status.test.ts +13 -0
  94. package/src/db/workflow-runs/run-status.ts +15 -1
  95. package/src/db/workflow-runs.ts +3 -1
  96. package/src/index.ts +1 -5
  97. package/src/presentation/dto/step.test.ts +8 -0
  98. package/src/presentation/dto/step.ts +9 -0
  99. package/src/presentation/index.ts +0 -1
  100. package/src/presentation/routes/agent-runtime-config.test.ts +84 -2
  101. package/src/presentation/routes/agent-runtime-config.ts +24 -0
  102. package/src/presentation/subscribers/index.ts +0 -1
  103. package/src/temporal/activities/index.ts +2 -8
  104. package/src/temporal/activities/orchestration-activities.test.ts +17 -1
  105. package/src/temporal/activities/orchestration-activities.ts +13 -46
  106. package/src/temporal/workflows/job-execution-orchestration.test.ts +6 -25
  107. package/src/temporal/workflows/job-execution-orchestration.ts +17 -43
  108. package/src/temporal/workflows/job-listener-orchestration-continuation.test.ts +0 -3
  109. package/src/temporal/workflows/job-listener-orchestration.test.ts +9 -12
  110. package/src/temporal/workflows/job-listener-orchestration.ts +0 -8
  111. package/src/temporal/workflows/run-orchestration.test.ts +22 -33
  112. package/src/temporal/workflows/run-orchestration.ts +4 -28
  113. package/src/temporal/workflows/test-env.ts +14 -34
  114. package/test/fixtures/runners-inter-module.ts +0 -3
  115. package/test/helpers/workflow-runs.ts +25 -0
  116. package/tsconfig.build.tsbuildinfo +1 -1
  117. package/dist/presentation/subscribers/on-runner-job-queued.d.ts +0 -3
  118. package/dist/presentation/subscribers/on-runner-job-queued.d.ts.map +0 -1
  119. package/dist/presentation/subscribers/on-runner-job-queued.js +0 -20
  120. package/dist/presentation/subscribers/on-runner-job-queued.js.map +0 -1
  121. package/src/presentation/subscribers/on-runner-job-queued.test.ts +0 -57
  122. package/src/presentation/subscribers/on-runner-job-queued.ts +0 -23
@@ -26,14 +26,14 @@ import {remainingMs} from './deadline.js';
26
26
  * execution pending while it is queued, then performs the versioned pending → running
27
27
  * transition after the current claim. One deadline spans both waits; claim never resets
28
28
  * it. Signals can arrive in any order, so the precedence is finished > lease expired >
29
- * claimed > timeout. Lease cleanup remains best-effort: a runners DB outage must never
30
- * block the child workflow from returning the job outcome to run-orchestration.
29
+ * claimed > timeout. Workflows commits queue and terminal facts with its state transitions;
30
+ * runners reconciles its local queue, lease, and reservation projections asynchronously.
31
31
  */
32
32
 
33
33
  const {
34
34
  setJobStatus,
35
35
  setJobExecutionStatus,
36
- enqueueJobExecutionForRunner,
36
+ queueJobExecutionActivity,
37
37
  bulkSetStepStatuses,
38
38
  failJobExecutionAsTimedOutActivity,
39
39
  resolveLeaseExpiredJobExecutionActivity,
@@ -48,14 +48,6 @@ const {resolveJobStatusFromJobExecutionsActivity} = proxyActivities<
48
48
  retry: {maximumAttempts: 5},
49
49
  });
50
50
 
51
- // Lease cleanup gets a bounded retry policy of its own: after a few attempts the
52
- // workflow stops waiting on it (see releaseLeaseBestEffort) rather than blocking
53
- // the DAG on runners-side availability.
54
- const {releaseLeaseActivity} = proxyActivities<ReturnType<typeof createOrchestrationActivities>>({
55
- startToCloseTimeout: '30s',
56
- retry: {maximumAttempts: 5},
57
- });
58
-
59
51
  const DEFAULT_EXECUTION_MAX_DURATION_MS = 6 * 60 * 60 * 1000;
60
52
 
61
53
  export const jobFinishedSignal =
@@ -71,12 +63,9 @@ export interface JobClaimedSignalPayload {
71
63
  export const jobClaimedSignal = defineSignal<[JobClaimedSignalPayload]>(JOB_CLAIMED_SIGNAL);
72
64
 
73
65
  export interface JobExecutionOrchestrationInput {
74
- workspaceId: string;
75
- workflowRunId: string;
76
66
  runAttemptId: string;
77
67
  jobId: string;
78
68
  jobExecutionId: string;
79
- projectId: string;
80
69
  jobVersion: number;
81
70
  executionVersion: number;
82
71
  executionTimeoutMs?: number | null | undefined;
@@ -89,17 +78,6 @@ export interface JobExecutionOrchestrationResult {
89
78
  jobVersion: number;
90
79
  }
91
80
 
92
- async function releaseLeaseBestEffort(jobExecutionId: string): Promise<void> {
93
- try {
94
- await releaseLeaseActivity({jobExecutionId});
95
- } catch (err) {
96
- log.warn('lease release failed; stuck detector will reap the row', {
97
- jobExecutionId,
98
- error: String(err),
99
- });
100
- }
101
- }
102
-
103
81
  async function resolveJobStatusOrFailClosed(
104
82
  input: JobExecutionOrchestrationInput,
105
83
  ): Promise<{status: RuntimeCompletionStatus; jobVersion: number}> {
@@ -121,7 +99,7 @@ async function resolveJobStatusOrFailClosed(
121
99
  }
122
100
  }
123
101
 
124
- async function enqueueJobExecution(input: JobExecutionOrchestrationInput): Promise<void> {
102
+ async function queueJobExecution(input: JobExecutionOrchestrationInput) {
125
103
  if (hasNoRequiredRunnerLabels(input.requiredLabels)) {
126
104
  throw ApplicationFailure.nonRetryable(
127
105
  `Job ${input.jobId} has no required runner labels`,
@@ -129,15 +107,12 @@ async function enqueueJobExecution(input: JobExecutionOrchestrationInput): Promi
129
107
  );
130
108
  }
131
109
 
132
- await enqueueJobExecutionForRunner({
133
- workspaceId: input.workspaceId,
134
- workflowRunId: input.workflowRunId,
135
- runAttemptId: input.runAttemptId,
136
- jobId: input.jobId,
137
- jobExecutionId: input.jobExecutionId,
138
- projectId: input.projectId,
139
- requiredLabels: input.requiredLabels,
140
- });
110
+ return jobExecutionStartOutcome(
111
+ await queueJobExecutionActivity({
112
+ jobId: input.jobId,
113
+ jobExecutionId: input.jobExecutionId,
114
+ }),
115
+ );
141
116
  }
142
117
 
143
118
  async function markJobExecutionRunning(
@@ -218,7 +193,6 @@ async function resolveFinishedJobExecution({
218
193
  terminationReason: 'finished',
219
194
  status,
220
195
  });
221
- await releaseLeaseBestEffort(input.jobExecutionId);
222
196
  return {status, jobVersion: input.jobVersion};
223
197
  }
224
198
  const resolved = await resolveJobStatusOrFailClosed(input);
@@ -228,7 +202,6 @@ async function resolveFinishedJobExecution({
228
202
  terminationReason: 'finished',
229
203
  status: resolved.status,
230
204
  });
231
- await releaseLeaseBestEffort(input.jobExecutionId);
232
205
  return resolved;
233
206
  }
234
207
 
@@ -257,7 +230,6 @@ async function resolveLeaseExpiredJobExecution({
257
230
  terminationReason: 'lease_expired',
258
231
  status: leaseExpired.status,
259
232
  });
260
- await releaseLeaseBestEffort(input.jobExecutionId);
261
233
  return {status: leaseExpired.status, jobVersion: input.jobVersion};
262
234
  }
263
235
  const {status, jobVersion} = await resolveJobStatusOrFailClosed(input);
@@ -267,13 +239,12 @@ async function resolveLeaseExpiredJobExecution({
267
239
  terminationReason: 'lease_expired',
268
240
  status,
269
241
  });
270
- await releaseLeaseBestEffort(input.jobExecutionId);
271
242
  return {status, jobVersion};
272
243
  }
273
244
 
274
- // Timeout backstop. The activity atomically fails the execution, marks
275
- // `timed_out_at`, and enqueues WORKFLOWS_JOB_EXECUTION_TIMED_OUT; the runners subscriber
276
- // then asks the runner to cancel. The lease is intentionally NOT released here.
245
+ // Timeout backstop. The activity atomically fails the execution, marks `timed_out_at`, and
246
+ // enqueues the generic terminal execution fact. The runners subscriber then asks the runner to
247
+ // cancel. The lease is intentionally NOT released here.
277
248
  async function resolveTimedOutJobExecution({
278
249
  input,
279
250
  runningVersion,
@@ -314,7 +285,10 @@ export async function jobExecutionOrchestration(
314
285
  // Register every signal before enqueue can block or publish a claim/outcome event. The
315
286
  // handlers retain signals that arrive while the enqueue activity is in flight.
316
287
  registerJobExecutionSignalHandlers(input.jobExecutionId, signals);
317
- await enqueueJobExecution(input);
288
+ const queued = await queueJobExecution(input);
289
+ if (queued.kind === 'terminal') {
290
+ return {status: queued.result.status, jobVersion: input.jobVersion};
291
+ }
318
292
 
319
293
  const timeoutMs = input.executionTimeoutMs ?? DEFAULT_EXECUTION_MAX_DURATION_MS;
320
294
  const deadline = Date.now() + timeoutMs;
@@ -63,9 +63,6 @@ describe('listener continue-as-new guard', () => {
63
63
 
64
64
  it('carries listener loop state into the continued run', () => {
65
65
  const input: JobListenerOrchestrationInput = {
66
- workspaceId: 'workspace-1',
67
- workflowRunId: 'run-1',
68
- projectId: 'project-1',
69
66
  runAttemptId: 'attempt-1',
70
67
  jobId: 'job-1',
71
68
  jobVersion: 3,
@@ -197,7 +197,7 @@ describe('jobListenerOrchestration', () => {
197
197
 
198
198
  expect(listenerFiringOutcomeCalls()).toHaveLength(0);
199
199
  expect(resolveJobListenerCalls().map((c) => c.params.reason)).toEqual(['until']);
200
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
200
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(0);
201
201
  });
202
202
 
203
203
  test('resolves with timeout when the listening deadline elapses', async () => {
@@ -233,9 +233,10 @@ describe('jobListenerOrchestration', () => {
233
233
 
234
234
  await runListener({maxExecutions: 1});
235
235
 
236
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
237
- expect(callsNamed('enqueueJobExecutionForRunner')[0]?.params).toMatchObject({
238
- requiredLabels: ['gpu', 'linux'],
236
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
237
+ expect(callsNamed('queueJobExecutionActivity')[0]?.params).toMatchObject({
238
+ jobId,
239
+ jobExecutionId: 'exec-1',
239
240
  });
240
241
  expect(listenerFiringOutcomeCalls().map((c) => c.params.outcome)).toEqual(['succeeded']);
241
242
  });
@@ -250,7 +251,7 @@ describe('jobListenerOrchestration', () => {
250
251
  await runListener({maxExecutions: 1});
251
252
 
252
253
  // The child actually ran (enqueued), then returned failed.
253
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
254
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
254
255
  expect(listenerFiringOutcomeCalls().map((c) => c.params.outcome)).toEqual(['failed']);
255
256
  expect(resolveJobListenerCalls().map((c) => c.params.reason)).toEqual(['max_executions']);
256
257
  });
@@ -265,7 +266,7 @@ describe('jobListenerOrchestration', () => {
265
266
  await runListener({maxExecutions: 2});
266
267
 
267
268
  // Sequence 1 failed at materialization (no child enqueued); sequence 2 ran and succeeded.
268
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
269
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
269
270
  expect(listenerFiringOutcomeCalls().map((c) => c.params.outcome)).toEqual([
270
271
  'failed',
271
272
  'succeeded',
@@ -369,15 +370,12 @@ describe('jobListenerOrchestration', () => {
369
370
  });
370
371
 
371
372
  const handle = await startListener({onResolve: 'cancel'});
372
- await waitForActivity('enqueueJobExecutionForRunner');
373
+ await waitForActivity('queueJobExecutionActivity');
373
374
  await handle.signal(LISTENER_RESOLVE_SIGNAL);
374
375
  await handle.result();
375
376
 
376
377
  expect(settleListenerCalls().map((c) => c.params.status)).toEqual(['cancelled']);
377
378
  expect(listenerFiringOutcomeCalls().map((c) => c.params.outcome)).toEqual(['cancelled']);
378
- expect(callsNamed('cancelRunnerJobsActivity')).toEqual([
379
- {name: 'cancelRunnerJobsActivity', params: {jobIds: [jobId]}},
380
- ]);
381
379
  expect(resolveJobListenerCalls().map((c) => c.params.reason)).toEqual(['until']);
382
380
  });
383
381
 
@@ -390,7 +388,7 @@ describe('jobListenerOrchestration', () => {
390
388
  });
391
389
 
392
390
  const handle = await startListener({onResolve: 'finish'});
393
- await waitForActivity('enqueueJobExecutionForRunner');
391
+ await waitForActivity('queueJobExecutionActivity');
394
392
  // Resolution arrives mid-firing; finish mode must wait for the child, not cancel it.
395
393
  await handle.signal(LISTENER_RESOLVE_SIGNAL);
396
394
  const child = testEnv.client.workflow.getHandle(`job:${jobId}`);
@@ -398,7 +396,6 @@ describe('jobListenerOrchestration', () => {
398
396
  await handle.result();
399
397
 
400
398
  expect(settleListenerCalls()).toHaveLength(0);
401
- expect(callsNamed('cancelRunnerJobsActivity')).toHaveLength(0);
402
399
  expect(listenerFiringOutcomeCalls().map((c) => c.params.outcome)).toEqual(['succeeded']);
403
400
  expect(resolveJobListenerCalls().map((c) => c.params.reason)).toEqual(['until']);
404
401
  });
@@ -25,7 +25,6 @@ const {
25
25
  resolveJobListenerActivity,
26
26
  settleListenerJobExecutionActivity,
27
27
  recordListenerFiringOutcomeActivity,
28
- cancelRunnerJobsActivity,
29
28
  } = proxyActivities<ReturnType<typeof createOrchestrationActivities>>({
30
29
  startToCloseTimeout: '30s',
31
30
  });
@@ -34,9 +33,6 @@ export const listenerEventsAvailableSignal = defineSignal<[]>(LISTENER_EVENTS_AV
34
33
  export const listenerResolveSignal = defineSignal<[]>(LISTENER_RESOLVE_SIGNAL);
35
34
 
36
35
  export interface JobListenerOrchestrationInput {
37
- workspaceId: string;
38
- workflowRunId: string;
39
- projectId: string;
40
36
  runAttemptId: string;
41
37
  jobId: string;
42
38
  jobVersion: number;
@@ -413,9 +409,6 @@ async function runListenerExecution(params: {
413
409
  workflowIdReusePolicy: 'ALLOW_DUPLICATE',
414
410
  args: [
415
411
  {
416
- workspaceId: params.input.workspaceId,
417
- workflowRunId: params.input.workflowRunId,
418
- projectId: params.input.projectId,
419
412
  jobId: params.input.jobId,
420
413
  jobExecutionId: params.jobExecutionId,
421
414
  runAttemptId: params.input.runAttemptId,
@@ -447,7 +440,6 @@ async function runListenerExecution(params: {
447
440
  status: 'cancelled',
448
441
  });
449
442
  await recordListenerFiringOutcomeActivity({outcome: 'cancelled'});
450
- await cancelRunnerJobsActivity({jobIds: [params.input.jobId]});
451
443
  return;
452
444
  }
453
445
  }
@@ -59,17 +59,13 @@ describe('runOrchestration', () => {
59
59
 
60
60
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
61
61
  expect(runAttemptStatuses).toEqual(['running', 'succeeded']);
62
- const enqueueCalls = callsNamed('enqueueJobExecutionForRunner');
62
+ const enqueueCalls = callsNamed('queueJobExecutionActivity');
63
63
  expect(enqueueCalls).toHaveLength(3);
64
- // The lease tuple is sourced from the loaded dag (workspace/project/run together).
65
- for (const call of enqueueCalls) {
66
- expect(call.params).toMatchObject({
67
- workspaceId: 'workspace-1',
68
- projectId: 'project-1',
69
- runAttemptId: 'r1-attempt-1',
70
- requiredLabels: ['ubuntu22'],
71
- });
72
- }
64
+ expect(enqueueCalls.map((call) => call.params)).toEqual([
65
+ {jobId: 'j1', jobExecutionId: 'j1'},
66
+ {jobId: 'j2', jobExecutionId: 'j2'},
67
+ {jobId: 'j3', jobExecutionId: 'j3'},
68
+ ]);
73
69
  });
74
70
 
75
71
  test('already-succeeded upstream jobs are not re-enqueued', async () => {
@@ -83,7 +79,7 @@ describe('runOrchestration', () => {
83
79
 
84
80
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
85
81
  expect(runAttemptStatuses).toEqual(['running', 'succeeded']);
86
- const enqueueCalls = callsNamed('enqueueJobExecutionForRunner');
82
+ const enqueueCalls = callsNamed('queueJobExecutionActivity');
87
83
  expect(enqueueCalls).toHaveLength(1);
88
84
  expect(enqueueCalls[0]?.params).toMatchObject({jobId: 'j2'});
89
85
  });
@@ -99,7 +95,7 @@ describe('runOrchestration', () => {
99
95
 
100
96
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
101
97
  expect(runAttemptStatuses).toEqual(['running', 'failed']);
102
- const enqueueCalls = callsNamed('enqueueJobExecutionForRunner');
98
+ const enqueueCalls = callsNamed('queueJobExecutionActivity');
103
99
  expect(enqueueCalls).toHaveLength(1);
104
100
  expect(enqueueCalls[0]?.params).toMatchObject({jobId: 'j2'});
105
101
  });
@@ -116,7 +112,7 @@ describe('runOrchestration', () => {
116
112
 
117
113
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
118
114
  expect(runAttemptStatuses).toEqual(['running', 'succeeded']);
119
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(3);
115
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(3);
120
116
  });
121
117
 
122
118
  test('single job fails, dependents are skipped', async () => {
@@ -132,7 +128,7 @@ describe('runOrchestration', () => {
132
128
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
133
129
  expect(runAttemptStatuses).toEqual(['running', 'failed']);
134
130
 
135
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
131
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
136
132
  const jobStatuses = setJobStatusCalls().map((c) => ({
137
133
  id: c.params.jobId,
138
134
  status: c.params.status,
@@ -162,7 +158,7 @@ describe('runOrchestration', () => {
162
158
 
163
159
  const runAttemptStatuses = setRunAttemptStatusCalls().map((c) => c.params.status);
164
160
  expect(runAttemptStatuses).toEqual(['running', 'failed']);
165
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
161
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
166
162
 
167
163
  const jobStatuses = setJobStatusCalls().map((c) => ({
168
164
  id: c.params.jobId,
@@ -199,7 +195,7 @@ describe('runOrchestration', () => {
199
195
  },
200
196
  },
201
197
  ]);
202
- expect(callsNamed('enqueueJobExecutionForRunner').map((call) => call.params)).toEqual([
198
+ expect(callsNamed('queueJobExecutionActivity').map((call) => call.params)).toEqual([
203
199
  expect.objectContaining({jobId: 'j1'}),
204
200
  expect.objectContaining({jobId: 'j2'}),
205
201
  ]);
@@ -221,7 +217,7 @@ describe('runOrchestration', () => {
221
217
 
222
218
  await executeRun();
223
219
 
224
- expect(callsNamed('enqueueJobExecutionForRunner').map((call) => call.params)).toEqual([
220
+ expect(callsNamed('queueJobExecutionActivity').map((call) => call.params)).toEqual([
225
221
  expect.objectContaining({jobId: 'j1'}),
226
222
  ]);
227
223
  expect(setRunAttemptStatusCalls().map((c) => c.params.status)).toEqual([
@@ -249,7 +245,7 @@ describe('runOrchestration', () => {
249
245
  },
250
246
  },
251
247
  ]);
252
- const enqueuedJobs = callsNamed('enqueueJobExecutionForRunner').map((call) => call.params);
248
+ const enqueuedJobs = callsNamed('queueJobExecutionActivity').map((call) => call.params);
253
249
  expect(enqueuedJobs).toHaveLength(3);
254
250
  expect(enqueuedJobs).toEqual(
255
251
  expect.arrayContaining([
@@ -273,7 +269,7 @@ describe('runOrchestration', () => {
273
269
 
274
270
  await executeRun();
275
271
 
276
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
272
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(0);
277
273
  expect(setRunAttemptStatusCalls().map((c) => c.params.status)).toEqual([
278
274
  'running',
279
275
  'succeeded',
@@ -310,7 +306,7 @@ describe('runOrchestration', () => {
310
306
  expect(finalRunAttemptStatus?.params.version).toBeGreaterThan(0);
311
307
  });
312
308
 
313
- test('cancel signal stops scheduling and fans out runner cancellation', async () => {
309
+ test('cancel signal stops scheduling without calling runners directly', async () => {
314
310
  const jobs = [dagJob('j1', 'build'), dagJob('j2', 'deploy', ['build'])];
315
311
  setCfg({dag: makeDag(jobs, 'r-cancel'), jobResults: new Map(), skipSignal: true});
316
312
 
@@ -319,16 +315,13 @@ describe('runOrchestration', () => {
319
315
  workflowId: `run:${workflowRunId}`,
320
316
  args: [{workflowRunId, workspaceId}],
321
317
  });
322
- await waitForActivity('enqueueJobExecutionForRunner');
318
+ await waitForActivity('queueJobExecutionActivity');
323
319
 
324
320
  await handle.signal('run-cancel');
325
321
  await handle.result();
326
322
 
327
323
  expect(setRunAttemptStatusCalls().map((c) => c.params.status)).toEqual(['running']);
328
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(1);
329
- expect(callsNamed('cancelRunnerJobsActivity')).toEqual([
330
- {name: 'cancelRunnerJobsActivity', params: {jobIds: ['j1', 'j2']}},
331
- ]);
324
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(1);
332
325
  });
333
326
 
334
327
  test('starts listening jobs and cancels them with the run', async () => {
@@ -347,10 +340,7 @@ describe('runOrchestration', () => {
347
340
  await handle.result();
348
341
 
349
342
  expect(setRunAttemptStatusCalls().map((c) => c.params.status)).toEqual(['running']);
350
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
351
- expect(callsNamed('cancelRunnerJobsActivity')).toEqual([
352
- {name: 'cancelRunnerJobsActivity', params: {jobIds: ['j1']}},
353
- ]);
343
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(0);
354
344
  });
355
345
 
356
346
  test('aborts early when the initial running write reports an already-terminal run', async () => {
@@ -364,8 +354,7 @@ describe('runOrchestration', () => {
364
354
  await executeRun();
365
355
 
366
356
  expect(setRunAttemptStatusCalls().map((c) => c.params.status)).toEqual(['running']);
367
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(0);
368
- expect(callsNamed('cancelRunnerJobsActivity')).toHaveLength(0);
357
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(0);
369
358
  });
370
359
 
371
360
  test('diamond DAG with partial failure', async () => {
@@ -383,7 +372,7 @@ describe('runOrchestration', () => {
383
372
  expect(runAttemptStatuses).toEqual(['running', 'failed']);
384
373
 
385
374
  // A, B, C enqueued: D skipped because B failed
386
- expect(callsNamed('enqueueJobExecutionForRunner')).toHaveLength(3);
375
+ expect(callsNamed('queueJobExecutionActivity')).toHaveLength(3);
387
376
  const jobStatuses = setJobStatusCalls().map((c) => ({
388
377
  id: c.params.jobId,
389
378
  status: c.params.status,
@@ -401,7 +390,7 @@ describe('runOrchestration', () => {
401
390
  setCfg({
402
391
  dag: makeDag(jobs, 'r7'),
403
392
  jobResults: new Map(),
404
- enqueueError: 'Runner service unavailable',
393
+ queueError: 'Runner service unavailable',
405
394
  });
406
395
 
407
396
  await expect(executeRun()).rejects.toThrow();
@@ -9,7 +9,6 @@ import {
9
9
  } from '@temporalio/workflow';
10
10
  import {
11
11
  createRuntimeRunProgress,
12
- nonCompletedRuntimeJobIds,
13
12
  type RuntimeRunProgress,
14
13
  recordRuntimeJobResult,
15
14
  recordSkippedRuntimeJob,
@@ -26,15 +25,10 @@ import {deadlineReached, remainingMs} from './deadline.js';
26
25
  import {jobExecutionOrchestration} from './job-execution-orchestration.js';
27
26
  import {jobListenerOrchestration} from './job-listener-orchestration.js';
28
27
 
29
- const {
30
- loadRunAttemptDag,
31
- evaluateJobActivationsActivity,
32
- setRunAttemptStatus,
33
- setJobStatus,
34
- cancelRunnerJobsActivity,
35
- } = proxyActivities<ReturnType<typeof createOrchestrationActivities>>({
36
- startToCloseTimeout: '30s',
37
- });
28
+ const {loadRunAttemptDag, evaluateJobActivationsActivity, setRunAttemptStatus, setJobStatus} =
29
+ proxyActivities<ReturnType<typeof createOrchestrationActivities>>({
30
+ startToCloseTimeout: '30s',
31
+ });
38
32
 
39
33
  const {failRunAsTimedOutActivity} = proxyActivities<
40
34
  ReturnType<typeof createOrchestrationActivities>
@@ -74,12 +68,10 @@ export async function runOrchestration(input: RunOrchestrationInput): Promise<vo
74
68
 
75
69
  while (true) {
76
70
  if (cancelRequested) {
77
- await cancelNonCompletedRunnerJobs(dag, progress);
78
71
  return;
79
72
  }
80
73
  if (deadlineReached(runDeadline)) {
81
74
  await failRunAsTimedOutActivity({runAttemptId: input.runAttemptId});
82
- await cancelNonCompletedRunnerJobs(dag, progress);
83
75
  return;
84
76
  }
85
77
 
@@ -144,12 +136,10 @@ export async function runOrchestration(input: RunOrchestrationInput): Promise<vo
144
136
 
145
137
  const settled = await waitForNextSettlement(inFlight, () => cancelRequested, runDeadline);
146
138
  if (settled.kind === 'cancelled') {
147
- await cancelNonCompletedRunnerJobs(dag, progress);
148
139
  return;
149
140
  }
150
141
  if (settled.kind === 'timed-out') {
151
142
  await failRunAsTimedOutActivity({runAttemptId: input.runAttemptId});
152
- await cancelNonCompletedRunnerJobs(dag, progress);
153
143
  return;
154
144
  }
155
145
  inFlight.delete(settled.job.key);
@@ -187,9 +177,6 @@ function launchJob(
187
177
  workflowId: `job-listener:${job.id}`,
188
178
  args: [
189
179
  {
190
- workspaceId: run.workspaceId,
191
- workflowRunId: run.workflowRunId,
192
- projectId: run.projectId,
193
180
  jobId: job.id,
194
181
  runAttemptId: run.runAttemptId,
195
182
  jobVersion: runtimeJobVersion(job, progress),
@@ -235,9 +222,6 @@ function launchJob(
235
222
  workflowId: `job:${job.id}`,
236
223
  args: [
237
224
  {
238
- workspaceId: run.workspaceId,
239
- workflowRunId: run.workflowRunId,
240
- projectId: run.projectId,
241
225
  jobId: job.id,
242
226
  jobExecutionId: job.jobExecutionId,
243
227
  runAttemptId: run.runAttemptId,
@@ -273,11 +257,3 @@ async function waitForNextSettlement(
273
257
  );
274
258
  return await Promise.race([childSettled, cancel]);
275
259
  }
276
-
277
- async function cancelNonCompletedRunnerJobs(
278
- dag: RunDag,
279
- progress: RuntimeRunProgress,
280
- ): Promise<void> {
281
- const jobIds = nonCompletedRuntimeJobIds(dag.jobs, progress);
282
- await cancelRunnerJobsActivity({jobIds});
283
- }
@@ -25,11 +25,11 @@ export interface TestConfig {
25
25
  dag: RunDag;
26
26
  /** Map of jobId → status the mock runner should signal back */
27
27
  jobResults: Map<string, RuntimeCompletionStatus>;
28
- /** If set, enqueueJobExecutionForRunner will throw with this message instead of signaling */
29
- enqueueError?: string;
30
- /** If true, enqueueJobExecutionForRunner sends two job-finished signals (for dedup testing) */
28
+ /** If set, queueJobExecutionActivity will throw with this message instead of signaling */
29
+ queueError?: string;
30
+ /** If true, queueJobExecutionActivity sends two job-finished signals (for dedup testing) */
31
31
  duplicateSignal?: boolean;
32
- /** If true, enqueueJobExecutionForRunner does nothing without a signal for timeout testing */
32
+ /** If true, queueJobExecutionActivity does nothing without a signal for timeout testing */
33
33
  skipSignal?: boolean;
34
34
  /** If true, emit the claim but hold back the terminal outcome (for deadline testing) */
35
35
  skipOutcomeSignal?: boolean;
@@ -43,8 +43,6 @@ export interface TestConfig {
43
43
  leaseExpiredStatus?: RuntimeCompletionStatus;
44
44
  /** If set, resolveJobStatusFromJobExecutionsActivity throws with this message */
45
45
  resolveJobStatusError?: string;
46
- /** If set, releaseLeaseActivity throws (non-retryable) to prove the workflow still returns */
47
- releaseLeaseError?: string;
48
46
  /** Scripted job activation decisions keyed by job id; defaults to start */
49
47
  activationDecisions?: Map<string, JobActivationDecision>;
50
48
  /** If set, failJobExecutionAsTimedOutActivity throws (for timeout error-path testing) */
@@ -289,30 +287,19 @@ function createMockActivities() {
289
287
  calls.push({name: 'bulkSetStepStatuses', params});
290
288
  },
291
289
 
292
- cancelRunnerJobsActivity: (params: {jobIds: string[]}) => {
293
- calls.push({name: 'cancelRunnerJobsActivity', params});
294
- },
295
-
296
290
  // Scheduling is step-less. The mock runner reports the job outcome by signalling
297
291
  // the per-step terminal-completion signal (job-finished) and/or the lease-expiry
298
292
  // signal, reproducing the outbox → subscriber → signal rail.
299
- enqueueJobExecutionForRunner: async (params: {
300
- workspaceId: string;
301
- workflowRunId: string;
302
- runAttemptId: string;
303
- jobId: string;
304
- jobExecutionId: string;
305
- projectId: string;
306
- requiredLabels: string[];
307
- }) => {
308
- calls.push({name: 'enqueueJobExecutionForRunner', params});
293
+ queueJobExecutionActivity: async (params: {jobId: string; jobExecutionId: string}) => {
294
+ calls.push({name: 'queueJobExecutionActivity', params});
309
295
 
310
- if (cfg.enqueueError) {
296
+ if (cfg.queueError) {
311
297
  const {ApplicationFailure} = await import('@temporalio/common');
312
- throw ApplicationFailure.nonRetryable(cfg.enqueueError);
298
+ throw ApplicationFailure.nonRetryable(cfg.queueError);
313
299
  }
314
300
 
315
- if (cfg.skipSignal) return;
301
+ const queued = {newVersion: nextVersion(), status: 'pending' as const};
302
+ if (cfg.skipSignal) return queued;
316
303
 
317
304
  const status = cfg.jobResults.get(params.jobId) ?? 'succeeded';
318
305
  const handle = testEnv.client.workflow.getHandle(`job:${params.jobId}`);
@@ -321,17 +308,17 @@ function createMockActivities() {
321
308
  claimedAt: cfg.claimedAt ?? '2026-06-22T10:05:00.000Z',
322
309
  });
323
310
 
324
- if (cfg.skipOutcomeSignal) return;
311
+ if (cfg.skipOutcomeSignal) return queued;
325
312
 
326
313
  if (cfg.signalLeaseExpired) {
327
314
  await handle.signal(JOB_LEASE_EXPIRED_SIGNAL, {jobExecutionId: params.jobExecutionId});
328
- return;
315
+ return queued;
329
316
  }
330
317
 
331
318
  if (cfg.signalBoth) {
332
319
  await handle.signal(JOB_FINISHED_SIGNAL, {status, jobExecutionId: params.jobExecutionId});
333
320
  await handle.signal(JOB_LEASE_EXPIRED_SIGNAL, {jobExecutionId: params.jobExecutionId});
334
- return;
321
+ return queued;
335
322
  }
336
323
 
337
324
  // The terminal outbox is independent from the claim, but the mock delivers it from the
@@ -342,6 +329,7 @@ function createMockActivities() {
342
329
  status,
343
330
  duplicateSignal: cfg.duplicateSignal ?? false,
344
331
  });
332
+ return queued;
345
333
  },
346
334
 
347
335
  resolveLeaseExpiredJobExecutionActivity: (params: {
@@ -369,14 +357,6 @@ function createMockActivities() {
369
357
  };
370
358
  },
371
359
 
372
- releaseLeaseActivity: async (params: {jobExecutionId: string}) => {
373
- calls.push({name: 'releaseLeaseActivity', params});
374
- if (cfg.releaseLeaseError) {
375
- const {ApplicationFailure} = await import('@temporalio/common');
376
- throw ApplicationFailure.nonRetryable(cfg.releaseLeaseError);
377
- }
378
- },
379
-
380
360
  failJobExecutionAsTimedOutActivity: async (params: {
381
361
  jobExecutionId: string;
382
362
  runAttemptId: string;
@@ -35,9 +35,6 @@ export function resetRunnersTestClient(): void {
35
35
  }
36
36
 
37
37
  export const runnersTestClient: RunnersInterModuleClient = {
38
- enqueueJobExecution: async () => ({}),
39
- releaseJobExecution: async () => ({}),
40
- cancelJobs: async () => ({}),
41
38
  getLeaseState: async (params) => ({active: activeLeases.has(leaseKey(params))}),
42
39
  getEffectiveRunnerToolCapabilities: async ({runnerSessionId}) =>
43
40
  toolCapabilities.get(runnerSessionId) ?? {capabilities: {harnesses: {}}, reportFresh: false},
@@ -1,4 +1,5 @@
1
1
  import {
2
+ WORKFLOWS_JOB_EXECUTION_TERMINATED,
2
3
  WORKFLOWS_JOB_TERMINATED,
3
4
  WORKFLOWS_STEP_ATTEMPT_TERMINATED,
4
5
  WORKFLOWS_WORKFLOW_RUN_ATTEMPT_CREATED,
@@ -137,6 +138,30 @@ export async function jobTerminatedEvents(jobId: string) {
137
138
  );
138
139
  }
139
140
 
141
+ export async function jobExecutionTerminatedEvents(jobExecutionId: string) {
142
+ const rows = await db()
143
+ .select({payload: workflowsOutbox.payload})
144
+ .from(workflowsOutbox)
145
+ .where(
146
+ and(
147
+ eq(workflowsOutbox.eventType, WORKFLOWS_JOB_EXECUTION_TERMINATED),
148
+ sql`${workflowsOutbox.payload}->>'jobExecutionId' = ${jobExecutionId}`,
149
+ ),
150
+ );
151
+ return rows.map(
152
+ (row) =>
153
+ row.payload as {
154
+ jobId: string;
155
+ jobExecutionId: string;
156
+ workflowRunId: string;
157
+ workflowRunAttemptId: string;
158
+ status: string;
159
+ statusReason: string | null;
160
+ statusReasonMessage?: string | null;
161
+ },
162
+ );
163
+ }
164
+
140
165
  export async function runTerminatedEvents(workflowRunId: string) {
141
166
  const rows = await db()
142
167
  .select({payload: workflowsOutbox.payload})