deepline 0.1.179 → 0.1.180

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.
@@ -44,7 +44,11 @@ import type {
44
44
  PlayRuntimeManifest,
45
45
  PlayRuntimeManifestMap,
46
46
  } from '../../../shared_libs/plays/compiler-manifest';
47
- import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
47
+ import {
48
+ WORKSPACE_STORAGE_NOT_READY_CODE,
49
+ WORKSPACE_STORAGE_NOT_READY_MESSAGE,
50
+ normalizePlayRunFailure,
51
+ } from '../../../shared_libs/play-runtime/run-failure';
48
52
  import type { PlayRunLedgerEvent } from '../../../shared_libs/play-runtime/run-ledger';
49
53
  import {
50
54
  COORDINATOR_INTERNAL_TOKEN_HEADER,
@@ -3152,11 +3156,59 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
3152
3156
  });
3153
3157
  }
3154
3158
  let dispatchedEvent = event;
3155
- dispatchedEvent = await hydrateWorkflowDbSessions({
3156
- env: this.env,
3157
- event: dispatchedEvent,
3158
- trace,
3159
- });
3159
+ try {
3160
+ dispatchedEvent = await hydrateWorkflowDbSessions({
3161
+ env: this.env,
3162
+ event: dispatchedEvent,
3163
+ trace,
3164
+ });
3165
+ } catch (hydrateError) {
3166
+ // The workflow re-entered but cannot read its externalized DB sessions.
3167
+ // A 410 (expired retention window) is known-permanent: the sessions are
3168
+ // stored once at submit and never re-minted here, so the workflow can
3169
+ // only keep failing. Publish a clean run.failed instead of letting the
3170
+ // raw transport error escape uncaught — this call sits OUTSIDE the
3171
+ // dispatchWorkflow try/catch that authors run.failed.
3172
+ //
3173
+ // A 404 that survived the read-retry ladder (`workflow db session
3174
+ // lookup failed 404`) rethrows WITHOUT marking, like every other error:
3175
+ // sessions are persisted (awaited) before the instance is created, so a
3176
+ // 404 here is anomalous — most plausibly a framework re-entry against an
3177
+ // ALREADY-TERMINAL run whose DO state the post-finish alarm cleaned up.
3178
+ // Authoring run.failed there could flip a completed (billed + durable)
3179
+ // run to failed via newest-terminal-wins. Only the 410 is safe to mark.
3180
+ const hydrateMessage =
3181
+ hydrateError instanceof Error
3182
+ ? hydrateError.message
3183
+ : String(hydrateError);
3184
+ if (hydrateMessage.includes('workflow db session lookup failed 410')) {
3185
+ await markWorkflowRuntimeFailure({
3186
+ env: this.env,
3187
+ event: dispatchedEvent,
3188
+ error: hydrateError,
3189
+ }).catch((markError) => {
3190
+ console.error(
3191
+ '[coordinator] failed to forward expired-db-session run failure',
3192
+ {
3193
+ runId: entryTrace.runId,
3194
+ message:
3195
+ markError instanceof Error
3196
+ ? markError.message
3197
+ : String(markError),
3198
+ },
3199
+ );
3200
+ });
3201
+ if (entryTrace.runId) {
3202
+ await writeCoordinatorTerminalState(this.env, {
3203
+ runId: entryTrace.runId,
3204
+ status: 'failed',
3205
+ error: normalizeCoordinatorWorkflowRuntimeFailure(hydrateError)
3206
+ .message,
3207
+ }).catch(() => undefined);
3208
+ }
3209
+ }
3210
+ throw hydrateError;
3211
+ }
3160
3212
  const dispatchTrace = readWorkflowTraceContext(dispatchedEvent);
3161
3213
  trace({
3162
3214
  runId: dispatchTrace.runId,
@@ -3685,6 +3737,35 @@ function formatTailLogPart(value: unknown): string {
3685
3737
  }
3686
3738
  }
3687
3739
 
3740
+ /**
3741
+ * Recognize the tenant-Postgres degradation signatures that used to escape the
3742
+ * coordinator submit path as an opaque 500 "internal error" with no repair
3743
+ * instruction: revoked CONNECT / grant drift, connect stalls, and NeonDbError.
3744
+ * Conservative on purpose — anything not matched stays a generic internal
3745
+ * error (with its phase) so we do not mislabel unrelated failures.
3746
+ */
3747
+ function isTenantStorageDegradationError(error: unknown): boolean {
3748
+ const message = error instanceof Error ? error.message : String(error);
3749
+ return (
3750
+ /permission denied (?:for|to grant) (?:schema|table|relation|database|sequence|role)\b/i.test(
3751
+ message,
3752
+ ) ||
3753
+ /must be owner of (?:table|relation|sequence|schema|database|function|procedure|routine|type)\b/i.test(
3754
+ message,
3755
+ ) ||
3756
+ /Runtime Postgres connection timed out/i.test(message) ||
3757
+ /\bNeonDbError\b/i.test(message) ||
3758
+ /timeout exceeded when trying to connect/i.test(message) ||
3759
+ /connection terminated unexpectedly/i.test(message)
3760
+ // Deliberately NOT matching bare socket/DNS codes (ECONNRESET, ETIMEDOUT,
3761
+ // ECONNREFUSED, EAI_AGAIN): those are thrown by Cloudflare service bindings,
3762
+ // the Workflow API, and Durable Object fetches too, not just tenant
3763
+ // Postgres. Matching them here would mislabel a transient CF-infra blip as
3764
+ // WORKSPACE_STORAGE_NOT_READY ("run db repair"). Every remaining pattern
3765
+ // carries explicit Postgres/Neon context.
3766
+ );
3767
+ }
3768
+
3688
3769
  function coordinatorWorkflowRouteErrorResponse(input: {
3689
3770
  runId: string;
3690
3771
  action: string;
@@ -3692,17 +3773,46 @@ function coordinatorWorkflowRouteErrorResponse(input: {
3692
3773
  }): Response {
3693
3774
  const message =
3694
3775
  input.error instanceof Error ? input.error.message : String(input.error);
3776
+ const phase = `coordinator.${input.action || 'unknown'}`;
3695
3777
  console.error('[coordinator.workflow_route.error]', {
3696
3778
  runId: input.runId,
3697
3779
  action: input.action,
3698
3780
  error: message,
3699
3781
  });
3782
+ if (input.action === 'submit' && isTenantStorageDegradationError(input.error)) {
3783
+ // De-redact: a tenant-storage failure gets the loud, actionable
3784
+ // WORKSPACE_STORAGE_NOT_READY message + repair instruction instead of the
3785
+ // generic "internal error". Same contract as the runtime route.
3786
+ //
3787
+ // Scoped to `submit` on purpose: the message states "nothing was billed",
3788
+ // which is only true in the pre-run submit phase. The other workflow
3789
+ // actions (status/result/tail/cancel/signal) run against an already-active,
3790
+ // possibly-billing run, so surfacing "storage not ready, nothing billed,
3791
+ // run db repair" there would be false and alarming on a healthy run.
3792
+ console.error('[coordinator.workflow_route.storage_not_ready]', {
3793
+ runId: input.runId,
3794
+ action: input.action,
3795
+ phase,
3796
+ error: message,
3797
+ });
3798
+ return Response.json(
3799
+ {
3800
+ error: {
3801
+ code: WORKSPACE_STORAGE_NOT_READY_CODE,
3802
+ message: WORKSPACE_STORAGE_NOT_READY_MESSAGE,
3803
+ phase,
3804
+ runId: input.runId,
3805
+ },
3806
+ },
3807
+ { status: 503 },
3808
+ );
3809
+ }
3700
3810
  return Response.json(
3701
3811
  {
3702
3812
  error: {
3703
3813
  code: 'COORDINATOR_WORKFLOW_ROUTE_FAILED',
3704
3814
  message,
3705
- phase: `coordinator.${input.action || 'unknown'}`,
3815
+ phase,
3706
3816
  runId: input.runId,
3707
3817
  },
3708
3818
  },
@@ -3869,6 +3979,7 @@ async function handleWorkflowRoute(input: {
3869
3979
  });
3870
3980
  })
3871
3981
  .catch((error: unknown) => {
3982
+ const storageNotReady = isTenantStorageDegradationError(error);
3872
3983
  recordSubmitTiming({
3873
3984
  phase: 'coordinator.harness_prewarm_postgres',
3874
3985
  ms: Date.now() - prewarmStartedAt,
@@ -3877,8 +3988,21 @@ async function handleWorkflowRoute(input: {
3877
3988
  status: 'failed',
3878
3989
  sessions: preloadedDbSessions.length,
3879
3990
  error: error instanceof Error ? error.message : String(error),
3991
+ storageNotReady,
3880
3992
  },
3881
3993
  });
3994
+ // The prewarm runs in waitUntil, so it cannot fail the already-issued
3995
+ // submit. But its cause used to be fully redacted in coordinator
3996
+ // logs. Log the tenant-storage classification loudly so the real
3997
+ // cause (and the WORKSPACE_STORAGE_NOT_READY repair path) is visible.
3998
+ if (storageNotReady) {
3999
+ console.error('[coordinator.harness_prewarm_postgres.storage_not_ready]', {
4000
+ runId: submittedRunId,
4001
+ playName: params.playName,
4002
+ code: WORKSPACE_STORAGE_NOT_READY_CODE,
4003
+ error: error instanceof Error ? error.message : String(error),
4004
+ });
4005
+ }
3882
4006
  });
3883
4007
  input.ctx?.waitUntil(prewarmPromise);
3884
4008
  }
@@ -17,7 +17,10 @@
17
17
  */
18
18
 
19
19
  import { sanitizeLiveLogLines } from './runtime/live-progress';
20
- import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
20
+ import {
21
+ DB_SESSION_MAX_TTL_SECONDS,
22
+ type PreloadedRuntimeDbSession,
23
+ } from '../../../shared_libs/play-runtime/db-session';
21
24
 
22
25
  type DurableObjectStorage = {
23
26
  get<T>(key: string): Promise<T | undefined>;
@@ -259,7 +262,12 @@ const COORDINATOR_RUN_EVENT_MAX_ENTRIES = 500;
259
262
  const FINISH_ALARM_DELAY_MS = 60_000; // self-evict 1 min after finish() called
260
263
  const WORKFLOW_RUN_STATE_TTL_MS = 60 * 60 * 1000;
261
264
  const WORKFLOW_RUN_RETRY_STATE_MAX_BYTES = 110_000;
262
- const WORKFLOW_DB_SESSIONS_TTL_MS = 10 * 60_000;
265
+ // Invariant: DB sessions must not expire before the workflow run/retry state.
266
+ // hydrateWorkflowDbSessions re-reads sessions on every workflow re-entry, so a
267
+ // shorter session TTL used to kill any run that ran longer than the session
268
+ // window (10 min) with a misleading transport 410.
269
+ const WORKFLOW_DB_SESSIONS_TTL_MS = WORKFLOW_RUN_STATE_TTL_MS;
270
+ const WORKFLOW_DB_SESSIONS_MAX_TTL_MS = DB_SESSION_MAX_TTL_SECONDS * 1000;
263
271
 
264
272
  function jsonByteLength(value: unknown): number {
265
273
  return new TextEncoder().encode(JSON.stringify(value)).byteLength;
@@ -673,7 +681,7 @@ export class PlayDedup implements DurableObject {
673
681
  body.dbSessionsTtlMs > 0
674
682
  ? Math.min(
675
683
  Math.max(Math.floor(body.dbSessionsTtlMs), 60_000),
676
- 30 * 60_000,
684
+ WORKFLOW_DB_SESSIONS_MAX_TTL_MS,
677
685
  )
678
686
  : WORKFLOW_DB_SESSIONS_TTL_MS;
679
687
  const retryKey = `${WORKFLOW_RUN_RETRY_KEY_PREFIX}${runId}`;
@@ -933,7 +941,10 @@ export class PlayDedup implements DurableObject {
933
941
  typeof body?.ttlMs === 'number' &&
934
942
  Number.isFinite(body.ttlMs) &&
935
943
  body.ttlMs > 0
936
- ? Math.min(Math.max(Math.floor(body.ttlMs), 60_000), 30 * 60_000)
944
+ ? Math.min(
945
+ Math.max(Math.floor(body.ttlMs), 60_000),
946
+ WORKFLOW_DB_SESSIONS_MAX_TTL_MS,
947
+ )
937
948
  : WORKFLOW_DB_SESSIONS_TTL_MS;
938
949
  const state: WorkflowDbSessionsState = {
939
950
  runId,
@@ -969,10 +980,16 @@ export class PlayDedup implements DurableObject {
969
980
  }
970
981
  if (state.expiresAt <= Date.now()) {
971
982
  await this.state.storage.delete(WORKFLOW_DB_SESSIONS_KEY);
972
- return new Response(JSON.stringify({ error: 'db sessions expired' }), {
973
- status: 410,
974
- headers: { 'content-type': 'application/json' },
975
- });
983
+ return new Response(
984
+ JSON.stringify({
985
+ error:
986
+ 'workflow db sessions expired: the run exceeded the session retention window before completing. This usually follows an earlier runtime persistence failure or a stalled run; re-run to resume.',
987
+ }),
988
+ {
989
+ status: 410,
990
+ headers: { 'content-type': 'application/json' },
991
+ },
992
+ );
976
993
  }
977
994
  assertEncryptedDbSessionsForStorage(state.sessions);
978
995
  return new Response(
@@ -1,13 +1,19 @@
1
1
  export const DURABLE_OBJECT_DEPLOY_HANDOFF_STORAGE_ERROR =
2
2
  "The Durable Object's code has been updated, this version can no longer access storage";
3
3
 
4
- export const DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS = [50, 150, 350];
4
+ export const DURABLE_OBJECT_DEPLOY_HANDOFF_WORKER_NOT_FOUND_ERROR =
5
+ 'Worker not found.';
6
+
7
+ export const DURABLE_OBJECT_DEPLOY_HANDOFF_RETRY_DELAYS_MS = [
8
+ 250, 750, 1_500, 3_000, 5_000, 8_000,
9
+ ];
5
10
 
6
11
  export function durableObjectDeployHandoffMessage(
7
12
  value: unknown,
8
13
  ): string | null {
9
14
  const message = value instanceof Error ? value.message : String(value ?? '');
10
- return message.includes(DURABLE_OBJECT_DEPLOY_HANDOFF_STORAGE_ERROR)
15
+ return message.includes(DURABLE_OBJECT_DEPLOY_HANDOFF_STORAGE_ERROR) ||
16
+ message.includes(DURABLE_OBJECT_DEPLOY_HANDOFF_WORKER_NOT_FOUND_ERROR)
11
17
  ? message
12
18
  : null;
13
19
  }