deepline 0.1.65 → 0.1.67

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.
@@ -17,6 +17,41 @@
17
17
  */
18
18
 
19
19
  import { sanitizeLiveLogLines } from './runtime/live-progress';
20
+ import type { PreloadedRuntimeDbSession } from '../../../shared_libs/play-runtime/db-session';
21
+
22
+ type DurableObjectStorage = {
23
+ get<T>(key: string): Promise<T | undefined>;
24
+ get<T>(keys: string[]): Promise<Map<string, T>>;
25
+ put<T>(key: string, value: T): Promise<void>;
26
+ put<T>(entries: Record<string, T>): Promise<void>;
27
+ delete(key: string | string[]): Promise<unknown>;
28
+ deleteAll(): Promise<void>;
29
+ list<T>(options?: { prefix?: string }): Promise<Map<string, T>>;
30
+ setAlarm(scheduledTime: number): Promise<void>;
31
+ };
32
+
33
+ type DurableObjectState = {
34
+ storage: DurableObjectStorage;
35
+ blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;
36
+ };
37
+
38
+ type DurableObject = {
39
+ fetch(request: Request): Promise<Response>;
40
+ alarm?(): Promise<void>;
41
+ };
42
+
43
+ type DurableObjectId = {
44
+ toString(): string;
45
+ };
46
+
47
+ type DurableObjectStub = {
48
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
49
+ };
50
+
51
+ type DurableObjectNamespace = {
52
+ idFromName(name: string): DurableObjectId;
53
+ get(id: DurableObjectId): DurableObjectStub;
54
+ };
20
55
 
21
56
  type DedupEntry =
22
57
  | { status: 'in_flight'; claimedBy: string; claimedAt: number }
@@ -83,6 +118,13 @@ type WorkflowRunRetryState = {
83
118
  expiresAt: number;
84
119
  };
85
120
 
121
+ type WorkflowDbSessionsState = {
122
+ runId: string;
123
+ sessions: PreloadedRuntimeDbSession[];
124
+ storedAt: number;
125
+ expiresAt: number;
126
+ };
127
+
86
128
  type CoordinatorTraceEntry = {
87
129
  ts: number;
88
130
  source: 'coordinator' | 'dynamic_worker';
@@ -166,6 +208,7 @@ const DEDUP_KEY_PREFIX = 'd:';
166
208
  const WORKFLOW_POOL_KEY_PREFIX = 'p:';
167
209
  const WORKFLOW_POOL_RUN_KEY_PREFIX = 'm:';
168
210
  const WORKFLOW_RUN_RETRY_KEY_PREFIX = 'r:';
211
+ const WORKFLOW_DB_SESSIONS_KEY = 'db-sessions';
169
212
  const COORDINATOR_TRACE_KEY_PREFIX = 't:';
170
213
  const COORDINATOR_RUN_EVENT_KEY_PREFIX = 'e:';
171
214
  const COORDINATOR_TERMINAL_KEY = 'terminal';
@@ -177,11 +220,27 @@ const WORKFLOW_POOL_DEFAULT_TTL_MS = 8 * 60 * 1000;
177
220
  const WORKFLOW_POOL_RUN_MAPPING_TTL_MS = 60 * 60 * 1000;
178
221
  const WORKFLOW_POOL_READY_MAX_AGE_MS = 7 * 60_000;
179
222
  const WORKFLOW_RUN_RETRY_STATE_MAX_BYTES = 110_000;
223
+ const WORKFLOW_DB_SESSIONS_TTL_MS = 10 * 60_000;
180
224
 
181
225
  function jsonByteLength(value: unknown): number {
182
226
  return new TextEncoder().encode(JSON.stringify(value)).byteLength;
183
227
  }
184
228
 
229
+ function assertEncryptedDbSessionsForStorage(
230
+ sessions: PreloadedRuntimeDbSession[],
231
+ ): void {
232
+ for (const session of sessions) {
233
+ if (session.session.postgresUrl) {
234
+ throw new Error('preloaded db session storage rejects raw Postgres URLs');
235
+ }
236
+ if (!session.session.encryptedPostgresUrl) {
237
+ throw new Error(
238
+ 'preloaded db session storage requires encrypted Postgres URLs',
239
+ );
240
+ }
241
+ }
242
+ }
243
+
185
244
  interface DedupEnv {
186
245
  PLAY_DEDUP: DurableObjectNamespace;
187
246
  }
@@ -252,6 +311,10 @@ export class PlayDedup implements DurableObject {
252
311
  return await this.handleRunRetryStatePut(req);
253
312
  case '/run-retry-claim':
254
313
  return await this.handleRunRetryClaim(req);
314
+ case '/db-sessions-put':
315
+ return await this.handleDbSessionsPut(req);
316
+ case '/db-sessions-get':
317
+ return await this.handleDbSessionsGet(req);
255
318
  case '/pool-clear':
256
319
  return await this.handlePoolClear(req);
257
320
  case '/trace-add':
@@ -1086,6 +1149,77 @@ export class PlayDedup implements DurableObject {
1086
1149
  });
1087
1150
  }
1088
1151
 
1152
+ private async handleDbSessionsPut(req: Request): Promise<Response> {
1153
+ const body = (await req.json().catch(() => null)) as {
1154
+ runId?: unknown;
1155
+ sessions?: unknown;
1156
+ ttlMs?: unknown;
1157
+ } | null;
1158
+ const runId = typeof body?.runId === 'string' ? body.runId : '';
1159
+ const sessions = Array.isArray(body?.sessions)
1160
+ ? (body.sessions as PreloadedRuntimeDbSession[])
1161
+ : null;
1162
+ if (!runId || !sessions) {
1163
+ return new Response('runId and sessions are required', { status: 400 });
1164
+ }
1165
+ assertEncryptedDbSessionsForStorage(sessions);
1166
+ const now = Date.now();
1167
+ const ttlMs =
1168
+ typeof body?.ttlMs === 'number' &&
1169
+ Number.isFinite(body.ttlMs) &&
1170
+ body.ttlMs > 0
1171
+ ? Math.min(Math.max(Math.floor(body.ttlMs), 60_000), 30 * 60_000)
1172
+ : WORKFLOW_DB_SESSIONS_TTL_MS;
1173
+ const state: WorkflowDbSessionsState = {
1174
+ runId,
1175
+ sessions,
1176
+ storedAt: now,
1177
+ expiresAt: now + ttlMs,
1178
+ };
1179
+ await this.state.storage.put(WORKFLOW_DB_SESSIONS_KEY, state);
1180
+ return new Response(
1181
+ JSON.stringify({
1182
+ ok: true,
1183
+ sessionCount: sessions.length,
1184
+ expiresAt: state.expiresAt,
1185
+ }),
1186
+ { headers: { 'content-type': 'application/json' } },
1187
+ );
1188
+ }
1189
+
1190
+ private async handleDbSessionsGet(req: Request): Promise<Response> {
1191
+ const url = new URL(req.url);
1192
+ const runId = url.searchParams.get('runId') ?? '';
1193
+ if (!runId) {
1194
+ return new Response('runId is required', { status: 400 });
1195
+ }
1196
+ const state = await this.state.storage.get<WorkflowDbSessionsState>(
1197
+ WORKFLOW_DB_SESSIONS_KEY,
1198
+ );
1199
+ if (!state || state.runId !== runId) {
1200
+ return new Response(JSON.stringify({ error: 'db sessions not found' }), {
1201
+ status: 404,
1202
+ headers: { 'content-type': 'application/json' },
1203
+ });
1204
+ }
1205
+ if (state.expiresAt <= Date.now()) {
1206
+ await this.state.storage.delete(WORKFLOW_DB_SESSIONS_KEY);
1207
+ return new Response(JSON.stringify({ error: 'db sessions expired' }), {
1208
+ status: 410,
1209
+ headers: { 'content-type': 'application/json' },
1210
+ });
1211
+ }
1212
+ assertEncryptedDbSessionsForStorage(state.sessions);
1213
+ return new Response(
1214
+ JSON.stringify({
1215
+ sessions: state.sessions,
1216
+ sessionCount: state.sessions.length,
1217
+ expiresAt: state.expiresAt,
1218
+ }),
1219
+ { headers: { 'content-type': 'application/json' } },
1220
+ );
1221
+ }
1222
+
1089
1223
  private async handlePoolClear(req: Request): Promise<Response> {
1090
1224
  const version = this.workflowPoolVersion(req);
1091
1225
  const [pool, mappings, retries] = await Promise.all([
@@ -86,6 +86,7 @@ import type {
86
86
  PlayRunLedgerStepProgress,
87
87
  PlayRunLedgerStepStatus,
88
88
  } from '../../../shared_libs/play-runtime/run-ledger';
89
+ import type { PlayHarnessRpc } from '../../play-harness-worker/src/rpc-types';
89
90
  import {
90
91
  createCsvDatasetHandle,
91
92
  createInlineDatasetHandle,
@@ -272,18 +273,11 @@ type WorkerEnv = {
272
273
  ): Promise<void>;
273
274
  };
274
275
  /**
275
- * Service binding to the long-lived Play Harness Worker
276
+ * Required service binding to the long-lived Play Harness Worker
276
277
  * (apps/play-harness-worker). Wired by the coordinator's WorkerLoader
277
- * factory — see apps/play-runner-workers/src/coordinator-entry.ts. The
278
- * binding's typed RPC surface is `PlayHarnessRpc`, defined in
279
- * apps/play-harness-worker/src/rpc-types.ts.
280
- *
281
- * Optional: if absent (older coordinator deploy that hasn't been
282
- * upgraded, or a dev environment running without the harness Worker),
283
- * SDK call sites that try to reach into the harness will throw a
284
- * loud error. Loud failures > silent fallbacks.
278
+ * factory — see apps/play-runner-workers/src/coordinator-entry.ts.
285
279
  */
286
- HARNESS?: import('../../play-harness-worker/src/rpc-types').PlayHarnessRpc;
280
+ HARNESS: PlayHarnessRpc;
287
281
  VERCEL_PROTECTION_BYPASS_TOKEN?: string;
288
282
  };
289
283
 
@@ -314,7 +308,18 @@ function captureCoordinatorBinding(env: WorkerEnv): void {
314
308
  * across plays.
315
309
  */
316
310
  function captureHarnessBinding(env: WorkerEnv): void {
317
- setHarnessBinding(env.HARNESS ?? null);
311
+ setHarnessBinding(requireHarnessBinding(env));
312
+ }
313
+
314
+ function requireHarnessBinding(env: WorkerEnv): PlayHarnessRpc {
315
+ const harness = env.HARNESS;
316
+ if (!harness) {
317
+ throw new Error(
318
+ 'Cloudflare play Workers require the HARNESS service binding. ' +
319
+ 'Start apps/play-harness-worker before the coordinator or fix wrangler.toml services.',
320
+ );
321
+ }
322
+ return harness;
318
323
  }
319
324
 
320
325
  /**
@@ -325,17 +330,12 @@ function captureHarnessBinding(env: WorkerEnv): void {
325
330
  * to plumb a separate health route or instrument SDK call sites.
326
331
  *
327
332
  * Behavior on fail:
328
- * - Binding missing entirelylog clearly that HARNESS is unwired so
329
- * anyone investigating cold-path perf knows the harness isn't in the
330
- * loop yet.
331
- * - Binding present but ping throws → log the error. Don't re-throw —
332
- * a probe failure must NEVER block the play; if a real call site
333
- * later needs HARNESS and the binding is broken, that call site's
334
- * own throw is the loud failure (see harness-stub → requireBinding).
333
+ * - Binding or ping failure fail the run. HARNESS is required in every
334
+ * deployed environment; no fallback should hide a miswired harness.
335
335
  *
336
336
  * This is intentionally idempotent and cheap (a single ping RPC,
337
337
  * deduplicated per isolate via the module-level guard) so the probe
338
- * itself never adds measurable latency to a run.
338
+ * itself never adds meaningful latency to a run.
339
339
  */
340
340
  let harnessProbeFiredForIsolate = false;
341
341
  async function probeHarnessOnce(
@@ -343,33 +343,16 @@ async function probeHarnessOnce(
343
343
  runPrefix: string,
344
344
  ): Promise<void> {
345
345
  if (harnessProbeFiredForIsolate) return;
346
- harnessProbeFiredForIsolate = true;
347
- if (!env.HARNESS) {
348
- console.log(
349
- `${runPrefix} [harness-probe] env.HARNESS unwired — coordinator did not pass the binding. ` +
350
- `Per-play SDK call sites that reach into the harness will throw clearly. ` +
351
- `See apps/play-harness-worker/README.md.`,
352
- );
353
- return;
354
- }
355
- if (
356
- typeof (env.HARNESS as { ping?: () => Promise<{ ok: true; ts: number }> })
357
- .ping !== 'function'
358
- ) {
359
- console.log(
360
- `${runPrefix} [harness-probe] env.HARNESS is present but does not expose ping(); ` +
361
- `continuing and relying on the first real call to fail if the contract changed.`,
362
- );
363
- return;
364
- }
346
+ const harness = requireHarnessBinding(env);
365
347
  try {
366
- const result = await env.HARNESS.ping();
348
+ const result = await harness.ping();
349
+ harnessProbeFiredForIsolate = true;
367
350
  console.log(
368
351
  `${runPrefix} [harness-probe] env.HARNESS connected ts=${result.ts}`,
369
352
  );
370
353
  } catch (error) {
371
354
  const message = error instanceof Error ? error.message : String(error);
372
- console.log(
355
+ throw new Error(
373
356
  `${runPrefix} [harness-probe] env.HARNESS resolved but ping failed: ${message}`,
374
357
  );
375
358
  }
@@ -3028,6 +3011,7 @@ async function persistCompletedMapRows(input: {
3028
3011
  await harnessPersistCompletedSheetRows({
3029
3012
  baseUrl: input.req.baseUrl,
3030
3013
  executorToken: input.req.executorToken,
3014
+ orgId: input.req.orgId,
3031
3015
  preloadedDbSessions: input.req.preloadedDbSessions ?? null,
3032
3016
  playName: input.req.playName,
3033
3017
  tableNamespace: input.tableNamespace,
@@ -3060,6 +3044,7 @@ async function prepareMapRows(input: {
3060
3044
  const result = await harnessStartSheetDataset({
3061
3045
  baseUrl: input.req.baseUrl,
3062
3046
  executorToken: input.req.executorToken,
3047
+ orgId: input.req.orgId,
3063
3048
  preloadedDbSessions: input.req.preloadedDbSessions ?? null,
3064
3049
  playName: input.req.playName,
3065
3050
  tableNamespace: input.tableNamespace,
@@ -3233,22 +3218,22 @@ function createMinimalWorkerCtx(
3233
3218
  };
3234
3219
  const rootGovernance = req.playCallGovernance;
3235
3220
  const rootRunId = rootGovernance?.rootRunId ?? req.runId;
3236
- const receiptStore = env.HARNESS
3237
- ? createHarnessWorkerReceiptStore({ executorToken: req.executorToken })
3238
- : undefined;
3221
+ const receiptStore = createHarnessWorkerReceiptStore({
3222
+ executorToken: req.executorToken,
3223
+ orgId: req.orgId,
3224
+ preloadedDbSessions: req.preloadedDbSessions ?? null,
3225
+ userEmail: req.userEmail,
3226
+ });
3239
3227
  const executeWithRuntimeReceipt = async <T>(
3240
3228
  key: string,
3241
3229
  execute: () => Promise<T> | T,
3242
3230
  repairRunningReceiptForSameRun = false,
3243
3231
  ): Promise<T> => {
3244
3232
  const serialized = await runWorkerRuntimeReceiptBoundary<unknown>({
3245
- baseUrl: req.baseUrl,
3246
- executorToken: req.executorToken,
3247
3233
  orgId: req.orgId,
3248
3234
  playName: req.playName,
3249
3235
  runId: req.runId,
3250
3236
  key,
3251
- postRuntimeApi,
3252
3237
  receiptStore,
3253
3238
  execute: async () => serializeDurableStepValue(await execute()),
3254
3239
  repairRunningReceiptForSameRun,
@@ -3897,6 +3882,7 @@ function createMinimalWorkerCtx(
3897
3882
  const result = await harnessReadSheetDatasetRows({
3898
3883
  baseUrl: req.baseUrl,
3899
3884
  executorToken: req.executorToken,
3885
+ orgId: req.orgId,
3900
3886
  playName: req.playName,
3901
3887
  tableNamespace: name,
3902
3888
  runId: req.runId,
@@ -5273,6 +5259,7 @@ async function persistResultDatasets(
5273
5259
  await harnessStartSheetDataset({
5274
5260
  baseUrl: req.baseUrl,
5275
5261
  executorToken: req.executorToken,
5262
+ orgId: req.orgId,
5276
5263
  playName: req.playName,
5277
5264
  tableNamespace: dataset.tableNamespace,
5278
5265
  sheetContract: requireSheetContract(req, dataset.tableNamespace),
@@ -5295,6 +5282,7 @@ async function persistResultDatasets(
5295
5282
  await harnessStartSheetDataset({
5296
5283
  baseUrl: req.baseUrl,
5297
5284
  executorToken: req.executorToken,
5285
+ orgId: req.orgId,
5298
5286
  preloadedDbSessions: req.preloadedDbSessions ?? null,
5299
5287
  playName: req.playName,
5300
5288
  tableNamespace: dataset.tableNamespace,
@@ -5502,14 +5490,14 @@ export class TenantWorkflow extends WorkflowEntrypoint<
5502
5490
  );
5503
5491
  captureCoordinatorBinding(this.env);
5504
5492
  captureRuntimeApiBinding(this.env);
5505
- // Hand the harness service binding (if wired) to the SDK-side stub.
5493
+ // Hand the required harness service binding to the SDK-side stub.
5506
5494
  // Must run BEFORE any SDK call site that would reach into HARNESS,
5507
5495
  // i.e. before user play code is invoked. Idempotent within a run.
5508
5496
  captureHarnessBinding(this.env);
5509
5497
  // Fire the one-time wiring probe (deduplicated across runs in the
5510
5498
  // same isolate). Awaited so the result is in the log before user code
5511
- // begins, but never throws broken HARNESS at probe time doesn't
5512
- // block the play; real call-site errors do.
5499
+ // begins. A missing or unhealthy HARNESS fails the run before user code
5500
+ // can accidentally take a slower fallback path.
5513
5501
  await probeHarnessOnce(this.env, runPrefix);
5514
5502
  const abortController = new AbortController();
5515
5503
  try {
@@ -3,27 +3,40 @@ import {
3
3
  harnessCompleteRuntimeReceipt,
4
4
  harnessFailRuntimeReceipt,
5
5
  } from '../../../../sdk/src/plays/harness-stub';
6
+ import type { PreloadedRuntimeDbSessionInput } from '../../../play-harness-worker/src/rpc-types';
6
7
  import type { WorkerRuntimeReceiptStore } from './receipts';
7
8
 
8
9
  export function createHarnessWorkerReceiptStore(input: {
9
10
  executorToken: string;
11
+ orgId: string;
12
+ preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
13
+ userEmail?: string | null;
10
14
  }): WorkerRuntimeReceiptStore {
11
15
  return {
12
16
  claimReceipt(command) {
13
17
  return harnessClaimRuntimeReceipt({
14
18
  executorToken: input.executorToken,
19
+ orgId: input.orgId,
20
+ preloadedDbSessions: input.preloadedDbSessions ?? null,
21
+ userEmail: input.userEmail ?? null,
15
22
  ...command,
16
23
  });
17
24
  },
18
25
  completeReceipt(command) {
19
26
  return harnessCompleteRuntimeReceipt({
20
27
  executorToken: input.executorToken,
28
+ orgId: input.orgId,
29
+ preloadedDbSessions: input.preloadedDbSessions ?? null,
30
+ userEmail: input.userEmail ?? null,
21
31
  ...command,
22
32
  });
23
33
  },
24
34
  failReceipt(command) {
25
35
  return harnessFailRuntimeReceipt({
26
36
  executorToken: input.executorToken,
37
+ orgId: input.orgId,
38
+ preloadedDbSessions: input.preloadedDbSessions ?? null,
39
+ userEmail: input.userEmail ?? null,
27
40
  ...command,
28
41
  });
29
42
  },
@@ -1,68 +1,28 @@
1
- import type {
2
- WorkReceipt,
3
- WorkReceiptClaim,
4
- WorkReceiptCommand,
5
- WorkReceiptStatus,
6
- WorkReceiptStore,
1
+ import {
2
+ buildScopedWorkReceiptKey,
3
+ type WorkReceipt,
4
+ type WorkReceiptClaim,
5
+ type WorkReceiptCommand,
6
+ type WorkReceiptStatus,
7
+ type WorkReceiptStore,
7
8
  } from '../../../../shared_libs/play-runtime/work-receipts';
8
9
 
9
10
  export type RuntimeReceiptStatus = WorkReceiptStatus;
10
11
 
11
12
  export type WorkerRuntimeReceipt = WorkReceipt;
12
13
 
13
- export type WorkerRuntimeReceiptResponse = {
14
- receipt?: WorkerRuntimeReceipt | null;
15
- };
16
-
17
- export type WorkerRuntimeReceiptAction =
18
- | {
19
- action: 'get_runtime_step_receipt';
20
- playName: string;
21
- runId: string;
22
- key: string;
23
- }
24
- | {
25
- action: 'claim_runtime_step_receipt';
26
- playName: string;
27
- runId: string;
28
- key: string;
29
- }
30
- | {
31
- action: 'complete_runtime_step_receipt';
32
- playName: string;
33
- runId: string;
34
- key: string;
35
- output: unknown;
36
- }
37
- | {
38
- action: 'fail_runtime_step_receipt';
39
- playName: string;
40
- runId: string;
41
- key: string;
42
- error: string;
43
- };
44
-
45
14
  export type WorkerRuntimeReceiptCommand = WorkReceiptCommand;
46
15
 
47
16
  export type WorkerRuntimeReceiptClaim = WorkReceiptClaim;
48
17
 
49
18
  export type WorkerRuntimeReceiptStore = WorkReceiptStore;
50
19
 
51
- type PostRuntimeApi = (
52
- baseUrl: string,
53
- executorToken: string,
54
- body: WorkerRuntimeReceiptAction,
55
- ) => Promise<WorkerRuntimeReceiptResponse>;
56
-
57
20
  type RuntimeReceiptContext = {
58
- baseUrl?: string;
59
- executorToken?: string;
60
21
  orgId?: string | null;
61
22
  playName: string;
62
23
  runId: string;
63
24
  key: string;
64
- postRuntimeApi?: PostRuntimeApi;
65
- receiptStore?: WorkerRuntimeReceiptStore;
25
+ receiptStore: WorkerRuntimeReceiptStore;
66
26
  };
67
27
 
68
28
  function scopedReceiptKey(input: {
@@ -70,7 +30,7 @@ function scopedReceiptKey(input: {
70
30
  playName: string;
71
31
  key: string;
72
32
  }): string {
73
- return `ctx:${input.orgId?.trim() || 'org'}:${input.playName.trim() || 'play'}:${input.key}`;
33
+ return buildScopedWorkReceiptKey(input);
74
34
  }
75
35
 
76
36
  function receiptOutput<T>(receipt: WorkerRuntimeReceipt): T {
@@ -90,92 +50,6 @@ function runningReceiptError(
90
50
  );
91
51
  }
92
52
 
93
- function isReusableReceipt(receipt: WorkerRuntimeReceipt): boolean {
94
- return receipt.status === 'completed' || receipt.status === 'skipped';
95
- }
96
-
97
- export function createRuntimeApiWorkerReceiptStore(input: {
98
- baseUrl: string;
99
- executorToken: string;
100
- postRuntimeApi: PostRuntimeApi;
101
- }): WorkerRuntimeReceiptStore {
102
- const postRuntimeReceiptAction = (body: WorkerRuntimeReceiptAction) =>
103
- input.postRuntimeApi(input.baseUrl, input.executorToken, body);
104
-
105
- return {
106
- async claimReceipt(command) {
107
- const claimed = await postRuntimeReceiptAction({
108
- action: 'claim_runtime_step_receipt',
109
- playName: command.playName,
110
- runId: command.runId,
111
- key: command.key,
112
- });
113
- if (claimed.receipt) {
114
- return { disposition: 'claimed', receipt: claimed.receipt };
115
- }
116
-
117
- const latest = await postRuntimeReceiptAction({
118
- action: 'get_runtime_step_receipt',
119
- playName: command.playName,
120
- runId: command.runId,
121
- key: command.key,
122
- });
123
- if (latest.receipt && isReusableReceipt(latest.receipt)) {
124
- return { disposition: 'reused', receipt: latest.receipt };
125
- }
126
- if (latest.receipt?.status === 'running') {
127
- return { disposition: 'running', receipt: latest.receipt };
128
- }
129
- if (latest.receipt?.status === 'failed') {
130
- return { disposition: 'failed', receipt: latest.receipt };
131
- }
132
- throw new Error(
133
- `Runtime receipt ${command.key} claim did not return execution ownership.`,
134
- );
135
- },
136
-
137
- async completeReceipt(command) {
138
- const completed = await postRuntimeReceiptAction({
139
- action: 'complete_runtime_step_receipt',
140
- playName: command.playName,
141
- runId: command.runId,
142
- key: command.key,
143
- output: command.output,
144
- });
145
- return completed.receipt ?? null;
146
- },
147
-
148
- async failReceipt(command) {
149
- const failed = await postRuntimeReceiptAction({
150
- action: 'fail_runtime_step_receipt',
151
- playName: command.playName,
152
- runId: command.runId,
153
- key: command.key,
154
- error: command.error,
155
- });
156
- return failed.receipt ?? null;
157
- },
158
- };
159
- }
160
-
161
- function resolveReceiptStore(
162
- input: RuntimeReceiptContext,
163
- ): WorkerRuntimeReceiptStore {
164
- if (input.receiptStore) {
165
- return input.receiptStore;
166
- }
167
- if (!input.baseUrl || !input.executorToken || !input.postRuntimeApi) {
168
- throw new Error(
169
- 'Runtime receipts require either a receiptStore or Runtime API transport.',
170
- );
171
- }
172
- return createRuntimeApiWorkerReceiptStore({
173
- baseUrl: input.baseUrl,
174
- executorToken: input.executorToken,
175
- postRuntimeApi: input.postRuntimeApi,
176
- });
177
- }
178
-
179
53
  async function executeAndPersistReceipt<T>(input: {
180
54
  key: string;
181
55
  playName: string;
@@ -223,7 +97,7 @@ export async function runWorkerRuntimeReceiptBoundary<T>(
223
97
  },
224
98
  ): Promise<T> {
225
99
  const key = scopedReceiptKey(input);
226
- const receiptStore = resolveReceiptStore(input);
100
+ const receiptStore = input.receiptStore;
227
101
  const claimed = await receiptStore.claimReceipt({
228
102
  playName: input.playName,
229
103
  runId: input.runId,
@@ -57,6 +57,9 @@ import type {
57
57
  PublishPlayVersionResult,
58
58
  StartPlayRunRequest,
59
59
  DeletePlayResult,
60
+ SharePageStatus,
61
+ PublishSharePageRequest,
62
+ UpdateSharePageRequest,
60
63
  ToolDefinition,
61
64
  ToolSearchOptions,
62
65
  ToolSearchResult,
@@ -1625,6 +1628,76 @@ export class DeeplineClient {
1625
1628
  return this.http.delete<DeletePlayResult>(`/api/v2/plays/${encodedName}`);
1626
1629
  }
1627
1630
 
1631
+ // ——————————————————————————————————————————————————————————
1632
+ // Plays — public share pages
1633
+ // ——————————————————————————————————————————————————————————
1634
+
1635
+ /**
1636
+ * Current share status for a play: the public page (if any), the published
1637
+ * copy, and the revision picker. Read-only.
1638
+ */
1639
+ async getSharePage(name: string): Promise<SharePageStatus> {
1640
+ const encodedName = encodeURIComponent(name);
1641
+ return this.http.get<SharePageStatus>(`/api/v2/plays/${encodedName}/share`);
1642
+ }
1643
+
1644
+ /**
1645
+ * Publish (or repoint) the play's public share page to a revision. Requires
1646
+ * `acknowledgedUnlisted: true` — the page is publicly viewable. Org-admin only.
1647
+ */
1648
+ async publishSharePage(
1649
+ name: string,
1650
+ request: PublishSharePageRequest,
1651
+ ): Promise<SharePageStatus> {
1652
+ const encodedName = encodeURIComponent(name);
1653
+ return this.http.post<SharePageStatus>(
1654
+ `/api/v2/plays/${encodedName}/share`,
1655
+ request,
1656
+ );
1657
+ }
1658
+
1659
+ /**
1660
+ * Update share-page settings (SEO indexing, credit-cost / latency display)
1661
+ * without moving the published pointer. Org-admin only.
1662
+ */
1663
+ async updateSharePage(
1664
+ name: string,
1665
+ request: UpdateSharePageRequest,
1666
+ ): Promise<SharePageStatus> {
1667
+ const encodedName = encodeURIComponent(name);
1668
+ return this.http.patch<SharePageStatus>(
1669
+ `/api/v2/plays/${encodedName}/share`,
1670
+ request,
1671
+ );
1672
+ }
1673
+
1674
+ /**
1675
+ * Unshare: hard-delete the play's public page and its cards. Returns the
1676
+ * fresh status (now `share: null`). Org-admin only. Idempotent — a no-op when
1677
+ * the play was never published.
1678
+ */
1679
+ async unpublishSharePage(name: string): Promise<SharePageStatus> {
1680
+ const encodedName = encodeURIComponent(name);
1681
+ return this.http.delete<SharePageStatus>(
1682
+ `/api/v2/plays/${encodedName}/share`,
1683
+ );
1684
+ }
1685
+
1686
+ /**
1687
+ * Regenerate the LLM landing-page copy for a revision (defaults to the
1688
+ * published one). Org-admin only.
1689
+ */
1690
+ async regenerateSharePage(
1691
+ name: string,
1692
+ request: { revisionId?: string } = {},
1693
+ ): Promise<SharePageStatus> {
1694
+ const encodedName = encodeURIComponent(name);
1695
+ return this.http.post<SharePageStatus>(
1696
+ `/api/v2/plays/${encodedName}/share/regenerate`,
1697
+ request,
1698
+ );
1699
+ }
1700
+
1628
1701
  // ——————————————————————————————————————————————————————————
1629
1702
  // Plays — high-level orchestration
1630
1703
  // ——————————————————————————————————————————————————————————