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.
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +131 -7
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +25 -8
- package/dist/bundling-sources/apps/play-runner-workers/src/durable-object-deploy-handoff.ts +8 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +379 -57
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/batching.ts +42 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +112 -1
- package/dist/bundling-sources/sdk/src/client.ts +237 -0
- package/dist/bundling-sources/sdk/src/http.ts +100 -9
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +58 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +99 -25
- package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +7 -2
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-salvage.ts +158 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +44 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +6 -0
- package/dist/cli/index.js +338 -13
- package/dist/cli/index.mjs +338 -13
- package/dist/index.d.mts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +229 -9
- package/dist/index.mjs +229 -9
- package/package.json +1 -1
|
@@ -44,7 +44,11 @@ import type {
|
|
|
44
44
|
PlayRuntimeManifest,
|
|
45
45
|
PlayRuntimeManifestMap,
|
|
46
46
|
} from '../../../shared_libs/plays/compiler-manifest';
|
|
47
|
-
import {
|
|
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
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
973
|
-
|
|
974
|
-
|
|
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
|
|
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
|
}
|