deepline 0.1.101 → 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/index.js CHANGED
@@ -261,10 +261,14 @@ var SDK_RELEASE = {
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
- version: "0.1.101",
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",
265
269
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
266
270
  supportPolicy: {
267
- latest: "0.1.101",
271
+ latest: "0.1.102",
268
272
  minimumSupported: "0.1.53",
269
273
  deprecatedBelow: "0.1.53"
270
274
  }
@@ -415,6 +419,22 @@ var HttpClient = class {
415
419
  parsed = body;
416
420
  }
417
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
+ }
418
438
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
419
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}`;
420
440
  throw new DeeplineError(msg, response.status, "API_ERROR", {
@@ -475,6 +495,22 @@ var HttpClient = class {
475
495
  }
476
496
  if (!response.ok) {
477
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
+ }
478
514
  const parsed = parseResponseBody(body);
479
515
  throw new DeeplineError(
480
516
  apiErrorMessage(parsed, response.status),
@@ -540,6 +576,31 @@ function parseResponseBody(body) {
540
576
  return body;
541
577
  }
542
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
+ }
543
604
  function apiErrorMessage(parsed, status) {
544
605
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
545
606
  if (typeof errorValue === "string") {
@@ -635,7 +696,7 @@ function isTransientPlayStreamError(error) {
635
696
  return error.statusCode >= 500 && error.statusCode < 600;
636
697
  }
637
698
  const text = error instanceof Error ? error.message : String(error);
638
- 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(
639
700
  text
640
701
  );
641
702
  }
@@ -867,6 +928,10 @@ function buildSnapshotFromLedger(snapshot) {
867
928
  return {
868
929
  runId: snapshot.runId,
869
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,
870
935
  updatedAt: snapshot.updatedAt ?? snapshot.finishedAt ?? snapshot.startedAt ?? null,
871
936
  logs: snapshot.logTail,
872
937
  totalLogCount: snapshot.totalLogCount,
package/dist/index.mjs CHANGED
@@ -183,10 +183,14 @@ var SDK_RELEASE = {
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
- version: "0.1.101",
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",
187
191
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
188
192
  supportPolicy: {
189
- latest: "0.1.101",
193
+ latest: "0.1.102",
190
194
  minimumSupported: "0.1.53",
191
195
  deprecatedBelow: "0.1.53"
192
196
  }
@@ -337,6 +341,22 @@ var HttpClient = class {
337
341
  parsed = body;
338
342
  }
339
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
+ }
340
360
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
341
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}`;
342
362
  throw new DeeplineError(msg, response.status, "API_ERROR", {
@@ -397,6 +417,22 @@ var HttpClient = class {
397
417
  }
398
418
  if (!response.ok) {
399
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
+ }
400
436
  const parsed = parseResponseBody(body);
401
437
  throw new DeeplineError(
402
438
  apiErrorMessage(parsed, response.status),
@@ -462,6 +498,31 @@ function parseResponseBody(body) {
462
498
  return body;
463
499
  }
464
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
+ }
465
526
  function apiErrorMessage(parsed, status) {
466
527
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
467
528
  if (typeof errorValue === "string") {
@@ -557,7 +618,7 @@ function isTransientPlayStreamError(error) {
557
618
  return error.statusCode >= 500 && error.statusCode < 600;
558
619
  }
559
620
  const text = error instanceof Error ? error.message : String(error);
560
- 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(
561
622
  text
562
623
  );
563
624
  }
@@ -789,6 +850,10 @@ function buildSnapshotFromLedger(snapshot) {
789
850
  return {
790
851
  runId: snapshot.runId,
791
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,
792
857
  updatedAt: snapshot.updatedAt ?? snapshot.finishedAt ?? snapshot.startedAt ?? null,
793
858
  logs: snapshot.logTail,
794
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
- const url = new URL(request.url);
3034
- if (url.pathname === '/health') {
3035
- return new Response('ok', { status: 200 });
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
- if (url.pathname === '/warmup/submit') {
3038
- const authError = authorizeCoordinatorControlRequest({ request, env });
3039
- if (authError) return authError;
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
- if (url.pathname === '/tail-log-token/probe') {
3043
- const authError = authorizeCoordinatorControlRequest({ request, env });
3044
- if (authError) return authError;
3045
- const expectedTailLogToken = env.DEEPLINE_TAIL_LOG_TOKEN?.trim();
3046
- if (!expectedTailLogToken) {
3047
- return Response.json(
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
- if (url.pathname === '/staged-files/put') {
3066
- const authError = authorizeCoordinatorControlRequest({ request, env });
3067
- if (authError) return authError;
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
- // Workflow routes: /workflow/{runId}/{action}
3071
- const wfMatch = url.pathname.match(/^\/workflow\/([^/]+)(?:\/(.+))?$/);
3072
- if (wfMatch) {
3073
- const runId = decodeURIComponent(wfMatch[1]!);
3074
- const action = wfMatch[2] ?? '';
3075
- const authError = authorizeCoordinatorControlRequest({
3076
- request,
3077
- env,
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
- requireRunScope: isWorkflowMutatingAction(action),
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
- // Dedup routes: /dedup/{runId}/{action}
3086
- const dedupMatch = url.pathname.match(/^\/dedup\/([^/]+)(?:\/(.+))?$/);
3087
- if (dedupMatch) {
3088
- const runId = decodeURIComponent(dedupMatch[1]!);
3089
- const action = dedupMatch[2] ?? '';
3090
- const authError = authorizeCoordinatorControlRequest({
3091
- request,
3092
- env,
3093
- runId,
3094
- requireRunScope: request.method !== 'GET' && request.method !== 'HEAD',
3095
- });
3096
- if (authError) return authError;
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
- return new Response('not found', { status: 404 });
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/