@outputai/cli 0.9.1-dev.000c5f3.0 → 0.9.1-dev.667788.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 (36) hide show
  1. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  2. package/dist/generated/framework_version.json +1 -1
  3. package/dist/views/dev/dev_app.js +3 -17
  4. package/dist/views/dev/modals/steps_modal.js +1 -1
  5. package/dist/views/dev/panels/runs_panel.js +2 -7
  6. package/dist/views/dev/panels/workflows_panel.js +1 -1
  7. package/dist/views/dev/state/ui_state.d.ts +0 -8
  8. package/dist/views/dev/state/ui_state.js +1 -5
  9. package/oclif.manifest.json +1 -81
  10. package/package.json +4 -4
  11. package/dist/commands/workflow/history.d.ts +0 -19
  12. package/dist/commands/workflow/history.js +0 -99
  13. package/dist/commands/workflow/history.spec.d.ts +0 -1
  14. package/dist/commands/workflow/history.spec.js +0 -26
  15. package/dist/services/workflow_history/correlator.d.ts +0 -26
  16. package/dist/services/workflow_history/correlator.js +0 -261
  17. package/dist/services/workflow_history/correlator.spec.d.ts +0 -1
  18. package/dist/services/workflow_history/correlator.spec.js +0 -116
  19. package/dist/services/workflow_history.d.ts +0 -23
  20. package/dist/services/workflow_history.js +0 -79
  21. package/dist/services/workflow_history.spec.d.ts +0 -1
  22. package/dist/services/workflow_history.spec.js +0 -87
  23. package/dist/utils/span_labels.d.ts +0 -11
  24. package/dist/utils/span_labels.js +0 -22
  25. package/dist/utils/span_labels.spec.d.ts +0 -1
  26. package/dist/utils/span_labels.spec.js +0 -36
  27. package/dist/utils/waterfall.d.ts +0 -31
  28. package/dist/utils/waterfall.js +0 -192
  29. package/dist/utils/waterfall.spec.d.ts +0 -1
  30. package/dist/utils/waterfall.spec.js +0 -100
  31. package/dist/views/dev/hooks/use_step_graph.d.ts +0 -22
  32. package/dist/views/dev/hooks/use_step_graph.js +0 -76
  33. package/dist/views/dev/modals/step_graph_modal.d.ts +0 -6
  34. package/dist/views/dev/modals/step_graph_modal.js +0 -148
  35. /package/dist/views/dev/utils/{ui_constants.d.ts → constants.d.ts} +0 -0
  36. /package/dist/views/dev/utils/{ui_constants.js → constants.js} +0 -0
@@ -1,261 +0,0 @@
1
- /**
2
- * Correlates a flat list of Temporal history events into per-activity spans.
3
- *
4
- * Each ACTIVITY_TASK_SCHEDULED event opens a span (its eventId IS the
5
- * scheduledEventId every subsequent ACTIVITY_TASK_* event for the same activity
6
- * references). STARTED, COMPLETED, FAILED, TIMED_OUT, and CANCELED events extend
7
- * that span. Spans without a terminal event are 'running' (if STARTED was seen)
8
- * or 'pending'.
9
- *
10
- * Child workflows follow the same pattern but with their own event family:
11
- * START_CHILD_WORKFLOW_EXECUTION_INITIATED opens the span, then
12
- * CHILD_WORKFLOW_EXECUTION_STARTED / _COMPLETED / _FAILED / _TIMED_OUT /
13
- * _CANCELED / _TERMINATED reference it via `initiatedEventId`.
14
- *
15
- * Pure function — no I/O. A TypeScript port of Atlas's
16
- * `OutputWorkflows::WorkflowHistory::Correlator` (Ruby). Kept structurally
17
- * faithful so the two stay easy to diff. The CLI has no step catalog, so a
18
- * span's display name is the humanized step name rather than a catalog label.
19
- */
20
- import { capitalCase } from 'change-case';
21
- // Framework-level activities the user doesn't care about — Output's own trace
22
- // destination resolution and the lifecycle webhook back to Atlas.
23
- const NOISE_STEP_PREFIXES = ['__internal#'];
24
- const NOISE_STEP_EXACT = ['$shared#callWebhook'];
25
- const ACTIVITY_TERMINAL_TYPES = [
26
- 'ACTIVITY_TASK_COMPLETED', 'ACTIVITY_TASK_FAILED',
27
- 'ACTIVITY_TASK_TIMED_OUT', 'ACTIVITY_TASK_CANCELED'
28
- ];
29
- const CHILD_TERMINAL_TYPES = [
30
- 'CHILD_WORKFLOW_EXECUTION_COMPLETED', 'CHILD_WORKFLOW_EXECUTION_FAILED',
31
- 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT', 'CHILD_WORKFLOW_EXECUTION_CANCELED',
32
- 'CHILD_WORKFLOW_EXECUTION_TERMINATED', 'START_CHILD_WORKFLOW_EXECUTION_FAILED'
33
- ];
34
- function eventTypeName(event) {
35
- return event.eventTypeName ?? '';
36
- }
37
- function eventId(event) {
38
- return String(event.eventId);
39
- }
40
- function eventAttributes(event) {
41
- const key = event && Object.keys(event).find(k => k.endsWith('EventAttributes'));
42
- return key ? event[key] : undefined;
43
- }
44
- // Temporal's int64 fields arrive as either a String (the API serializer
45
- // stringifies scheduledEventId / startedEventId) or a raw protobuf Long struct
46
- // `{ low, high, unsigned }` (initiatedEventId, etc). Normalize to a String key
47
- // so both shapes match the opener event's stringified eventId.
48
- function normalizeEventId(value) {
49
- if (value === null || value === undefined) {
50
- return null;
51
- }
52
- if (typeof value === 'string') {
53
- return value || null;
54
- }
55
- if (typeof value === 'object' && 'low' in value) {
56
- const low = value.low;
57
- return low === null || low === undefined ? null : String(low);
58
- }
59
- return String(value) || null;
60
- }
61
- function scheduledEventIdFor(event) {
62
- return normalizeEventId(eventAttributes(event)?.scheduledEventId);
63
- }
64
- function initiatedEventIdFor(event) {
65
- return normalizeEventId(eventAttributes(event)?.initiatedEventId);
66
- }
67
- // For the noise filter: the full `workflow#step` name (prefixes/exacts match it).
68
- function fullStepName(scheduled) {
69
- const attrs = eventAttributes(scheduled) ?? {};
70
- return attrs.activityType?.name ??
71
- attrs.stepName ??
72
- 'unknown';
73
- }
74
- // For display: the bare step name (segment after `#`). The API serializer
75
- // already exposes this as `stepName`; fall back to splitting `activityType.name`.
76
- function cleanStepName(scheduled) {
77
- const attrs = eventAttributes(scheduled) ?? {};
78
- const explicit = attrs.stepName;
79
- if (explicit) {
80
- return explicit;
81
- }
82
- const full = attrs.activityType?.name;
83
- if (full) {
84
- return full.includes('#') ? full.split('#').pop() : full;
85
- }
86
- return 'unknown';
87
- }
88
- function isNoise(stepName) {
89
- return NOISE_STEP_EXACT.includes(stepName) ||
90
- NOISE_STEP_PREFIXES.some(prefix => stepName.startsWith(prefix));
91
- }
92
- function failureMessageOf(attrs) {
93
- const failure = attrs.failure;
94
- return failure?.message ?? null;
95
- }
96
- function attemptFor(started) {
97
- return eventAttributes(started ?? undefined)?.attempt ?? 1;
98
- }
99
- function statusFor(started, terminalStatus) {
100
- if (terminalStatus) {
101
- return terminalStatus;
102
- }
103
- return started ? 'running' : 'pending';
104
- }
105
- function parseTime(value) {
106
- if (value === null || value === undefined || value === '') {
107
- return null;
108
- }
109
- return String(value);
110
- }
111
- function toMs(iso) {
112
- if (!iso) {
113
- return null;
114
- }
115
- const ms = Date.parse(iso);
116
- return Number.isNaN(ms) ? null : ms;
117
- }
118
- // Offset math relative to the workflow start (timeline origin). Mirrors
119
- // Atlas's Span#start_offset_ms / #end_offset_ms / #duration_ms.
120
- function withOffsets(span, startMs) {
121
- const startAnchor = toMs(span.startedAt) ?? toMs(span.scheduledAt);
122
- const startOffsetMs = startMs !== null && startAnchor !== null ? Math.round(startAnchor - startMs) : 0;
123
- const endAnchor = toMs(span.completedAt) ?? toMs(span.startedAt);
124
- const endOffsetMs = startMs !== null && endAnchor !== null ? Math.round(endAnchor - startMs) : startOffsetMs;
125
- return { ...span, startOffsetMs, endOffsetMs, durationMs: endOffsetMs - startOffsetMs };
126
- }
127
- function resolveActivityTerminal(terminal) {
128
- if (!terminal) {
129
- return {};
130
- }
131
- const attrs = eventAttributes(terminal) ?? {};
132
- const completedAt = parseTime(terminal.eventTime);
133
- switch (eventTypeName(terminal)) {
134
- case 'ACTIVITY_TASK_COMPLETED':
135
- return { status: 'completed', completedAt, output: attrs.result };
136
- case 'ACTIVITY_TASK_FAILED':
137
- return { status: 'failed', completedAt, failureMessage: failureMessageOf(attrs) };
138
- case 'ACTIVITY_TASK_TIMED_OUT':
139
- return { status: 'failed', completedAt, failureMessage: 'Timed out' };
140
- case 'ACTIVITY_TASK_CANCELED':
141
- return { status: 'failed', completedAt, failureMessage: 'Canceled' };
142
- default:
143
- return {};
144
- }
145
- }
146
- function resolveChildTerminal(terminal) {
147
- if (!terminal) {
148
- return {};
149
- }
150
- const attrs = eventAttributes(terminal) ?? {};
151
- const completedAt = parseTime(terminal.eventTime);
152
- switch (eventTypeName(terminal)) {
153
- case 'CHILD_WORKFLOW_EXECUTION_COMPLETED':
154
- return { status: 'completed', completedAt, output: attrs.result };
155
- case 'CHILD_WORKFLOW_EXECUTION_FAILED':
156
- return { status: 'failed', completedAt, failureMessage: failureMessageOf(attrs) };
157
- case 'CHILD_WORKFLOW_EXECUTION_TIMED_OUT':
158
- return { status: 'failed', completedAt, failureMessage: 'Timed out' };
159
- case 'CHILD_WORKFLOW_EXECUTION_CANCELED':
160
- return { status: 'failed', completedAt, failureMessage: 'Canceled' };
161
- case 'CHILD_WORKFLOW_EXECUTION_TERMINATED':
162
- return { status: 'failed', completedAt, failureMessage: 'Terminated' };
163
- case 'START_CHILD_WORKFLOW_EXECUTION_FAILED':
164
- return { status: 'failed', completedAt, failureMessage: attrs.cause || 'Failed to start' };
165
- default:
166
- return {};
167
- }
168
- }
169
- function buildActivitySpan(id, slot, startMs) {
170
- const { scheduled, started, terminal } = slot;
171
- const scheduledAttrs = eventAttributes(scheduled) ?? {};
172
- const terminalFields = resolveActivityTerminal(terminal);
173
- return withOffsets({
174
- id,
175
- name: capitalCase(cleanStepName(scheduled)),
176
- technicalName: fullStepName(scheduled),
177
- description: null,
178
- status: statusFor(started, terminalFields.status),
179
- kind: 'activity',
180
- attempt: attemptFor(started),
181
- input: scheduledAttrs.input,
182
- output: terminalFields.output,
183
- startedAt: parseTime(started?.eventTime),
184
- scheduledAt: parseTime(scheduled.eventTime),
185
- completedAt: terminalFields.completedAt ?? null,
186
- failureMessage: terminalFields.failureMessage ?? null
187
- }, startMs);
188
- }
189
- // Child workflow spans render from the initiated event time so the full child
190
- // duration is visible (matching Temporal's own UI). Pending children anchor at
191
- // initiated time rather than null, else they'd all stack at offset 0.
192
- function buildChildSpan(id, slot, startMs) {
193
- const { scheduled, started, terminal } = slot;
194
- const scheduledAttrs = eventAttributes(scheduled) ?? {};
195
- const workflowType = scheduledAttrs.workflowType?.name ??
196
- 'child_workflow';
197
- const terminalFields = resolveChildTerminal(terminal);
198
- return withOffsets({
199
- id: `child-${id}`,
200
- name: capitalCase(workflowType),
201
- technicalName: workflowType,
202
- description: null,
203
- status: statusFor(started, terminalFields.status),
204
- kind: 'child_workflow',
205
- attempt: 1,
206
- input: scheduledAttrs.input,
207
- output: terminalFields.output,
208
- startedAt: parseTime(scheduled.eventTime),
209
- scheduledAt: parseTime(scheduled.eventTime),
210
- completedAt: terminalFields.completedAt ?? null,
211
- failureMessage: terminalFields.failureMessage ?? null
212
- }, startMs);
213
- }
214
- function emptySlot(opener) {
215
- return { scheduled: opener, started: null, terminal: null };
216
- }
217
- function attach(slots, event, key) {
218
- if (!key) {
219
- return;
220
- }
221
- const slot = slots.get(key);
222
- if (!slot) {
223
- return;
224
- }
225
- if (eventTypeName(event).endsWith('_STARTED')) {
226
- slot.started = event;
227
- }
228
- else {
229
- slot.terminal = event;
230
- }
231
- }
232
- /**
233
- * @param events - flat Temporal history events, in chronological order
234
- * @param workflowStartTimeMs - epoch ms used as the timeline origin (0 offset)
235
- */
236
- export function correlate(events, workflowStartTimeMs) {
237
- const activities = new Map();
238
- const children = new Map();
239
- for (const event of events) {
240
- const type = eventTypeName(event);
241
- if (type === 'ACTIVITY_TASK_SCHEDULED') {
242
- activities.set(eventId(event), emptySlot(event));
243
- }
244
- else if (type === 'ACTIVITY_TASK_STARTED' || ACTIVITY_TERMINAL_TYPES.includes(type)) {
245
- attach(activities, event, scheduledEventIdFor(event));
246
- }
247
- else if (type === 'START_CHILD_WORKFLOW_EXECUTION_INITIATED') {
248
- children.set(eventId(event), emptySlot(event));
249
- }
250
- else if (type === 'CHILD_WORKFLOW_EXECUTION_STARTED' || CHILD_TERMINAL_TYPES.includes(type)) {
251
- attach(children, event, initiatedEventIdFor(event));
252
- }
253
- }
254
- const activitySpans = [...activities.entries()]
255
- .filter(([, slot]) => !isNoise(fullStepName(slot.scheduled)))
256
- .map(([id, slot]) => buildActivitySpan(id, slot, workflowStartTimeMs));
257
- const childSpans = [...children.entries()]
258
- .map(([id, slot]) => buildChildSpan(id, slot, workflowStartTimeMs))
259
- .filter(span => !isNoise(span.technicalName));
260
- return [...activitySpans, ...childSpans].sort((a, b) => a.startOffsetMs - b.startOffsetMs);
261
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,116 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { correlate } from './correlator.js';
3
- const T0 = Date.parse('2026-06-22T12:00:00.000Z');
4
- const at = (seconds) => new Date(T0 + (seconds * 1000)).toISOString();
5
- // Minimal Temporal-shaped event builders.
6
- const scheduled = (id, name, sec, input) => ({
7
- eventId: id,
8
- eventTypeName: 'ACTIVITY_TASK_SCHEDULED',
9
- eventTime: at(sec),
10
- activityTaskScheduledEventAttributes: { activityType: { name }, activityId: `act-${id}`, input }
11
- });
12
- const started = (id, scheduledEventId, sec) => ({
13
- eventId: id,
14
- eventTypeName: 'ACTIVITY_TASK_STARTED',
15
- eventTime: at(sec),
16
- activityTaskStartedEventAttributes: { scheduledEventId, attempt: 1 }
17
- });
18
- const completed = (id, scheduledEventId, sec) => ({
19
- eventId: id,
20
- eventTypeName: 'ACTIVITY_TASK_COMPLETED',
21
- eventTime: at(sec),
22
- activityTaskCompletedEventAttributes: { scheduledEventId, result: { ok: true } }
23
- });
24
- const failed = (id, scheduledEventId, sec, message) => ({
25
- eventId: id,
26
- eventTypeName: 'ACTIVITY_TASK_FAILED',
27
- eventTime: at(sec),
28
- activityTaskFailedEventAttributes: { scheduledEventId, failure: { message } }
29
- });
30
- function buildEvents() {
31
- return [
32
- { eventId: '1', eventTypeName: 'WORKFLOW_EXECUTION_STARTED', eventTime: at(0), workflowExecutionStartedEventAttributes: {} },
33
- // Sequential completed activity (0s → 27s)
34
- scheduled('2', 'contentBrief#compressText', 0, { text: 'x' }),
35
- started('3', '2', 0),
36
- completed('4', '2', 27),
37
- // Parallel fan-out of the same step: one completes (1s), one fails (8s)
38
- scheduled('5', 'contentBrief#scrapeSerpPage', 30),
39
- scheduled('6', 'contentBrief#scrapeSerpPage', 30),
40
- started('7', '5', 30),
41
- started('8', '6', 30),
42
- completed('9', '5', 31),
43
- failed('10', '6', 38, 'boom'),
44
- // Noise steps — must be filtered out
45
- scheduled('11', '$shared#callWebhook', 35),
46
- started('11s', '11', 35),
47
- scheduled('12', '__internal#resolveTrace', 35),
48
- // Pending activity (scheduled, never started)
49
- scheduled('13', 'contentBrief#generateBrief', 40),
50
- // Child workflow (initiatedEventId arrives as a protobuf Long struct)
51
- {
52
- eventId: '20', eventTypeName: 'START_CHILD_WORKFLOW_EXECUTION_INITIATED', eventTime: at(50),
53
- startChildWorkflowExecutionInitiatedEventAttributes: { workflowType: { name: 'subWorkflow' } }
54
- },
55
- {
56
- eventId: '21', eventTypeName: 'CHILD_WORKFLOW_EXECUTION_STARTED', eventTime: at(50),
57
- childWorkflowExecutionStartedEventAttributes: { initiatedEventId: { low: 20, high: 0, unsigned: false } }
58
- },
59
- {
60
- eventId: '22', eventTypeName: 'CHILD_WORKFLOW_EXECUTION_COMPLETED', eventTime: at(60),
61
- childWorkflowExecutionCompletedEventAttributes: { initiatedEventId: { low: 20, high: 0, unsigned: false }, result: { done: true } }
62
- }
63
- ];
64
- }
65
- describe('correlate', () => {
66
- const spans = correlate(buildEvents(), T0);
67
- const byId = (id) => spans.find(s => s.id === id);
68
- it('filters framework noise steps', () => {
69
- expect(spans.every(s => !s.technicalName.startsWith('__internal#'))).toBe(true);
70
- expect(spans.some(s => s.technicalName === '$shared#callWebhook')).toBe(false);
71
- });
72
- it('produces one span per real activity plus the child workflow', () => {
73
- expect(spans).toHaveLength(5);
74
- });
75
- it('returns spans sorted by start offset', () => {
76
- const offsets = spans.map(s => s.startOffsetMs);
77
- expect(offsets).toEqual([...offsets].sort((a, b) => a - b));
78
- });
79
- it('correlates a completed activity with humanized name, offset, and duration', () => {
80
- const compress = byId('2');
81
- expect(compress.name).toBe('Compress Text');
82
- expect(compress.status).toBe('completed');
83
- expect(compress.startOffsetMs).toBe(0);
84
- expect(compress.durationMs).toBe(27_000);
85
- expect(compress.attempt).toBe(1);
86
- expect(compress.kind).toBe('activity');
87
- });
88
- it('distinguishes parallel instances of the same step', () => {
89
- const ok = byId('5');
90
- const bad = byId('6');
91
- expect(ok.name).toBe('Scrape Serp Page');
92
- expect(ok.status).toBe('completed');
93
- expect(ok.startOffsetMs).toBe(30_000);
94
- expect(ok.durationMs).toBe(1_000);
95
- expect(bad.status).toBe('failed');
96
- expect(bad.failureMessage).toBe('boom');
97
- expect(bad.durationMs).toBe(8_000);
98
- });
99
- it('marks a scheduled-only activity pending with a zero-duration span at its scheduled offset', () => {
100
- const pending = byId('13');
101
- expect(pending.status).toBe('pending');
102
- expect(pending.startOffsetMs).toBe(40_000);
103
- expect(pending.durationMs).toBe(0);
104
- });
105
- it('correlates a child workflow via a Long-struct initiatedEventId', () => {
106
- const child = byId('child-20');
107
- expect(child.name).toBe('Sub Workflow');
108
- expect(child.kind).toBe('child_workflow');
109
- expect(child.status).toBe('completed');
110
- expect(child.startOffsetMs).toBe(50_000);
111
- expect(child.durationMs).toBe(10_000);
112
- });
113
- it('is robust to an empty event list', () => {
114
- expect(correlate([], T0)).toEqual([]);
115
- });
116
- });
@@ -1,23 +0,0 @@
1
- import { type HistoryEvent, type Span } from '#services/workflow_history/correlator.js';
2
- export interface WorkflowMeta {
3
- workflowId?: string;
4
- runId?: string;
5
- status?: string;
6
- startTime?: string;
7
- closeTime?: string | null;
8
- historyLength?: number;
9
- taskQueue?: string;
10
- }
11
- export interface FetchWorkflowHistoryOptions {
12
- workflowId: string;
13
- runId?: string;
14
- includePayloads?: boolean;
15
- }
16
- export interface WorkflowHistoryResult {
17
- workflow: WorkflowMeta | null;
18
- runId: string | null;
19
- events: HistoryEvent[];
20
- spans: Span[];
21
- totalDurationMs: number;
22
- }
23
- export declare function fetchWorkflowHistory(options: FetchWorkflowHistoryOptions): Promise<WorkflowHistoryResult>;
@@ -1,79 +0,0 @@
1
- /**
2
- * Fetches a workflow run's full Temporal history from the API and correlates the
3
- * flat event stream into spans (one per step) the CLI can render as a waterfall.
4
- *
5
- * Pages through `GET /workflow/{id}/history` (mirroring Atlas's
6
- * OutputWorkflows::WorkflowHistory): the first page carries the workflow
7
- * metadata and resolves the run ID; subsequent pages echo that run ID alongside
8
- * the `nextPageToken` (the endpoint requires runId once a pageToken is used).
9
- */
10
- import { getWorkflowIdHistory } from '#api/generated/api.js';
11
- import { correlate } from '#services/workflow_history/correlator.js';
12
- const PAGE_SIZE = 50;
13
- function toMs(value) {
14
- if (!value) {
15
- return null;
16
- }
17
- const ms = Date.parse(String(value));
18
- return Number.isNaN(ms) ? null : ms;
19
- }
20
- function earliestEventMs(events) {
21
- const times = events
22
- .map(e => toMs(e.eventTime))
23
- .filter((ms) => ms !== null);
24
- return times.length > 0 ? Math.min(...times) : null;
25
- }
26
- // Timeline origin (0 offset): the workflow's start time, falling back to the
27
- // WORKFLOW_EXECUTION_STARTED event, then the earliest event seen.
28
- function workflowStartMs(meta, events) {
29
- const fromMeta = toMs(meta?.startTime);
30
- if (fromMeta !== null) {
31
- return fromMeta;
32
- }
33
- const started = events.find(e => e.eventTypeName === 'WORKFLOW_EXECUTION_STARTED');
34
- const fromStarted = toMs(started?.eventTime);
35
- if (fromStarted !== null) {
36
- return fromStarted;
37
- }
38
- return earliestEventMs(events);
39
- }
40
- function totalDuration(meta, spans, startMs) {
41
- const closeMs = toMs(meta?.closeTime ?? undefined);
42
- if (closeMs !== null && startMs !== null && (closeMs - startMs) > 0) {
43
- return closeMs - startMs;
44
- }
45
- const maxEnd = spans.reduce((max, span) => Math.max(max, span.endOffsetMs), 0);
46
- return Math.max(maxEnd, 1);
47
- }
48
- async function fetchAllPages(workflowId, includePayloads, runId, pageToken, acc) {
49
- const response = await getWorkflowIdHistory(workflowId, { runId, pageSize: PAGE_SIZE, pageToken, includePayloads });
50
- if (!response.data) {
51
- throw new Error('API returned invalid response (missing data)');
52
- }
53
- const data = response.data;
54
- // The generated `data.workflow` is an opaque `{ [key: string]: unknown }`, so
55
- // narrow it to WorkflowMeta via `unknown` (its real fields are validated by
56
- // the server, mirroring Atlas's metadata shape).
57
- const meta = acc.meta ?? data.workflow ?? null;
58
- const resolvedRunId = runId ?? data.runId ?? acc.runId;
59
- const events = [...acc.events, ...(data.events ?? [])];
60
- const nextToken = data.nextPageToken ?? undefined;
61
- const nextAcc = { meta, runId: resolvedRunId, events };
62
- if (nextToken) {
63
- return fetchAllPages(workflowId, includePayloads, resolvedRunId, nextToken, nextAcc);
64
- }
65
- return nextAcc;
66
- }
67
- export async function fetchWorkflowHistory(options) {
68
- const { workflowId, runId, includePayloads = false } = options;
69
- const { meta, runId: resolvedRunId, events } = await fetchAllPages(workflowId, includePayloads, runId, undefined, { meta: null, runId, events: [] });
70
- const startMs = workflowStartMs(meta, events);
71
- const spans = correlate(events, startMs);
72
- return {
73
- workflow: meta,
74
- runId: resolvedRunId ?? meta?.runId ?? null,
75
- events,
76
- spans,
77
- totalDurationMs: totalDuration(meta, spans, startMs)
78
- };
79
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,87 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { getWorkflowIdHistory } from '#api/generated/api.js';
3
- import { fetchWorkflowHistory } from '#services/workflow_history.js';
4
- vi.mock('#api/generated/api.js', () => ({ getWorkflowIdHistory: vi.fn() }));
5
- // The real correlator runs (only the API client is mocked), so spans are derived
6
- // from the events below — letting us assert duration/offset behaviour end to end.
7
- const mockGet = getWorkflowIdHistory;
8
- const T0 = Date.parse('2026-06-22T12:00:00.000Z');
9
- const at = (seconds) => new Date(T0 + (seconds * 1000)).toISOString();
10
- const page = (data) => ({ status: 200, data });
11
- const started = (id, scheduledEventId, sec) => ({
12
- eventId: id, eventTypeName: 'ACTIVITY_TASK_STARTED', eventTime: at(sec),
13
- activityTaskStartedEventAttributes: { scheduledEventId, attempt: 1 }
14
- });
15
- const completed = (id, scheduledEventId, sec) => ({
16
- eventId: id, eventTypeName: 'ACTIVITY_TASK_COMPLETED', eventTime: at(sec),
17
- activityTaskCompletedEventAttributes: { scheduledEventId }
18
- });
19
- const scheduled = (id, name, sec) => ({
20
- eventId: id, eventTypeName: 'ACTIVITY_TASK_SCHEDULED', eventTime: at(sec),
21
- activityTaskScheduledEventAttributes: { activityType: { name }, activityId: `act-${id}` }
22
- });
23
- const workflowStarted = (sec) => ({ eventId: '1', eventTypeName: 'WORKFLOW_EXECUTION_STARTED', eventTime: at(sec) });
24
- beforeEach(() => mockGet.mockReset());
25
- describe('fetchWorkflowHistory', () => {
26
- it('pages through results, pinning the resolved runId after the first page', async () => {
27
- mockGet
28
- .mockResolvedValueOnce(page({
29
- workflow: { workflowId: 'wf-123', runId: 'run-456', status: 'completed', startTime: at(0), closeTime: at(300) },
30
- runId: 'run-456',
31
- events: [workflowStarted(0), scheduled('2', 'contentBrief#compressText', 0)],
32
- nextPageToken: 'token-2'
33
- }))
34
- .mockResolvedValueOnce(page({
35
- workflow: null,
36
- runId: 'run-456',
37
- events: [started('3', '2', 0), completed('4', '2', 27)],
38
- nextPageToken: null
39
- }));
40
- const result = await fetchWorkflowHistory({ workflowId: 'wf-123' });
41
- expect(mockGet).toHaveBeenCalledTimes(2);
42
- expect(mockGet).toHaveBeenNthCalledWith(1, 'wf-123', { runId: undefined, pageSize: 50, pageToken: undefined, includePayloads: false });
43
- // Page 2 echoes the runId the first page resolved (the endpoint requires it once a pageToken is used).
44
- expect(mockGet).toHaveBeenNthCalledWith(2, 'wf-123', { runId: 'run-456', pageSize: 50, pageToken: 'token-2', includePayloads: false });
45
- expect(result.runId).toBe('run-456');
46
- expect(result.workflow?.workflowId).toBe('wf-123'); // metadata is taken from the first page only
47
- expect(result.events).toHaveLength(4);
48
- expect(result.totalDurationMs).toBe(300_000); // closeTime - startTime
49
- expect(result.spans).toHaveLength(1);
50
- expect(result.spans[0].durationMs).toBe(27_000);
51
- });
52
- it('forwards an explicit runId and includePayloads, stopping after a single page', async () => {
53
- mockGet.mockResolvedValueOnce(page({
54
- workflow: { workflowId: 'wf-9', runId: 'run-1', status: 'running', startTime: at(0) },
55
- runId: 'run-1', events: [], nextPageToken: null
56
- }));
57
- const result = await fetchWorkflowHistory({ workflowId: 'wf-9', runId: 'run-1', includePayloads: true });
58
- expect(mockGet).toHaveBeenCalledTimes(1);
59
- expect(mockGet).toHaveBeenCalledWith('wf-9', { runId: 'run-1', pageSize: 50, pageToken: undefined, includePayloads: true });
60
- expect(result.runId).toBe('run-1');
61
- });
62
- it('falls back to the last span end for total duration when there is no closeTime', async () => {
63
- mockGet.mockResolvedValueOnce(page({
64
- workflow: { workflowId: 'wf-2', runId: 'run-2', status: 'running', startTime: at(0) },
65
- runId: 'run-2',
66
- events: [scheduled('2', 'wf#step', 0), started('3', '2', 0), completed('4', '2', 12)],
67
- nextPageToken: null
68
- }));
69
- const result = await fetchWorkflowHistory({ workflowId: 'wf-2' });
70
- expect(result.totalDurationMs).toBe(12_000);
71
- });
72
- it('derives the timeline origin from WORKFLOW_EXECUTION_STARTED when metadata lacks a startTime', async () => {
73
- mockGet.mockResolvedValueOnce(page({
74
- workflow: { workflowId: 'wf-3', runId: 'run-3', status: 'completed' }, // no startTime / closeTime
75
- runId: 'run-3',
76
- events: [workflowStarted(0), scheduled('2', 'wf#step', 5), started('3', '2', 5), completed('4', '2', 10)],
77
- nextPageToken: null
78
- }));
79
- const result = await fetchWorkflowHistory({ workflowId: 'wf-3' });
80
- expect(result.spans[0].startOffsetMs).toBe(5_000);
81
- expect(result.totalDurationMs).toBe(10_000);
82
- });
83
- it('throws when the response has no data', async () => {
84
- mockGet.mockResolvedValueOnce({ status: 200 });
85
- await expect(fetchWorkflowHistory({ workflowId: 'wf-x' })).rejects.toThrow(/invalid response/);
86
- });
87
- });
@@ -1,11 +0,0 @@
1
- /**
2
- * A step's name is per step-TYPE, so a fan-out of N identical activities (e.g.
3
- * 9× "Scrape Serp Page") all share one label and become indistinguishable.
4
- * Number the repeats in chronological order so each instance is addressable —
5
- * "Scrape Serp Page #1", "#2", …. Labels that occur once are left untouched.
6
- *
7
- * Pass spans in their stored (chronological) order so #1 is the earliest. A
8
- * TypeScript port of Atlas's `buildSpanLabels`.
9
- */
10
- import type { Span } from '#services/workflow_history/correlator.js';
11
- export default function buildSpanLabels(spans: Span[]): Map<string, string>;
@@ -1,22 +0,0 @@
1
- const baseLabel = (span) => span.description ?? span.name;
2
- export default function buildSpanLabels(spans) {
3
- const totals = new Map();
4
- for (const span of spans) {
5
- const label = baseLabel(span);
6
- totals.set(label, (totals.get(label) ?? 0) + 1);
7
- }
8
- const seen = new Map();
9
- const labels = new Map();
10
- for (const span of spans) {
11
- const label = baseLabel(span);
12
- if ((totals.get(label) ?? 0) > 1) {
13
- const n = (seen.get(label) ?? 0) + 1;
14
- seen.set(label, n);
15
- labels.set(span.id, `${label} #${n}`);
16
- }
17
- else {
18
- labels.set(span.id, label);
19
- }
20
- }
21
- return labels;
22
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,36 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import buildSpanLabels from './span_labels.js';
3
- const span = (id, name) => ({
4
- id,
5
- name,
6
- technicalName: name,
7
- description: null,
8
- status: 'completed',
9
- kind: 'activity',
10
- attempt: 1,
11
- startedAt: null,
12
- scheduledAt: null,
13
- completedAt: null,
14
- startOffsetMs: 0,
15
- endOffsetMs: 0,
16
- durationMs: 0,
17
- failureMessage: null
18
- });
19
- describe('buildSpanLabels', () => {
20
- it('numbers repeated labels chronologically and leaves unique labels untouched', () => {
21
- const spans = [
22
- span('a', 'Compress Text'),
23
- span('b', 'Scrape Serp Page'),
24
- span('c', 'Scrape Serp Page'),
25
- span('d', 'Scrape Serp Page')
26
- ];
27
- const labels = buildSpanLabels(spans);
28
- expect(labels.get('a')).toBe('Compress Text');
29
- expect(labels.get('b')).toBe('Scrape Serp Page #1');
30
- expect(labels.get('c')).toBe('Scrape Serp Page #2');
31
- expect(labels.get('d')).toBe('Scrape Serp Page #3');
32
- });
33
- it('returns an empty map for no spans', () => {
34
- expect(buildSpanLabels([]).size).toBe(0);
35
- });
36
- });