@outputai/core 0.13.1-next.38c895c.0 → 0.13.1-next.41b143a.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.13.1-next.38c895c.0",
3
+ "version": "0.13.1-next.41b143a.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -40,10 +40,13 @@ export class ActivityExecutionInterceptor {
40
40
 
41
41
  async execute( input, next ) {
42
42
  const activityInfo = activityInfoFn();
43
- const { workflowExecution: { runId }, activityId, activityType, workflowType } = activityInfo;
43
+ const { workflowExecution: { runId }, activityId, attempt, activityType, workflowType } = activityInfo;
44
44
  const { traceInfo, workflowDetails } = headersToObject( input.headers );
45
45
  const outputActivityKind = this.activityKindMap.get( activityType );
46
46
  const workflowFilename = this.workflowsPathMap.get( workflowType );
47
+ // Retries reuse the same activityId, so the attempt is appended to keep each attempt a discrete trace entry.
48
+ // Also prefix with the runId to avoid conflict with child workflows.
49
+ const traceId = `${runId}:${activityId}:${attempt}`;
47
50
 
48
51
  if ( !outputActivityKind ) {
49
52
  throw new Error( `Activity interceptor: activity "${activityType}" was not registered.` );
@@ -58,7 +61,7 @@ export class ActivityExecutionInterceptor {
58
61
 
59
62
  // Adds context accessible information
60
63
  const storageContext = {
61
- parentId: activityId,
64
+ parentId: traceId,
62
65
  outputActivityKind,
63
66
  activityInfo,
64
67
  workflowDetails,
@@ -67,7 +70,7 @@ export class ActivityExecutionInterceptor {
67
70
  };
68
71
 
69
72
  mainEventBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
70
- Tracing.addEventStart( { id: activityId, name: activityType, kind: outputActivityKind, parentId: runId, details: input.args[0], traceInfo } );
73
+ Tracing.addEventStart( { id: traceId, name: activityType, kind: outputActivityKind, parentId: runId, details: input.args[0], traceInfo } );
71
74
 
72
75
  try {
73
76
  // Sends heartbeat to communicate that activity is still alive
@@ -76,20 +79,20 @@ export class ActivityExecutionInterceptor {
76
79
  const output = await Storage.runWithContext( async _ => next( input ), storageContext );
77
80
 
78
81
  mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, workflowDetails, outputActivityKind } );
79
- Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
82
+ Tracing.addEventEnd( { id: traceId, details: output, traceInfo } );
80
83
 
81
84
  return output;
82
85
 
83
86
  } catch ( error ) {
84
87
  // Record async completion handoff as a trace end without emitting a bus event.
85
88
  if ( error instanceof CompleteAsyncError ) {
86
- Tracing.addEventEnd( { id: activityId, details: ActivitySpecialOutput.ASYNC_HANDOFF, traceInfo } );
89
+ Tracing.addEventEnd( { id: traceId, details: ActivitySpecialOutput.ASYNC_HANDOFF, traceInfo } );
87
90
  throw error;
88
91
  }
89
92
 
90
93
  const unwrappedError = error instanceof TransparentFatalError ? error.cause : error;
91
94
 
92
- Tracing.addEventError( { id: activityId, details: unwrappedError, traceInfo } );
95
+ Tracing.addEventError( { id: traceId, details: unwrappedError, traceInfo } );
93
96
  mainEventBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error: unwrappedError } );
94
97
 
95
98
  // Native Temporal errors are just re-thrown
@@ -8,6 +8,7 @@ const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async f
8
8
  const activityInfoMock = vi.hoisted( () => ( {
9
9
  workflowExecution: { workflowId: 'wf-1', runId: 'run-1' },
10
10
  activityId: 'act-1',
11
+ attempt: 1,
11
12
  activityType: 'myWorkflow#myStep',
12
13
  workflowType: 'myWorkflow'
13
14
  } ) );
@@ -87,6 +88,8 @@ describe( 'ActivityExecutionInterceptor', () => {
87
88
  beforeEach( () => {
88
89
  vi.clearAllMocks();
89
90
  activityInfoMock.workflowType = 'myWorkflow';
91
+ activityInfoMock.workflowExecution.runId = 'run-1';
92
+ activityInfoMock.attempt = 1;
90
93
  delete activityInfoMock.retryPolicy;
91
94
  vi.useFakeTimers();
92
95
  // Default: heartbeat enabled with 50ms interval for fast tests
@@ -114,19 +117,19 @@ describe( 'ActivityExecutionInterceptor', () => {
114
117
  { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
115
118
  );
116
119
  expect( addEventStartMock ).toHaveBeenCalledWith( {
117
- id: 'act-1',
120
+ id: 'run-1:act-1:1',
118
121
  name: 'myWorkflow#myStep',
119
122
  kind: 'step',
120
123
  parentId: 'run-1',
121
124
  details: { someInput: 'data' },
122
125
  traceInfo: traceInfoMock
123
126
  } );
124
- expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'ok' }, traceInfo: traceInfoMock } );
127
+ expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'run-1:act-1:1', details: { result: 'ok' }, traceInfo: traceInfoMock } );
125
128
  expect( addEventErrorMock ).not.toHaveBeenCalled();
126
129
  expect( runWithContextMock ).toHaveBeenCalledWith(
127
130
  expect.any( Function ),
128
131
  expect.objectContaining( {
129
- parentId: 'act-1',
132
+ parentId: 'run-1:act-1:1',
130
133
  activityInfo: activityInfoMock,
131
134
  workflowDetails: workflowDetailsMock,
132
135
  outputActivityKind: 'step',
@@ -136,6 +139,37 @@ describe( 'ActivityExecutionInterceptor', () => {
136
139
  );
137
140
  } );
138
141
 
142
+ it( 'scopes trace ids by attempt so retries do not reuse the same id', async () => {
143
+ const { ActivityExecutionInterceptor } = await import( './activity.js' );
144
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
145
+ activityInfoMock.attempt = 3;
146
+ const next = vi.fn().mockResolvedValue( { result: 'ok' } );
147
+
148
+ await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'ok' } );
149
+ expect( addEventStartMock ).toHaveBeenCalledWith( expect.objectContaining( { id: 'run-1:act-1:3', parentId: 'run-1' } ) );
150
+ expect( addEventEndMock ).toHaveBeenCalledWith( expect.objectContaining( { id: 'run-1:act-1:3' } ) );
151
+ expect( runWithContextMock ).toHaveBeenCalledWith(
152
+ expect.any( Function ),
153
+ expect.objectContaining( { parentId: 'run-1:act-1:3' } )
154
+ );
155
+ } );
156
+
157
+ it( 'scopes trace ids by run so a child workflow reusing an activityId gets a distinct id', async () => {
158
+ const { ActivityExecutionInterceptor } = await import( './activity.js' );
159
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
160
+ const next = vi.fn().mockResolvedValue( { result: 'ok' } );
161
+
162
+ // Temporal numbers activityIds per run, so a parent and its child both reach the same 'act-1' on attempt 1.
163
+ await interceptor.execute( makeInput(), next );
164
+ activityInfoMock.workflowExecution.runId = 'run-2';
165
+ await interceptor.execute( makeInput(), next );
166
+
167
+ expect( addEventStartMock.mock.calls.map( ( [ { id, parentId } ] ) => ( { id, parentId } ) ) ).toEqual( [
168
+ { id: 'run-1:act-1:1', parentId: 'run-1' },
169
+ { id: 'run-2:act-1:1', parentId: 'run-2' }
170
+ ] );
171
+ } );
172
+
139
173
  it( 'wraps native errors with serialized details', async () => {
140
174
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
141
175
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
@@ -210,7 +244,7 @@ describe( 'ActivityExecutionInterceptor', () => {
210
244
  }
211
245
  } ]
212
246
  } );
213
- expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: cause, traceInfo: traceInfoMock } );
247
+ expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'run-1:act-1:1', details: cause, traceInfo: traceInfoMock } );
214
248
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
215
249
  activityInfo: activityInfoMock,
216
250
  workflowDetails: workflowDetailsMock,
@@ -226,7 +260,7 @@ describe( 'ActivityExecutionInterceptor', () => {
226
260
  const next = vi.fn().mockRejectedValue( error );
227
261
 
228
262
  await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
229
- expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: error, traceInfo: traceInfoMock } );
263
+ expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'run-1:act-1:1', details: error, traceInfo: traceInfoMock } );
230
264
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
231
265
  activityInfo: activityInfoMock,
232
266
  workflowDetails: workflowDetailsMock,
@@ -245,7 +279,7 @@ describe( 'ActivityExecutionInterceptor', () => {
245
279
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
246
280
  expect( mainEventBusEmitMock ).toHaveBeenCalledTimes( 1 );
247
281
  expect( addEventEndMock ).toHaveBeenCalledWith( {
248
- id: 'act-1',
282
+ id: 'run-1:act-1:1',
249
283
  details: ActivitySpecialOutput.ASYNC_HANDOFF,
250
284
  traceInfo: traceInfoMock
251
285
  } );
@@ -65,7 +65,7 @@ export const sinks = {
65
65
  trace: {
66
66
  start: {
67
67
  fn: ( workflowInfo, { id, name, kind, details } ) =>
68
- Tracing.addEventStart( { id, kind, name, details, parentId: workflowInfo.parent?.runId, traceInfo: workflowInfo.memo.traceInfo } ),
68
+ Tracing.addEventStart( { id, kind, name, details, parentId: workflowInfo.runId, traceInfo: workflowInfo.memo.traceInfo } ),
69
69
  callDuringReplay: false
70
70
  },
71
71
 
@@ -167,7 +167,7 @@ describe( 'worker/sinks', () => {
167
167
  expect( addEventErrorMock ).not.toHaveBeenCalled();
168
168
  } );
169
169
 
170
- it( 'trace.start records trace start events with workflow parent context', async () => {
170
+ it( 'trace.start parents trace start events on the current workflow, not its parent workflow', async () => {
171
171
  const { sinks } = await import( './sinks.js' );
172
172
 
173
173
  sinks.trace.start.fn( workflowInfo, {
@@ -182,7 +182,7 @@ describe( 'worker/sinks', () => {
182
182
  kind: ComponentType.STEP,
183
183
  name: 'myStep',
184
184
  details: { input: true },
185
- parentId: 'parent-run-1',
185
+ parentId: 'run-1',
186
186
  traceInfo: { traceId: 'trace-1' }
187
187
  } );
188
188
  } );