deepline 0.1.100 → 0.1.102
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/cli/index.js +434 -20
- package/dist/cli/index.mjs +437 -22
- package/dist/index.js +69 -3
- package/dist/index.mjs +69 -3
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +209 -68
- package/dist/repo/apps/play-runner-workers/src/entry.ts +141 -33
- package/dist/repo/sdk/src/http.ts +89 -0
- package/dist/repo/sdk/src/release.ts +7 -2
- package/dist/repo/sdk/src/stream-reconnect.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +204 -0
- package/dist/repo/shared_libs/play-runtime/run-ledger.ts +7 -2
- package/dist/repo/shared_libs/play-runtime/run-snapshot-stream.ts +8 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -260,10 +260,15 @@ var SDK_RELEASE = {
|
|
|
260
260
|
// 0.1.94 is claimed by PR #1527 — this watch-render fix ships as 0.1.95.
|
|
261
261
|
// 0.1.98 ships the duplicate-browser-tab fix (default-browser detection).
|
|
262
262
|
// 0.1.99 ships prebuilt job-change source-column preservation and validation fixes.
|
|
263
|
-
|
|
263
|
+
// 0.1.101 ships retryable play artifact publish failures and CI retry hardening.
|
|
264
|
+
// 0.1.102 ships the job-change ledger fixes: recovered-dataset export on
|
|
265
|
+
// failed runs, persisted/succeeded/failed row counts, strict local CSV
|
|
266
|
+
// preflight (existence, data rows, quotes, duplicate headers), HTML error
|
|
267
|
+
// scrubbing, and word-boundary watch truncation.
|
|
268
|
+
version: "0.1.102",
|
|
264
269
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
265
270
|
supportPolicy: {
|
|
266
|
-
latest: "0.1.
|
|
271
|
+
latest: "0.1.102",
|
|
267
272
|
minimumSupported: "0.1.53",
|
|
268
273
|
deprecatedBelow: "0.1.53"
|
|
269
274
|
}
|
|
@@ -414,6 +419,22 @@ var HttpClient = class {
|
|
|
414
419
|
parsed = body;
|
|
415
420
|
}
|
|
416
421
|
if (!response.ok) {
|
|
422
|
+
const htmlError = detectHtmlErrorBody(
|
|
423
|
+
body,
|
|
424
|
+
response.headers.get("content-type")
|
|
425
|
+
);
|
|
426
|
+
if (htmlError) {
|
|
427
|
+
throw new DeeplineError(
|
|
428
|
+
htmlError.message(response.status),
|
|
429
|
+
response.status,
|
|
430
|
+
"API_ERROR",
|
|
431
|
+
{
|
|
432
|
+
htmlErrorPage: true,
|
|
433
|
+
...htmlError.title ? { title: htmlError.title } : {},
|
|
434
|
+
...htmlError.workerThrewException ? { workerThrewException: true } : {}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
}
|
|
417
438
|
const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
|
|
418
439
|
const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
|
|
419
440
|
throw new DeeplineError(msg, response.status, "API_ERROR", {
|
|
@@ -474,6 +495,22 @@ var HttpClient = class {
|
|
|
474
495
|
}
|
|
475
496
|
if (!response.ok) {
|
|
476
497
|
const body = await response.text();
|
|
498
|
+
const htmlError = detectHtmlErrorBody(
|
|
499
|
+
body,
|
|
500
|
+
response.headers.get("content-type")
|
|
501
|
+
);
|
|
502
|
+
if (htmlError) {
|
|
503
|
+
throw new DeeplineError(
|
|
504
|
+
htmlError.message(response.status),
|
|
505
|
+
response.status,
|
|
506
|
+
"API_ERROR",
|
|
507
|
+
{
|
|
508
|
+
htmlErrorPage: true,
|
|
509
|
+
...htmlError.title ? { title: htmlError.title } : {},
|
|
510
|
+
...htmlError.workerThrewException ? { workerThrewException: true } : {}
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
}
|
|
477
514
|
const parsed = parseResponseBody(body);
|
|
478
515
|
throw new DeeplineError(
|
|
479
516
|
apiErrorMessage(parsed, response.status),
|
|
@@ -539,6 +576,31 @@ function parseResponseBody(body) {
|
|
|
539
576
|
return body;
|
|
540
577
|
}
|
|
541
578
|
}
|
|
579
|
+
function detectHtmlErrorBody(body, contentType) {
|
|
580
|
+
const trimmed = body.trim();
|
|
581
|
+
const lower = trimmed.toLowerCase();
|
|
582
|
+
const isHtml = (contentType ?? "").toLowerCase().includes("text/html") || lower.startsWith("<!doctype") || lower.startsWith("<html");
|
|
583
|
+
if (!isHtml) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
const titleMatch = trimmed.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
587
|
+
const title = titleMatch?.[1]?.replace(/\s+/g, " ").trim() || void 0;
|
|
588
|
+
const workerThrewException = /worker threw exception/i.test(trimmed);
|
|
589
|
+
return {
|
|
590
|
+
title,
|
|
591
|
+
workerThrewException,
|
|
592
|
+
message: (status) => {
|
|
593
|
+
const segments = [`HTTP ${status}`];
|
|
594
|
+
if (workerThrewException) {
|
|
595
|
+
segments.push("Worker threw exception");
|
|
596
|
+
}
|
|
597
|
+
if (title) {
|
|
598
|
+
segments.push(title);
|
|
599
|
+
}
|
|
600
|
+
return `${segments.join(": ")} (Cloudflare HTML error page suppressed)`;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
542
604
|
function apiErrorMessage(parsed, status) {
|
|
543
605
|
const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
|
|
544
606
|
if (typeof errorValue === "string") {
|
|
@@ -634,7 +696,7 @@ function isTransientPlayStreamError(error) {
|
|
|
634
696
|
return error.statusCode >= 500 && error.statusCode < 600;
|
|
635
697
|
}
|
|
636
698
|
const text = error instanceof Error ? error.message : String(error);
|
|
637
|
-
return /auth validation backend timed out|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
|
|
699
|
+
return /auth validation backend timed out|coordinator \/submit(?:\?[^ ]*)? 5\d\d|Worker threw exception|Internal Server Error|Service Unavailable|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
|
|
638
700
|
text
|
|
639
701
|
);
|
|
640
702
|
}
|
|
@@ -866,6 +928,10 @@ function buildSnapshotFromLedger(snapshot) {
|
|
|
866
928
|
return {
|
|
867
929
|
runId: snapshot.runId,
|
|
868
930
|
status: normalizePlayRunLiveStatus(snapshot.status),
|
|
931
|
+
createdAt: snapshot.createdAt ?? null,
|
|
932
|
+
startedAt: snapshot.startedAt ?? null,
|
|
933
|
+
finishedAt: snapshot.finishedAt ?? null,
|
|
934
|
+
durationMs: snapshot.durationMs ?? null,
|
|
869
935
|
updatedAt: snapshot.updatedAt ?? snapshot.finishedAt ?? snapshot.startedAt ?? null,
|
|
870
936
|
logs: snapshot.logTail,
|
|
871
937
|
totalLogCount: snapshot.totalLogCount,
|
package/dist/index.mjs
CHANGED
|
@@ -182,10 +182,15 @@ var SDK_RELEASE = {
|
|
|
182
182
|
// 0.1.94 is claimed by PR #1527 — this watch-render fix ships as 0.1.95.
|
|
183
183
|
// 0.1.98 ships the duplicate-browser-tab fix (default-browser detection).
|
|
184
184
|
// 0.1.99 ships prebuilt job-change source-column preservation and validation fixes.
|
|
185
|
-
|
|
185
|
+
// 0.1.101 ships retryable play artifact publish failures and CI retry hardening.
|
|
186
|
+
// 0.1.102 ships the job-change ledger fixes: recovered-dataset export on
|
|
187
|
+
// failed runs, persisted/succeeded/failed row counts, strict local CSV
|
|
188
|
+
// preflight (existence, data rows, quotes, duplicate headers), HTML error
|
|
189
|
+
// scrubbing, and word-boundary watch truncation.
|
|
190
|
+
version: "0.1.102",
|
|
186
191
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
187
192
|
supportPolicy: {
|
|
188
|
-
latest: "0.1.
|
|
193
|
+
latest: "0.1.102",
|
|
189
194
|
minimumSupported: "0.1.53",
|
|
190
195
|
deprecatedBelow: "0.1.53"
|
|
191
196
|
}
|
|
@@ -336,6 +341,22 @@ var HttpClient = class {
|
|
|
336
341
|
parsed = body;
|
|
337
342
|
}
|
|
338
343
|
if (!response.ok) {
|
|
344
|
+
const htmlError = detectHtmlErrorBody(
|
|
345
|
+
body,
|
|
346
|
+
response.headers.get("content-type")
|
|
347
|
+
);
|
|
348
|
+
if (htmlError) {
|
|
349
|
+
throw new DeeplineError(
|
|
350
|
+
htmlError.message(response.status),
|
|
351
|
+
response.status,
|
|
352
|
+
"API_ERROR",
|
|
353
|
+
{
|
|
354
|
+
htmlErrorPage: true,
|
|
355
|
+
...htmlError.title ? { title: htmlError.title } : {},
|
|
356
|
+
...htmlError.workerThrewException ? { workerThrewException: true } : {}
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
339
360
|
const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
|
|
340
361
|
const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
|
|
341
362
|
throw new DeeplineError(msg, response.status, "API_ERROR", {
|
|
@@ -396,6 +417,22 @@ var HttpClient = class {
|
|
|
396
417
|
}
|
|
397
418
|
if (!response.ok) {
|
|
398
419
|
const body = await response.text();
|
|
420
|
+
const htmlError = detectHtmlErrorBody(
|
|
421
|
+
body,
|
|
422
|
+
response.headers.get("content-type")
|
|
423
|
+
);
|
|
424
|
+
if (htmlError) {
|
|
425
|
+
throw new DeeplineError(
|
|
426
|
+
htmlError.message(response.status),
|
|
427
|
+
response.status,
|
|
428
|
+
"API_ERROR",
|
|
429
|
+
{
|
|
430
|
+
htmlErrorPage: true,
|
|
431
|
+
...htmlError.title ? { title: htmlError.title } : {},
|
|
432
|
+
...htmlError.workerThrewException ? { workerThrewException: true } : {}
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
}
|
|
399
436
|
const parsed = parseResponseBody(body);
|
|
400
437
|
throw new DeeplineError(
|
|
401
438
|
apiErrorMessage(parsed, response.status),
|
|
@@ -461,6 +498,31 @@ function parseResponseBody(body) {
|
|
|
461
498
|
return body;
|
|
462
499
|
}
|
|
463
500
|
}
|
|
501
|
+
function detectHtmlErrorBody(body, contentType) {
|
|
502
|
+
const trimmed = body.trim();
|
|
503
|
+
const lower = trimmed.toLowerCase();
|
|
504
|
+
const isHtml = (contentType ?? "").toLowerCase().includes("text/html") || lower.startsWith("<!doctype") || lower.startsWith("<html");
|
|
505
|
+
if (!isHtml) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const titleMatch = trimmed.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
509
|
+
const title = titleMatch?.[1]?.replace(/\s+/g, " ").trim() || void 0;
|
|
510
|
+
const workerThrewException = /worker threw exception/i.test(trimmed);
|
|
511
|
+
return {
|
|
512
|
+
title,
|
|
513
|
+
workerThrewException,
|
|
514
|
+
message: (status) => {
|
|
515
|
+
const segments = [`HTTP ${status}`];
|
|
516
|
+
if (workerThrewException) {
|
|
517
|
+
segments.push("Worker threw exception");
|
|
518
|
+
}
|
|
519
|
+
if (title) {
|
|
520
|
+
segments.push(title);
|
|
521
|
+
}
|
|
522
|
+
return `${segments.join(": ")} (Cloudflare HTML error page suppressed)`;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
464
526
|
function apiErrorMessage(parsed, status) {
|
|
465
527
|
const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
|
|
466
528
|
if (typeof errorValue === "string") {
|
|
@@ -556,7 +618,7 @@ function isTransientPlayStreamError(error) {
|
|
|
556
618
|
return error.statusCode >= 500 && error.statusCode < 600;
|
|
557
619
|
}
|
|
558
620
|
const text = error instanceof Error ? error.message : String(error);
|
|
559
|
-
return /auth validation backend timed out|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
|
|
621
|
+
return /auth validation backend timed out|coordinator \/submit(?:\?[^ ]*)? 5\d\d|Worker threw exception|Internal Server Error|Service Unavailable|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
|
|
560
622
|
text
|
|
561
623
|
);
|
|
562
624
|
}
|
|
@@ -788,6 +850,10 @@ function buildSnapshotFromLedger(snapshot) {
|
|
|
788
850
|
return {
|
|
789
851
|
runId: snapshot.runId,
|
|
790
852
|
status: normalizePlayRunLiveStatus(snapshot.status),
|
|
853
|
+
createdAt: snapshot.createdAt ?? null,
|
|
854
|
+
startedAt: snapshot.startedAt ?? null,
|
|
855
|
+
finishedAt: snapshot.finishedAt ?? null,
|
|
856
|
+
durationMs: snapshot.durationMs ?? null,
|
|
791
857
|
updatedAt: snapshot.updatedAt ?? snapshot.finishedAt ?? snapshot.startedAt ?? null,
|
|
792
858
|
logs: snapshot.logTail,
|
|
793
859
|
totalLogCount: snapshot.totalLogCount,
|
|
@@ -2854,6 +2854,21 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
2854
2854
|
instanceId: entryTrace.instanceId,
|
|
2855
2855
|
},
|
|
2856
2856
|
});
|
|
2857
|
+
if (entryTrace.runId) {
|
|
2858
|
+
this.ctx.waitUntil(
|
|
2859
|
+
appendCoordinatorRunEvent(this.env, {
|
|
2860
|
+
runId: entryTrace.runId,
|
|
2861
|
+
type: 'status',
|
|
2862
|
+
status: 'running',
|
|
2863
|
+
ts: Date.now(),
|
|
2864
|
+
}).catch((error) => {
|
|
2865
|
+
console.warn('[coordinator] workflow entry status event failed', {
|
|
2866
|
+
runId: entryTrace.runId,
|
|
2867
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2868
|
+
});
|
|
2869
|
+
}),
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2857
2872
|
if (entryTrace.submittedAt !== null) {
|
|
2858
2873
|
trace({
|
|
2859
2874
|
runId: entryTrace.runId,
|
|
@@ -3030,74 +3045,143 @@ const coordinatorEntrypoint = {
|
|
|
3030
3045
|
env: CoordinatorEnv,
|
|
3031
3046
|
ctx?: ExecutionContext,
|
|
3032
3047
|
): Promise<Response> {
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3048
|
+
// Top-level guard: no coordinator route may let an exception escape to
|
|
3049
|
+
// Cloudflare's default "Worker threw exception" HTML error page. Each
|
|
3050
|
+
// route group below additionally wraps itself for precise phase/runId
|
|
3051
|
+
// context; this outer catch is the backstop for URL parsing, route
|
|
3052
|
+
// matching, and any future route added without its own envelope.
|
|
3053
|
+
try {
|
|
3054
|
+
return await coordinatorRouteFetch(request, env, ctx);
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
return coordinatorRouteErrorResponse({
|
|
3057
|
+
logTag: '[coordinator.fetch.error]',
|
|
3058
|
+
code: 'COORDINATOR_ROUTE_FAILED',
|
|
3059
|
+
phase: 'coordinator.fetch',
|
|
3060
|
+
runId: null,
|
|
3061
|
+
error,
|
|
3062
|
+
});
|
|
3036
3063
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3064
|
+
},
|
|
3065
|
+
async tail(events: unknown[], env: CoordinatorEnv): Promise<void> {
|
|
3066
|
+
await flushTailRunLogs(events, env);
|
|
3067
|
+
},
|
|
3068
|
+
async scheduled(
|
|
3069
|
+
_controller: unknown,
|
|
3070
|
+
_env: CoordinatorEnv,
|
|
3071
|
+
_ctx?: ExecutionContext,
|
|
3072
|
+
): Promise<void> {},
|
|
3073
|
+
};
|
|
3074
|
+
|
|
3075
|
+
export default coordinatorEntrypoint;
|
|
3076
|
+
|
|
3077
|
+
/**
|
|
3078
|
+
* Route dispatcher for the coordinator fetch handler. Extracted as a standalone
|
|
3079
|
+
* function so the outer {@link coordinatorEntrypoint.fetch} try/catch backstop
|
|
3080
|
+
* is not coupled to `this` binding. Every route group wraps itself with a JSON
|
|
3081
|
+
* envelope; the outer catch covers URL parsing / route matching escapes.
|
|
3082
|
+
*/
|
|
3083
|
+
async function coordinatorRouteFetch(
|
|
3084
|
+
request: Request,
|
|
3085
|
+
env: CoordinatorEnv,
|
|
3086
|
+
ctx?: ExecutionContext,
|
|
3087
|
+
): Promise<Response> {
|
|
3088
|
+
const url = new URL(request.url);
|
|
3089
|
+
if (url.pathname === '/health') {
|
|
3090
|
+
return new Response('ok', { status: 200 });
|
|
3091
|
+
}
|
|
3092
|
+
if (url.pathname === '/warmup/submit') {
|
|
3093
|
+
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
3094
|
+
if (authError) return authError;
|
|
3095
|
+
try {
|
|
3040
3096
|
return await handleCoordinatorWarmup(request, env, ctx);
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
{ ok: false, error: 'tail log token is not configured' },
|
|
3049
|
-
{ status: 503 },
|
|
3050
|
-
);
|
|
3051
|
-
}
|
|
3052
|
-
const actualTailLogToken =
|
|
3053
|
-
request.headers.get('x-deepline-tail-log-token')?.trim() ?? '';
|
|
3054
|
-
if (actualTailLogToken !== expectedTailLogToken) {
|
|
3055
|
-
return Response.json(
|
|
3056
|
-
{ ok: false, error: 'tail log token mismatch' },
|
|
3057
|
-
{ status: 401 },
|
|
3058
|
-
);
|
|
3059
|
-
}
|
|
3060
|
-
return Response.json({
|
|
3061
|
-
ok: true,
|
|
3062
|
-
deployMarker: env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ?? null,
|
|
3097
|
+
} catch (error) {
|
|
3098
|
+
return coordinatorRouteErrorResponse({
|
|
3099
|
+
logTag: '[coordinator.warmup.error]',
|
|
3100
|
+
code: 'COORDINATOR_WARMUP_FAILED',
|
|
3101
|
+
phase: 'coordinator.warmup',
|
|
3102
|
+
runId: null,
|
|
3103
|
+
error,
|
|
3063
3104
|
});
|
|
3064
3105
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3106
|
+
}
|
|
3107
|
+
if (url.pathname === '/tail-log-token/probe') {
|
|
3108
|
+
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
3109
|
+
if (authError) return authError;
|
|
3110
|
+
const expectedTailLogToken = env.DEEPLINE_TAIL_LOG_TOKEN?.trim();
|
|
3111
|
+
if (!expectedTailLogToken) {
|
|
3112
|
+
return Response.json(
|
|
3113
|
+
{ ok: false, error: 'tail log token is not configured' },
|
|
3114
|
+
{ status: 503 },
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
const actualTailLogToken =
|
|
3118
|
+
request.headers.get('x-deepline-tail-log-token')?.trim() ?? '';
|
|
3119
|
+
if (actualTailLogToken !== expectedTailLogToken) {
|
|
3120
|
+
return Response.json(
|
|
3121
|
+
{ ok: false, error: 'tail log token mismatch' },
|
|
3122
|
+
{ status: 401 },
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
return Response.json({
|
|
3126
|
+
ok: true,
|
|
3127
|
+
deployMarker: env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ?? null,
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
if (url.pathname === '/staged-files/put') {
|
|
3131
|
+
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
3132
|
+
if (authError) return authError;
|
|
3133
|
+
try {
|
|
3068
3134
|
return await handleStagedFilePut(request, env);
|
|
3135
|
+
} catch (error) {
|
|
3136
|
+
return coordinatorRouteErrorResponse({
|
|
3137
|
+
logTag: '[coordinator.staged_file_put.error]',
|
|
3138
|
+
code: 'COORDINATOR_STAGED_FILE_PUT_FAILED',
|
|
3139
|
+
phase: 'coordinator.staged_files_put',
|
|
3140
|
+
runId: null,
|
|
3141
|
+
error,
|
|
3142
|
+
});
|
|
3069
3143
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3144
|
+
}
|
|
3145
|
+
// Workflow routes: /workflow/{runId}/{action}
|
|
3146
|
+
const wfMatch = url.pathname.match(/^\/workflow\/([^/]+)(?:\/(.+))?$/);
|
|
3147
|
+
if (wfMatch) {
|
|
3148
|
+
const runId = decodeURIComponent(wfMatch[1]!);
|
|
3149
|
+
const action = wfMatch[2] ?? '';
|
|
3150
|
+
const authError = authorizeCoordinatorControlRequest({
|
|
3151
|
+
request,
|
|
3152
|
+
env,
|
|
3153
|
+
runId,
|
|
3154
|
+
requireRunScope: isWorkflowMutatingAction(action),
|
|
3155
|
+
});
|
|
3156
|
+
if (authError) return authError;
|
|
3157
|
+
try {
|
|
3158
|
+
return await handleWorkflowRoute({ runId, action, request, env, ctx });
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
return coordinatorWorkflowRouteErrorResponse({
|
|
3078
3161
|
runId,
|
|
3079
|
-
|
|
3162
|
+
action,
|
|
3163
|
+
error,
|
|
3080
3164
|
});
|
|
3081
|
-
if (authError) return authError;
|
|
3082
|
-
return await handleWorkflowRoute({ runId, action, request, env, ctx });
|
|
3083
3165
|
}
|
|
3166
|
+
}
|
|
3084
3167
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3168
|
+
// Dedup routes: /dedup/{runId}/{action}
|
|
3169
|
+
const dedupMatch = url.pathname.match(/^\/dedup\/([^/]+)(?:\/(.+))?$/);
|
|
3170
|
+
if (dedupMatch) {
|
|
3171
|
+
const runId = decodeURIComponent(dedupMatch[1]!);
|
|
3172
|
+
const action = dedupMatch[2] ?? '';
|
|
3173
|
+
const authError = authorizeCoordinatorControlRequest({
|
|
3174
|
+
request,
|
|
3175
|
+
env,
|
|
3176
|
+
runId,
|
|
3177
|
+
requireRunScope: request.method !== 'GET' && request.method !== 'HEAD',
|
|
3178
|
+
});
|
|
3179
|
+
if (authError) return authError;
|
|
3180
|
+
try {
|
|
3097
3181
|
const doId = env.PLAY_DEDUP.idFromName(runId);
|
|
3098
3182
|
const stub = env.PLAY_DEDUP.get(doId);
|
|
3099
3183
|
const internalUrl = `https://internal/${action}`;
|
|
3100
|
-
return stub.fetch(internalUrl, {
|
|
3184
|
+
return await stub.fetch(internalUrl, {
|
|
3101
3185
|
method: request.method,
|
|
3102
3186
|
headers: request.headers,
|
|
3103
3187
|
body:
|
|
@@ -3105,21 +3189,19 @@ const coordinatorEntrypoint = {
|
|
|
3105
3189
|
? undefined
|
|
3106
3190
|
: request.body,
|
|
3107
3191
|
});
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
return coordinatorRouteErrorResponse({
|
|
3194
|
+
logTag: '[coordinator.dedup_route.error]',
|
|
3195
|
+
code: 'COORDINATOR_DEDUP_ROUTE_FAILED',
|
|
3196
|
+
phase: `coordinator.dedup.${action || 'unknown'}`,
|
|
3197
|
+
runId,
|
|
3198
|
+
error,
|
|
3199
|
+
});
|
|
3108
3200
|
}
|
|
3201
|
+
}
|
|
3109
3202
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
async tail(events: unknown[], env: CoordinatorEnv): Promise<void> {
|
|
3113
|
-
await flushTailRunLogs(events, env);
|
|
3114
|
-
},
|
|
3115
|
-
async scheduled(
|
|
3116
|
-
_controller: unknown,
|
|
3117
|
-
_env: CoordinatorEnv,
|
|
3118
|
-
_ctx?: ExecutionContext,
|
|
3119
|
-
): Promise<void> {},
|
|
3120
|
-
};
|
|
3121
|
-
|
|
3122
|
-
export default coordinatorEntrypoint;
|
|
3203
|
+
return new Response('not found', { status: 404 });
|
|
3204
|
+
}
|
|
3123
3205
|
|
|
3124
3206
|
const RUN_LOG_PREFIX_RE = /\[deepline-run:([^\]]+)\]\s*(.*)/;
|
|
3125
3207
|
const RUN_ID_RE = /\bplay\/[^/\s]+\/run\/[0-9a-zTt-]+/;
|
|
@@ -3211,6 +3293,65 @@ function formatTailLogPart(value: unknown): string {
|
|
|
3211
3293
|
}
|
|
3212
3294
|
}
|
|
3213
3295
|
|
|
3296
|
+
function coordinatorWorkflowRouteErrorResponse(input: {
|
|
3297
|
+
runId: string;
|
|
3298
|
+
action: string;
|
|
3299
|
+
error: unknown;
|
|
3300
|
+
}): Response {
|
|
3301
|
+
const message =
|
|
3302
|
+
input.error instanceof Error ? input.error.message : String(input.error);
|
|
3303
|
+
console.error('[coordinator.workflow_route.error]', {
|
|
3304
|
+
runId: input.runId,
|
|
3305
|
+
action: input.action,
|
|
3306
|
+
error: message,
|
|
3307
|
+
});
|
|
3308
|
+
return Response.json(
|
|
3309
|
+
{
|
|
3310
|
+
error: {
|
|
3311
|
+
code: 'COORDINATOR_WORKFLOW_ROUTE_FAILED',
|
|
3312
|
+
message,
|
|
3313
|
+
phase: `coordinator.${input.action || 'unknown'}`,
|
|
3314
|
+
runId: input.runId,
|
|
3315
|
+
},
|
|
3316
|
+
},
|
|
3317
|
+
{ status: 500 },
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
/**
|
|
3322
|
+
* Generic JSON envelope for any thrown error escaping a coordinator fetch route.
|
|
3323
|
+
* Mirrors {@link coordinatorWorkflowRouteErrorResponse} so that NO route group
|
|
3324
|
+
* in the fetch handler can leak Cloudflare's raw "Worker threw exception" HTML
|
|
3325
|
+
* page to clients/agents. Keep the envelope shape `{ error: { code, message,
|
|
3326
|
+
* phase, runId } }` and the console.error logging convention identical.
|
|
3327
|
+
*/
|
|
3328
|
+
function coordinatorRouteErrorResponse(input: {
|
|
3329
|
+
logTag: string;
|
|
3330
|
+
code: string;
|
|
3331
|
+
phase: string;
|
|
3332
|
+
runId: string | null;
|
|
3333
|
+
error: unknown;
|
|
3334
|
+
}): Response {
|
|
3335
|
+
const message =
|
|
3336
|
+
input.error instanceof Error ? input.error.message : String(input.error);
|
|
3337
|
+
console.error(input.logTag, {
|
|
3338
|
+
runId: input.runId,
|
|
3339
|
+
phase: input.phase,
|
|
3340
|
+
error: message,
|
|
3341
|
+
});
|
|
3342
|
+
return Response.json(
|
|
3343
|
+
{
|
|
3344
|
+
error: {
|
|
3345
|
+
code: input.code,
|
|
3346
|
+
message,
|
|
3347
|
+
phase: input.phase,
|
|
3348
|
+
runId: input.runId,
|
|
3349
|
+
},
|
|
3350
|
+
},
|
|
3351
|
+
{ status: 500 },
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3214
3355
|
// Operator-diagnostic console lines that carry the [deepline-run:] prefix but
|
|
3215
3356
|
// are not user-facing run output. The console scrape fans run-prefixed lines
|
|
3216
3357
|
// back into the run's durable Run Log Stream ('system' channel), so harness/
|