deepline 0.1.275 → 0.1.276
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/dist/bundling-sources/sdk/src/index.ts +4 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +588 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +25 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +62 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +49 -9
- package/dist/cli/index.js +566 -184
- package/dist/cli/index.mjs +566 -184
- package/dist/index.d.mts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +359 -56
- package/dist/index.mjs +359 -56
- package/package.json +1 -1
|
@@ -168,6 +168,10 @@ export type {
|
|
|
168
168
|
PlayRunActionPackage,
|
|
169
169
|
PlayRunDatasetActions,
|
|
170
170
|
PlayRunPackage,
|
|
171
|
+
PlayActivityObservation,
|
|
172
|
+
PlayActivityState,
|
|
173
|
+
PlayActivityTarget,
|
|
174
|
+
PlayRunActivityProjection,
|
|
171
175
|
PlayRunStart,
|
|
172
176
|
ClearPlayHistoryRequest,
|
|
173
177
|
ClearPlayHistoryResult,
|
|
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
|
|
|
155
155
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
156
156
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
157
157
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
158
|
-
version: '0.1.
|
|
158
|
+
version: '0.1.276',
|
|
159
159
|
contracts: {
|
|
160
160
|
api: {
|
|
161
161
|
name: 'sdk-http-api',
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest';
|
|
2
2
|
import type { PlayRuntimeSelection } from '../../shared_libs/play-runtime/runtime-environment';
|
|
3
|
+
import type {
|
|
4
|
+
PlayActivityObservation,
|
|
5
|
+
PlayActivityState,
|
|
6
|
+
PlayActivityTarget,
|
|
7
|
+
PlayRunActivityProjection,
|
|
8
|
+
} from '../../shared_libs/play-runtime/activity-observation';
|
|
9
|
+
export type {
|
|
10
|
+
PlayActivityObservation,
|
|
11
|
+
PlayActivityState,
|
|
12
|
+
PlayActivityTarget,
|
|
13
|
+
PlayRunActivityProjection,
|
|
14
|
+
};
|
|
3
15
|
import type {
|
|
4
16
|
DeeplineToolCategory,
|
|
5
17
|
PlayBootstrapFinderKind,
|
|
@@ -580,6 +592,8 @@ export interface PlayRunPackage {
|
|
|
580
592
|
finishedAt?: number | null;
|
|
581
593
|
durationMs?: number | null;
|
|
582
594
|
error?: string;
|
|
595
|
+
/** Canonical explanation of what this run is doing or waiting on. */
|
|
596
|
+
activity?: PlayRunActivityProjection | null;
|
|
583
597
|
};
|
|
584
598
|
/** Bounded customer-safe warnings about output projection or availability. */
|
|
585
599
|
warnings?: string[];
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical, producer-facing description of what a play is doing.
|
|
3
|
+
*
|
|
4
|
+
* Producers report facts. They do not choose log copy, UI placement, polling
|
|
5
|
+
* intervals, or whether a run is "healthy". The activity router and projector
|
|
6
|
+
* own those decisions centrally.
|
|
7
|
+
*/
|
|
8
|
+
export type PlayActivityTarget =
|
|
9
|
+
| {
|
|
10
|
+
kind: 'provider';
|
|
11
|
+
provider: string;
|
|
12
|
+
operation: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
kind: 'dataset';
|
|
17
|
+
tableNamespace: string;
|
|
18
|
+
operation: 'materializing' | 'persisting' | 'indexing' | 'reading';
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
kind: 'lease';
|
|
23
|
+
resourceKind: 'run' | 'dataset' | 'row' | 'runner';
|
|
24
|
+
resourceLabel: string;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
kind: 'runner';
|
|
28
|
+
backend: string;
|
|
29
|
+
label?: string;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
kind: 'step';
|
|
33
|
+
stepId: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
kind: 'capacity';
|
|
38
|
+
pool: 'worker' | 'provider' | 'dataset' | 'runner';
|
|
39
|
+
label?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type PlayActivityProgress = {
|
|
43
|
+
completed?: number;
|
|
44
|
+
total?: number;
|
|
45
|
+
failed?: number;
|
|
46
|
+
message?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type PlayActivityState =
|
|
50
|
+
| {
|
|
51
|
+
kind: 'active';
|
|
52
|
+
progress?: PlayActivityProgress;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
kind: 'queued';
|
|
56
|
+
reason: 'capacity';
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
kind: 'waiting';
|
|
60
|
+
reason:
|
|
61
|
+
| {
|
|
62
|
+
kind: 'external_event';
|
|
63
|
+
provider?: string;
|
|
64
|
+
eventKey?: string;
|
|
65
|
+
remaining?: number;
|
|
66
|
+
deadlineAt?: number;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
kind: 'dataset';
|
|
70
|
+
tableNamespace: string;
|
|
71
|
+
requiredPhase: 'available' | 'persisted' | 'indexed';
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
kind: 'lease';
|
|
75
|
+
resourceKind: 'run' | 'dataset' | 'row' | 'runner';
|
|
76
|
+
resourceLabel: string;
|
|
77
|
+
expiresAt?: number;
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
/** Mixed-version fallback only; new producers must choose a typed reason. */
|
|
81
|
+
kind: 'unknown';
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
kind: 'retrying';
|
|
86
|
+
reason: 'rate_limit' | 'provider_error' | 'transport_error';
|
|
87
|
+
retryAt: number;
|
|
88
|
+
attempt?: number;
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
kind: 'scheduled';
|
|
92
|
+
reason: 'sleep';
|
|
93
|
+
resumeAt: number;
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
kind: 'completed';
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
kind: 'failed';
|
|
100
|
+
code?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type PlayActivityObservation = {
|
|
104
|
+
schemaVersion: 1;
|
|
105
|
+
/** Stable within one run, for example `tool:discover_ctos` or `dataset:leads`. */
|
|
106
|
+
activityId: string;
|
|
107
|
+
/** Optional owning play step. */
|
|
108
|
+
stepId?: string;
|
|
109
|
+
target: PlayActivityTarget;
|
|
110
|
+
state: PlayActivityState;
|
|
111
|
+
/** Producer clock. The runtime envelope supplies run identity and ordering. */
|
|
112
|
+
observedAt: number;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type PlayActivityEvent = {
|
|
116
|
+
type: 'activity.observed';
|
|
117
|
+
observation: PlayActivityObservation;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
121
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isPlayActivityObservation(
|
|
125
|
+
value: unknown,
|
|
126
|
+
): value is PlayActivityObservation {
|
|
127
|
+
if (
|
|
128
|
+
!isRecord(value) ||
|
|
129
|
+
value.schemaVersion !== 1 ||
|
|
130
|
+
typeof value.activityId !== 'string' ||
|
|
131
|
+
!value.activityId.trim() ||
|
|
132
|
+
typeof value.observedAt !== 'number' ||
|
|
133
|
+
!Number.isFinite(value.observedAt) ||
|
|
134
|
+
!isRecord(value.target) ||
|
|
135
|
+
!isRecord(value.state)
|
|
136
|
+
) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const targetKinds = new Set([
|
|
140
|
+
'provider',
|
|
141
|
+
'dataset',
|
|
142
|
+
'lease',
|
|
143
|
+
'runner',
|
|
144
|
+
'step',
|
|
145
|
+
'capacity',
|
|
146
|
+
]);
|
|
147
|
+
const stateKinds = new Set([
|
|
148
|
+
'active',
|
|
149
|
+
'queued',
|
|
150
|
+
'waiting',
|
|
151
|
+
'retrying',
|
|
152
|
+
'scheduled',
|
|
153
|
+
'completed',
|
|
154
|
+
'failed',
|
|
155
|
+
]);
|
|
156
|
+
return (
|
|
157
|
+
typeof value.target.kind === 'string' &&
|
|
158
|
+
targetKinds.has(value.target.kind) &&
|
|
159
|
+
typeof value.state.kind === 'string' &&
|
|
160
|
+
stateKinds.has(value.state.kind)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type PlayRunActivityProjection = {
|
|
165
|
+
observation: PlayActivityObservation;
|
|
166
|
+
summary: string;
|
|
167
|
+
visibility: PlayActivityVisibility;
|
|
168
|
+
/** False only when old payloads lacked enough typed facts to classify. */
|
|
169
|
+
classified: boolean;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Persistence lanes are a routing concern, not a producer concern:
|
|
174
|
+
*
|
|
175
|
+
* - durable: append a meaningful transition to the run ledger;
|
|
176
|
+
* - snapshot: replace current progress for this activity;
|
|
177
|
+
* - liveness: update last-seen evidence without growing the event ledger;
|
|
178
|
+
* - drop: stale or duplicate observation.
|
|
179
|
+
*/
|
|
180
|
+
export type PlayActivityRoutingLane =
|
|
181
|
+
| 'durable'
|
|
182
|
+
| 'snapshot'
|
|
183
|
+
| 'liveness'
|
|
184
|
+
| 'drop';
|
|
185
|
+
|
|
186
|
+
function stableJson(value: unknown): string {
|
|
187
|
+
return JSON.stringify(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function targetIdentity(target: PlayActivityTarget): string {
|
|
191
|
+
return stableJson(target);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function routePlayActivityObservation(input: {
|
|
195
|
+
previous?: PlayActivityObservation | null;
|
|
196
|
+
next: PlayActivityObservation;
|
|
197
|
+
}): PlayActivityRoutingLane {
|
|
198
|
+
const { previous, next } = input;
|
|
199
|
+
if (!previous) return 'durable';
|
|
200
|
+
if (next.observedAt <= previous.observedAt) return 'drop';
|
|
201
|
+
if (targetIdentity(previous.target) !== targetIdentity(next.target)) {
|
|
202
|
+
return 'durable';
|
|
203
|
+
}
|
|
204
|
+
if (previous.state.kind !== next.state.kind) return 'durable';
|
|
205
|
+
if (next.state.kind !== 'active') {
|
|
206
|
+
return stableJson(previous.state) === stableJson(next.state)
|
|
207
|
+
? 'liveness'
|
|
208
|
+
: 'durable';
|
|
209
|
+
}
|
|
210
|
+
return stableJson(previous.state) === stableJson(next.state)
|
|
211
|
+
? 'liveness'
|
|
212
|
+
: 'snapshot';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type PlayActivityVisibility =
|
|
216
|
+
| 'hidden'
|
|
217
|
+
| 'timeline'
|
|
218
|
+
| 'headline'
|
|
219
|
+
| 'action';
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Central presentation policy. Short, self-healing retries do not become
|
|
223
|
+
* alarming UI/log messages; blockers and failures do.
|
|
224
|
+
*/
|
|
225
|
+
export function derivePlayActivityVisibility(input: {
|
|
226
|
+
observation: PlayActivityObservation;
|
|
227
|
+
now: number;
|
|
228
|
+
}): PlayActivityVisibility {
|
|
229
|
+
const { state } = input.observation;
|
|
230
|
+
if (state.kind === 'failed') return 'action';
|
|
231
|
+
if (state.kind === 'waiting') {
|
|
232
|
+
return state.reason.kind === 'external_event' ? 'action' : 'headline';
|
|
233
|
+
}
|
|
234
|
+
if (state.kind === 'queued' || state.kind === 'scheduled') return 'headline';
|
|
235
|
+
if (state.kind === 'retrying') {
|
|
236
|
+
return state.retryAt - input.now <= 30_000 ? 'hidden' : 'timeline';
|
|
237
|
+
}
|
|
238
|
+
if (state.kind === 'active') return 'headline';
|
|
239
|
+
return 'timeline';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function targetLabel(target: PlayActivityTarget): string {
|
|
243
|
+
switch (target.kind) {
|
|
244
|
+
case 'provider':
|
|
245
|
+
return target.label ?? `${target.provider} ${target.operation}`;
|
|
246
|
+
case 'dataset':
|
|
247
|
+
return target.label ?? `dataset ${target.tableNamespace}`;
|
|
248
|
+
case 'lease':
|
|
249
|
+
return `${target.resourceKind} ${target.resourceLabel}`;
|
|
250
|
+
case 'runner':
|
|
251
|
+
return target.label ?? `${target.backend} runner`;
|
|
252
|
+
case 'step':
|
|
253
|
+
return target.label ?? target.stepId;
|
|
254
|
+
case 'capacity':
|
|
255
|
+
return target.label ?? `${target.pool} capacity`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function summarizePlayActivity(
|
|
260
|
+
observation: PlayActivityObservation,
|
|
261
|
+
): string {
|
|
262
|
+
const label = targetLabel(observation.target);
|
|
263
|
+
const { state } = observation;
|
|
264
|
+
switch (state.kind) {
|
|
265
|
+
case 'active': {
|
|
266
|
+
const progress = state.progress;
|
|
267
|
+
const counts =
|
|
268
|
+
typeof progress?.completed === 'number' &&
|
|
269
|
+
typeof progress.total === 'number'
|
|
270
|
+
? ` (${progress.completed}/${progress.total})`
|
|
271
|
+
: '';
|
|
272
|
+
return `Active: ${label}${counts}.`;
|
|
273
|
+
}
|
|
274
|
+
case 'queued':
|
|
275
|
+
return `Queued: waiting for ${label}.`;
|
|
276
|
+
case 'waiting':
|
|
277
|
+
if (state.reason.kind === 'external_event') {
|
|
278
|
+
const provider = state.reason.provider
|
|
279
|
+
? ` from ${state.reason.provider}`
|
|
280
|
+
: '';
|
|
281
|
+
return `Blocked: waiting for an external event${provider}.`;
|
|
282
|
+
}
|
|
283
|
+
if (state.reason.kind === 'dataset') {
|
|
284
|
+
return `Blocked: waiting for dataset ${state.reason.tableNamespace} to become ${state.reason.requiredPhase}.`;
|
|
285
|
+
}
|
|
286
|
+
if (state.reason.kind === 'lease') {
|
|
287
|
+
return `Contended: waiting for ${state.reason.resourceKind} lease ${state.reason.resourceLabel}.`;
|
|
288
|
+
}
|
|
289
|
+
return `Paused: ${label}; the legacy producer did not report a typed reason.`;
|
|
290
|
+
case 'retrying':
|
|
291
|
+
return `Delayed: ${label} will retry automatically.`;
|
|
292
|
+
case 'scheduled':
|
|
293
|
+
return `Scheduled: ${label} will resume automatically.`;
|
|
294
|
+
case 'completed':
|
|
295
|
+
return `Completed: ${label}.`;
|
|
296
|
+
case 'failed':
|
|
297
|
+
return `Failed: ${label}.`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export type PlayActivityReporter = {
|
|
302
|
+
observe(
|
|
303
|
+
input: Omit<PlayActivityObservation, 'schemaVersion' | 'observedAt'> & {
|
|
304
|
+
observedAt?: number;
|
|
305
|
+
},
|
|
306
|
+
): void;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export function createPlayActivityReporter(input: {
|
|
310
|
+
emit: (event: PlayActivityEvent) => void;
|
|
311
|
+
now?: () => number;
|
|
312
|
+
}): PlayActivityReporter {
|
|
313
|
+
return {
|
|
314
|
+
observe(observation) {
|
|
315
|
+
input.emit({
|
|
316
|
+
type: 'activity.observed',
|
|
317
|
+
observation: {
|
|
318
|
+
...observation,
|
|
319
|
+
schemaVersion: 1,
|
|
320
|
+
observedAt: observation.observedAt ?? input.now?.() ?? Date.now(),
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
type ProjectionStep = {
|
|
328
|
+
nodeId: string;
|
|
329
|
+
status?: string;
|
|
330
|
+
label?: string;
|
|
331
|
+
updatedAt?: number | null;
|
|
332
|
+
progress?: PlayActivityProgress | null;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
type ProjectionDataset = {
|
|
336
|
+
datasetId?: string;
|
|
337
|
+
tableNamespace: string;
|
|
338
|
+
phase?: string;
|
|
339
|
+
persistedRows?: number;
|
|
340
|
+
succeededRows?: number;
|
|
341
|
+
failedRows?: number;
|
|
342
|
+
complete?: boolean;
|
|
343
|
+
updatedAt?: number;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
function isTerminalActivityState(state: PlayActivityState): boolean {
|
|
347
|
+
return state.kind === 'completed' || state.kind === 'failed';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function projection(
|
|
351
|
+
observation: PlayActivityObservation,
|
|
352
|
+
now: number,
|
|
353
|
+
classified = true,
|
|
354
|
+
): PlayRunActivityProjection {
|
|
355
|
+
return {
|
|
356
|
+
observation,
|
|
357
|
+
summary: summarizePlayActivity(observation),
|
|
358
|
+
visibility: derivePlayActivityVisibility({ observation, now }),
|
|
359
|
+
classified,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Read-side compatibility projector. Explicit typed observations win; existing
|
|
365
|
+
* suspension, dataset, and step facts are adapted without requiring every
|
|
366
|
+
* legacy producer to migrate in one deploy.
|
|
367
|
+
*/
|
|
368
|
+
export function projectPlayRunActivity(input: {
|
|
369
|
+
runId: string;
|
|
370
|
+
playName?: string | null;
|
|
371
|
+
status: string;
|
|
372
|
+
updatedAt?: number | null;
|
|
373
|
+
waitKind?: string | null;
|
|
374
|
+
waitUntil?: number | null;
|
|
375
|
+
eventKey?: string | null;
|
|
376
|
+
runtimeBackend?: string | null;
|
|
377
|
+
activeNodeId?: string | null;
|
|
378
|
+
nodeStates?: ProjectionStep[] | null;
|
|
379
|
+
datasets?: ProjectionDataset[] | null;
|
|
380
|
+
explicit?: PlayActivityObservation[] | null;
|
|
381
|
+
now?: number;
|
|
382
|
+
}): PlayRunActivityProjection | null {
|
|
383
|
+
const now = input.now ?? Date.now();
|
|
384
|
+
const observedAt = input.updatedAt ?? now;
|
|
385
|
+
const normalizedStatus = input.status.trim().toLowerCase();
|
|
386
|
+
if (
|
|
387
|
+
normalizedStatus === 'completed' ||
|
|
388
|
+
normalizedStatus === 'failed' ||
|
|
389
|
+
normalizedStatus === 'cancelled' ||
|
|
390
|
+
normalizedStatus === 'terminated' ||
|
|
391
|
+
normalizedStatus === 'timed_out'
|
|
392
|
+
) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const explicit = [...(input.explicit ?? [])]
|
|
397
|
+
.filter((candidate) => !isTerminalActivityState(candidate.state))
|
|
398
|
+
.sort((left, right) => right.observedAt - left.observedAt)[0];
|
|
399
|
+
if (explicit) return projection(explicit, now);
|
|
400
|
+
|
|
401
|
+
if (input.waitKind === 'detached_runner') {
|
|
402
|
+
return projection(
|
|
403
|
+
{
|
|
404
|
+
schemaVersion: 1,
|
|
405
|
+
activityId: 'runner:managed',
|
|
406
|
+
target: {
|
|
407
|
+
kind: 'runner',
|
|
408
|
+
backend: input.runtimeBackend ?? 'managed',
|
|
409
|
+
label: 'managed play runner',
|
|
410
|
+
},
|
|
411
|
+
state: { kind: 'active' },
|
|
412
|
+
observedAt,
|
|
413
|
+
},
|
|
414
|
+
now,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (input.waitKind === 'sleep') {
|
|
418
|
+
return projection(
|
|
419
|
+
{
|
|
420
|
+
schemaVersion: 1,
|
|
421
|
+
activityId: 'run:sleep',
|
|
422
|
+
target: {
|
|
423
|
+
kind: 'step',
|
|
424
|
+
stepId: input.activeNodeId ?? 'run',
|
|
425
|
+
label: input.playName ?? 'play',
|
|
426
|
+
},
|
|
427
|
+
state: {
|
|
428
|
+
kind: 'scheduled',
|
|
429
|
+
reason: 'sleep',
|
|
430
|
+
resumeAt: input.waitUntil ?? observedAt,
|
|
431
|
+
},
|
|
432
|
+
observedAt,
|
|
433
|
+
},
|
|
434
|
+
now,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
input.waitKind === 'integration_event' ||
|
|
439
|
+
input.waitKind === 'integration_event_batch'
|
|
440
|
+
) {
|
|
441
|
+
return projection(
|
|
442
|
+
{
|
|
443
|
+
schemaVersion: 1,
|
|
444
|
+
activityId: `event:${input.activeNodeId ?? 'run'}`,
|
|
445
|
+
stepId: input.activeNodeId ?? undefined,
|
|
446
|
+
target: {
|
|
447
|
+
kind: 'step',
|
|
448
|
+
stepId: input.activeNodeId ?? 'run',
|
|
449
|
+
label: input.playName ?? 'play',
|
|
450
|
+
},
|
|
451
|
+
state: {
|
|
452
|
+
kind: 'waiting',
|
|
453
|
+
reason: {
|
|
454
|
+
kind: 'external_event',
|
|
455
|
+
eventKey: input.eventKey ?? undefined,
|
|
456
|
+
deadlineAt: input.waitUntil ?? undefined,
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
observedAt,
|
|
460
|
+
},
|
|
461
|
+
now,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
// Mixed-version readers may see the durable WAITING status before the
|
|
465
|
+
// scheduler's typed wait projection is available. Do not let a stale
|
|
466
|
+
// running step overwrite that stronger lifecycle fact.
|
|
467
|
+
if (normalizedStatus === 'waiting') {
|
|
468
|
+
return projection(
|
|
469
|
+
{
|
|
470
|
+
schemaVersion: 1,
|
|
471
|
+
activityId: 'run:legacy-wait',
|
|
472
|
+
target: {
|
|
473
|
+
kind: 'step',
|
|
474
|
+
stepId: input.activeNodeId ?? 'run',
|
|
475
|
+
label: input.playName ?? 'play',
|
|
476
|
+
},
|
|
477
|
+
state: { kind: 'waiting', reason: { kind: 'unknown' } },
|
|
478
|
+
observedAt,
|
|
479
|
+
},
|
|
480
|
+
now,
|
|
481
|
+
false,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const pendingDataset = [...(input.datasets ?? [])]
|
|
486
|
+
.filter(
|
|
487
|
+
(dataset) =>
|
|
488
|
+
dataset.phase === 'registered' ||
|
|
489
|
+
(dataset.complete !== true && dataset.phase !== 'failed'),
|
|
490
|
+
)
|
|
491
|
+
.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
|
|
492
|
+
if (pendingDataset) {
|
|
493
|
+
return projection(
|
|
494
|
+
{
|
|
495
|
+
schemaVersion: 1,
|
|
496
|
+
activityId: `dataset:${pendingDataset.datasetId ?? pendingDataset.tableNamespace}`,
|
|
497
|
+
target: {
|
|
498
|
+
kind: 'dataset',
|
|
499
|
+
tableNamespace: pendingDataset.tableNamespace,
|
|
500
|
+
operation: 'materializing',
|
|
501
|
+
},
|
|
502
|
+
state: {
|
|
503
|
+
kind: 'active',
|
|
504
|
+
progress: {
|
|
505
|
+
completed: pendingDataset.persistedRows,
|
|
506
|
+
total:
|
|
507
|
+
typeof pendingDataset.succeededRows === 'number' ||
|
|
508
|
+
typeof pendingDataset.failedRows === 'number'
|
|
509
|
+
? (pendingDataset.succeededRows ?? 0) +
|
|
510
|
+
(pendingDataset.failedRows ?? 0)
|
|
511
|
+
: undefined,
|
|
512
|
+
failed: pendingDataset.failedRows,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
observedAt: pendingDataset.updatedAt ?? observedAt,
|
|
516
|
+
},
|
|
517
|
+
now,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const activeStep =
|
|
522
|
+
input.nodeStates?.find(
|
|
523
|
+
(candidate) => candidate.nodeId === input.activeNodeId,
|
|
524
|
+
) ??
|
|
525
|
+
input.nodeStates?.find((candidate) => candidate.status === 'running') ??
|
|
526
|
+
null;
|
|
527
|
+
if (activeStep) {
|
|
528
|
+
const toolId = activeStep.nodeId.startsWith('tool:')
|
|
529
|
+
? activeStep.nodeId.split(':').at(-1)?.trim() || null
|
|
530
|
+
: null;
|
|
531
|
+
const provider = toolId?.split(/[._]/)[0]?.trim() || null;
|
|
532
|
+
return projection(
|
|
533
|
+
{
|
|
534
|
+
schemaVersion: 1,
|
|
535
|
+
activityId: `step:${activeStep.nodeId}`,
|
|
536
|
+
stepId: activeStep.nodeId,
|
|
537
|
+
target:
|
|
538
|
+
toolId && provider
|
|
539
|
+
? {
|
|
540
|
+
kind: 'provider',
|
|
541
|
+
provider,
|
|
542
|
+
operation: toolId,
|
|
543
|
+
label: activeStep.label ?? toolId,
|
|
544
|
+
}
|
|
545
|
+
: {
|
|
546
|
+
kind: 'step',
|
|
547
|
+
stepId: activeStep.nodeId,
|
|
548
|
+
label: activeStep.label,
|
|
549
|
+
},
|
|
550
|
+
state: {
|
|
551
|
+
kind: 'active',
|
|
552
|
+
progress: activeStep.progress ?? undefined,
|
|
553
|
+
},
|
|
554
|
+
observedAt: activeStep.updatedAt ?? observedAt,
|
|
555
|
+
},
|
|
556
|
+
now,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (normalizedStatus === 'queued') {
|
|
561
|
+
return projection(
|
|
562
|
+
{
|
|
563
|
+
schemaVersion: 1,
|
|
564
|
+
activityId: 'capacity:worker',
|
|
565
|
+
target: { kind: 'capacity', pool: 'worker' },
|
|
566
|
+
state: { kind: 'queued', reason: 'capacity' },
|
|
567
|
+
observedAt,
|
|
568
|
+
},
|
|
569
|
+
now,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const fallback: PlayActivityObservation = {
|
|
574
|
+
schemaVersion: 1,
|
|
575
|
+
activityId: 'run:legacy',
|
|
576
|
+
target: {
|
|
577
|
+
kind: 'step',
|
|
578
|
+
stepId: input.activeNodeId ?? 'run',
|
|
579
|
+
label: input.playName ?? 'play',
|
|
580
|
+
},
|
|
581
|
+
state:
|
|
582
|
+
normalizedStatus === 'waiting'
|
|
583
|
+
? { kind: 'waiting', reason: { kind: 'unknown' } }
|
|
584
|
+
: { kind: 'active' },
|
|
585
|
+
observedAt,
|
|
586
|
+
};
|
|
587
|
+
return projection(fallback, now, normalizedStatus !== 'waiting');
|
|
588
|
+
}
|
|
@@ -5,6 +5,10 @@ import { pipeline } from 'node:stream/promises';
|
|
|
5
5
|
import type { PlaySheetContract } from '@shared_libs/plays/static-pipeline';
|
|
6
6
|
import type { PlayRowUpdate } from '@shared_libs/play-runtime/ctx-types';
|
|
7
7
|
import type { PlayRunTimelineEntry } from '@shared_libs/play-runtime/live-events';
|
|
8
|
+
import {
|
|
9
|
+
routePlayActivityObservation,
|
|
10
|
+
type PlayActivityObservation,
|
|
11
|
+
} from '@shared_libs/play-runtime/activity-observation';
|
|
8
12
|
import {
|
|
9
13
|
buildPlayRunLedgerEventsFromStatusPatch,
|
|
10
14
|
buildTerminalLogReplayEvents,
|
|
@@ -82,6 +86,8 @@ export type RuntimeStatusUpdate = {
|
|
|
82
86
|
complete?: boolean;
|
|
83
87
|
at: number;
|
|
84
88
|
}>;
|
|
89
|
+
/** Sparse typed activity facts. Routing/deduplication happens server-side. */
|
|
90
|
+
activityObservations?: PlayActivityObservation[];
|
|
85
91
|
/**
|
|
86
92
|
* Explicit terminal-output replay for final runner logs when the caller must
|
|
87
93
|
* keep `status` nonterminal. Direct-worker/Postgres progress finalizers do this so
|
|
@@ -1491,6 +1497,25 @@ async function updateRunStatusViaAppRuntimeUnlocked(
|
|
|
1491
1497
|
...(event.complete === undefined ? {} : { complete: event.complete }),
|
|
1492
1498
|
});
|
|
1493
1499
|
}
|
|
1500
|
+
const latestActivities = new Map(
|
|
1501
|
+
Object.entries(previousSnapshot.activitiesById),
|
|
1502
|
+
);
|
|
1503
|
+
for (const observation of update.activityObservations ?? []) {
|
|
1504
|
+
const previous = latestActivities.get(observation.activityId) ?? null;
|
|
1505
|
+
const lane = routePlayActivityObservation({
|
|
1506
|
+
previous,
|
|
1507
|
+
next: observation,
|
|
1508
|
+
});
|
|
1509
|
+
if (lane === 'drop' || lane === 'liveness') continue;
|
|
1510
|
+
events.push({
|
|
1511
|
+
type: 'activity.observed',
|
|
1512
|
+
runId: update.playId,
|
|
1513
|
+
source: 'worker',
|
|
1514
|
+
occurredAt: observation.observedAt,
|
|
1515
|
+
observation,
|
|
1516
|
+
});
|
|
1517
|
+
latestActivities.set(observation.activityId, observation);
|
|
1518
|
+
}
|
|
1494
1519
|
const terminalLogEvents =
|
|
1495
1520
|
terminalLogReplay && terminalLogReplay.lines.length > 0
|
|
1496
1521
|
? buildTerminalLogReplayEvents({
|