@ewhauser/world-celld 0.6.0 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.7.0 (2026-09-27)
6
+
7
+ ## What's Changed
8
+ * test(perf): cover mixed workflows, sustained load, and node failover by @ewhauser in https://github.com/ewhauser/world-celld/pull/64
9
+ * feat!: require celld v0.6.0 by @ewhauser in https://github.com/ewhauser/world-celld/pull/67
10
+ * perf(queue): dispatch consumer batches without a fill wait by @ewhauser in https://github.com/ewhauser/world-celld/pull/69
11
+ * perf(codec): use native base64 and a faster fallback by @ewhauser in https://github.com/ewhauser/world-celld/pull/70
12
+ * perf(runs): list runs inside the fleet in one round trip by @ewhauser in https://github.com/ewhauser/world-celld/pull/74
13
+ * perf(indexes): publish run mutations to indexes inside the fleet by @ewhauser in https://github.com/ewhauser/world-celld/pull/72
14
+ * perf(runs): keep idempotent run replays read-only by @ewhauser in https://github.com/ewhauser/world-celld/pull/75
15
+ * perf(events): honor preloadEvents on lazy hook resume by @ewhauser in https://github.com/ewhauser/world-celld/pull/71
16
+ * perf(streams): negotiate 512-chunk stream batches by @ewhauser in https://github.com/ewhauser/world-celld/pull/73
17
+ * perf(retention): finish small terminal cleanups in one pass by @ewhauser in https://github.com/ewhauser/world-celld/pull/76
18
+ * perf(retention): expire streams in pages of up to 256 chunks by @ewhauser in https://github.com/ewhauser/world-celld/pull/77
19
+
20
+
21
+ **Full Changelog**: https://github.com/ewhauser/world-celld/compare/v0.6.0...v0.7.0
22
+
5
23
  ## 0.6.0 (2026-09-16)
6
24
 
7
25
  ## What's Changed
package/README.md CHANGED
@@ -87,16 +87,17 @@ requests. It must be reachable from every celld node.
87
87
 
88
88
  ## Deploy the worker
89
89
 
90
- Before deploying, you need a celld v0.5.0 fleet, `esbuild` on `PATH`, and an
90
+ Before deploying, you need a celld v0.6.0 fleet, `esbuild` on `PATH`, and an
91
91
  object store that meets celld's conditional-write requirements. Refer to the
92
92
  [celld documentation](https://github.com/denoland/celld) for fleet and storage
93
93
  setup.
94
94
 
95
- **world-celld 0.5.0 is a breaking release and requires celld v0.5.0.**
96
- Older runtimes and node-level worker-variable overrides are unsupported.
95
+ **world-celld requires celld v0.6.0.** Older runtimes and node-level
96
+ worker-variable overrides are unsupported.
97
97
 
98
- For an existing fleet, follow the [v0.5.0 upgrade and feature review](docs/celld-v0.5-upgrade.md).
99
- Stop every old celld node before starting v0.5.0; this is not a rolling upgrade.
98
+ For an existing fleet, follow the [v0.6.0 upgrade and feature review](docs/celld-v0.6-upgrade.md).
99
+ A multi-node fleet uses fleet durability by default; stop every old celld node
100
+ before starting v0.6.0, because that is not a rolling upgrade.
100
101
 
101
102
  Before either deploy, set `vars.WORLD_SECRET` in both copied Wrangler configs
102
103
  to the same secret from your secret manager. Set optional
@@ -104,10 +105,10 @@ to the same secret from your secret manager. Set optional
104
105
  `vars.WORKFLOW_RETENTION_MS` there to the desired maximum workflow age
105
106
  (for example, `"7776000000"` for 90 days). Use private deployment copies outside
106
107
  source control: celld persists these variables in the deployment stored in the
107
- fleet bucket. v0.5.0 rejects `CELLD_VAR_*`; `.dev.vars` is read only by
108
+ fleet bucket. celld rejects `CELLD_VAR_*`; `.dev.vars` is read only by
108
109
  `celld dev`, not by `celld deploy`.
109
110
 
110
- No second storage service is required for queue payloads. celld v0.5.0 serves
111
+ No second storage service is required for queue payloads. celld v0.6.0 serves
111
112
  the `WORKFLOW_QUEUE_PAYLOADS` binding from the existing fleet bucket under
112
113
  `r2/workflow-world-queue-payloads/`. `r2_buckets` is the Wrangler-compatible
113
114
  configuration key for that binding; it does not require Cloudflare R2.
@@ -163,7 +164,7 @@ More deployment detail is in [`celld-worker/README.md`](./celld-worker/README.md
163
164
  and [`celld-queue-worker/README.md`](./celld-queue-worker/README.md).
164
165
 
165
166
  For opt-in bucket or collector telemetry and local `.dev.vars` setup, see
166
- the [operational examples](docs/celld-v0.5-upgrade.md#adopt-telemetry).
167
+ the [operational examples](docs/celld-v0.6-upgrade.md#adopt-telemetry).
167
168
 
168
169
  ## How it works
169
170
 
@@ -347,7 +348,7 @@ pnpm test:integration
347
348
 
348
349
  ### Real celld restart smoke
349
350
 
350
- The required CI smoke owns native celld v0.5.0 and MinIO processes on loopback,
351
+ The required CI smoke owns native celld v0.6.0 and MinIO processes on loopback,
351
352
  uses fresh temporary bucket and runtime state, and kills celld with `SIGKILL`
352
353
  before deleting its local working state and starting a new process against the
353
354
  bucket-backed state. It checks that:
@@ -382,12 +383,11 @@ failover correctness.
382
383
 
383
384
  The opt-in performance harness starts a fresh MinIO bucket and a single celld
384
385
  node with Docker Compose. Its queue workload verifies that every accepted
385
- message reaches a successful callback, including forced `503` redeliveries. A
386
- second workload creates terminal runs with streams and delayed queue messages,
387
- then verifies complete payload cleanup without resurrection. Results include
388
- queue and cleanup throughput plus p50, p95, p99, and maximum latency and are
389
- saved under `.perf-results/`. The harness pins celld v0.5.0 and deploys the same
390
- two-script native Queue topology as the restart smoke.
386
+ message reaches a successful callback, including forced `503` redeliveries. It
387
+ also measures a mixed run/step/hook/stream/queue lifecycle and terminal-run
388
+ cleanup. Results include throughput plus p50, p95, p99, and maximum latency and
389
+ are saved under `.perf-results/`. The harness pins celld v0.6.0 and deploys the
390
+ same two-script native Queue topology as the restart smoke.
391
391
 
392
392
  > This harness runs a single celld node and leaves the required storage-contract
393
393
  > probe enabled. Passing it does not establish multi-node fencing, ownership
@@ -422,6 +422,23 @@ Useful controls are `PERF_PAYLOAD_BYTES`, `PERF_RETRY_EVERY`,
422
422
  because local machines vary. Message-loss, message-ID, callback validity, and
423
423
  duplicate-success checks are always enforced.
424
424
 
425
+ `PERF_WORKFLOW_RUNS` and `PERF_WORKFLOW_CONCURRENCY` control the mixed workload.
426
+ Set `PERF_STEADY_SECONDS` to enable a duration-based queue run at
427
+ `PERF_STEADY_RATE` arrivals per second; its JSON records schedule lag and
428
+ one-second backlog samples. For a repeatable concurrency/payload sweep, run
429
+ `pnpm test:perf:minio:matrix`. It runs four isolated profiles and writes a
430
+ manifest with the Git commit, hardware, workload settings, and result paths.
431
+ `PERF_MATRIX_SOAK_SECONDS` changes the final steady profile's duration (60
432
+ seconds by default). Compare runs on the same host with the same profile and
433
+ without unrelated load; the harness does not set portable latency budgets.
434
+
435
+ `pnpm test:perf:minio:failover` starts one celld node, seeds a run, stream, and
436
+ queue workload (including accepted deliveries forced to retry), starts a peer
437
+ against the same bucket, kills the first node, then measures peer recovery and
438
+ delivery of both pending and new messages. Its output is
439
+ `minio-failover-latest.json`. This is a two-node local handoff test, not a
440
+ production object-store or fleet qualification.
441
+
425
442
  Bug reports and focused pull requests are welcome. Please include a regression
426
443
  test for behavior changes and run the checks above before submitting a PR.
427
444
 
@@ -1,6 +1,6 @@
1
1
  # world-celld Queue consumer
2
2
 
3
- This companion celld v0.5.0 script consumes the native `workflow-world` Queue
3
+ This companion celld v0.6.0 script consumes the native `workflow-world` Queue
4
4
  and forwards each delivery to the primary `workflow-world` script through a
5
5
  `QueueDeliveryRpc` named service binding. The consumer calls
6
6
  `deliver(secret, envelope, attempt)` and handles `complete`, `suspend`, or `retry`
@@ -16,7 +16,7 @@ celld deploy ./workflow-world-queue --bucket s3://my-cells-bucket
16
16
 
17
17
  Before deploying, set `vars.WORLD_SECRET` in the copied Wrangler config to the
18
18
  same bearer secret used by the primary worker. Keep that private deployment
19
- config out of source control. v0.5.0 rejects `CELLD_VAR_*` node overrides.
19
+ config out of source control. celld rejects `CELLD_VAR_*` node overrides.
20
20
  Deploy the primary worker after this script so it remains the fleet's
21
21
  public application. Native Queue operations are available through `celld queue
22
22
  info`, `peek`, `pause`, `resume`, `purge`, and `redrive`.
@@ -11,7 +11,9 @@
11
11
  {
12
12
  "queue": "workflow-world",
13
13
  "max_batch_size": 10,
14
- "max_batch_timeout": 1,
14
+ // Dispatch as soon as a message is ready. Every workflow step hop
15
+ // goes through this queue, so a batch-fill wait adds to each hop.
16
+ "max_batch_timeout": 0,
15
17
  "max_retries": 100,
16
18
  "dead_letter_queue": "workflow-world-dlq",
17
19
  "max_concurrency": 25,
@@ -15,7 +15,7 @@ Copy this directory and the companion Queue consumer out of `node_modules` so
15
15
  worker last. Before deploying, set `vars.WORLD_SECRET` in both copied
16
16
  Wrangler configs to the same secret from your secret manager. Keep those
17
17
  private deployment copies out of source control; celld stores their variables
18
- in the fleet bucket. Stop the whole old fleet before starting v0.5.0 nodes:
18
+ in the fleet bucket. Stop the whole old fleet before starting v0.6.0 nodes:
19
19
 
20
20
  ```sh
21
21
  cp -r node_modules/@ewhauser/world-celld/celld-worker ./workflow-world
@@ -30,7 +30,7 @@ to the native Queue.
30
30
 
31
31
  Requirements:
32
32
 
33
- - celld v0.5.0 (the currently tested runtime baseline).
33
+ - celld v0.6.0 (the currently tested runtime baseline).
34
34
  - `esbuild` on PATH (celld shells out to it).
35
35
  - A bucket with conditional-write support (celld's fencing requirement).
36
36
  - `WORLD_SECRET` set in the deployment config (`vars.WORLD_SECRET`) —
@@ -46,7 +46,7 @@ names the Workers-compatible binding API; it does not require Cloudflare R2.
46
46
  The bundled `wrangler.jsonc` declares an hourly UTC cron trigger. It does no
47
47
  catalog work by default. Set `vars.WORKFLOW_RETENTION_MS` to `"7776000000"`
48
48
  in the primary Wrangler config and redeploy to enable a maximum workflow age
49
- measured from run creation. v0.5.0 rejects `CELLD_VAR_*` node overrides.
49
+ measured from run creation. celld rejects `CELLD_VAR_*` node overrides.
50
50
  `.dev.vars` overrides apply only to `celld dev`, not production deployments.
51
51
 
52
52
  `7776000000` is 90 days. The policy includes pending and running workflows as
package/dist/index.d.ts CHANGED
@@ -149,6 +149,15 @@ interface ApplyEventSuccess {
149
149
  maxEvents?: number;
150
150
  /** Deadline for publishing this authoritative mutation to derivative indexes. */
151
151
  indexPublicationExpiresAt?: number;
152
+ /** Set by the worker router after it published `run` to the run catalog. */
153
+ runIndexPublished?: true;
154
+ /** Set by the worker router after it released `releasedHooks` from the hook indexes. */
155
+ hookIndexesReleased?: true;
156
+ /**
157
+ * The unchanged run returned with a `preloadEvents` hook_received replay
158
+ * log. It is not a run mutation, so it carries no index publication.
159
+ */
160
+ replayRun?: WorkflowRun;
152
161
  }
153
162
  type ApplyEventOutcome = ApplyEventSuccess | ApplyEventFailure;
154
163
  //#endregion
@@ -211,12 +220,16 @@ interface DurableObjectIdLike {
211
220
  }
212
221
  //#endregion
213
222
  //#region src/stream-protocol.d.ts
214
- /** Metadata plus payload and size-index entries remain below the 128-key platform limit. */
223
+ /**
224
+ * Chunks per write request that every worker accepts. A client sends larger
225
+ * batches only after a worker advertises {@link STREAM_CHUNK_LIMIT_HEADER}.
226
+ */
215
227
  declare const MAX_STREAM_WRITE_CHUNKS = 32;
216
228
  /** A chunk remains below the 2 MiB SQLite-backed DO key/value limit. */
217
229
  declare const MAX_STREAM_CHUNK_BYTES: number;
218
230
  /** Keeps serialized RPC bodies well below the 32 MiB Workers RPC limit. */
219
231
  declare const MAX_STREAM_BATCH_BYTES: number;
232
+ /** Chunks per read request that every worker accepts; see {@link MAX_STREAM_WRITE_CHUNKS}. */
220
233
  declare const MAX_STREAM_READ_CHUNKS = 32;
221
234
  declare const MAX_STREAM_READ_BYTES: number;
222
235
  declare const MAX_STREAM_ERROR_BYTES: number;
@@ -264,6 +277,13 @@ interface CelldStreamer extends Omit<Streamer, 'streams'> {
264
277
  }
265
278
  /** Direct StreamDO RPC surface. Remote stubs specialize read/write as binary HTTP. */
266
279
  interface StreamDOStub {
280
+ /**
281
+ * Remote stubs only: the per-request chunk limit the fleet has advertised.
282
+ * Stubs without it are held to the baseline every worker accepts.
283
+ */
284
+ streamChunkLimit?(): number;
285
+ /** Remote stubs only: drop an advertised limit an older worker rejected. */
286
+ resetStreamChunkLimit?(): void;
267
287
  writeChunks(runId: string, chunks: Uint8Array[]): Promise<StreamWriteResult>;
268
288
  readChunks(request: StreamReadRequest, signal?: AbortSignal): Promise<StreamReadResult>;
269
289
  closeStream(runId: string): Promise<void>;
@@ -323,6 +343,24 @@ interface CelldQueueProducer {
323
343
  send(envelope: NativeQueueEnvelope, options?: NativeQueueSendOptions): Promise<NativeQueueSendResult>;
324
344
  }
325
345
  //#endregion
346
+ //#region src/run-list.d.ts
347
+ type RunStatus = WorkflowRun['status'];
348
+ /** Wire shape of a run listing, shared by the client and the worker router. */
349
+ interface RunListRequest {
350
+ workflowName?: string;
351
+ statuses?: RunStatus[];
352
+ limit: number;
353
+ cursor?: string;
354
+ sortOrder?: 'asc' | 'desc';
355
+ /** 'none' strips run input and output from the returned page. */
356
+ resolveData?: 'none' | 'all';
357
+ }
358
+ interface RunListPage {
359
+ data: WorkflowRun[];
360
+ cursor: string | null;
361
+ hasMore: boolean;
362
+ }
363
+ //#endregion
326
364
  //#region src/indexes.d.ts
327
365
  interface IndexListOptions {
328
366
  prefix?: string;
@@ -360,6 +398,11 @@ interface WorkflowIndex {
360
398
  stored: boolean;
361
399
  }>;
362
400
  listRuns(options?: IndexListOptions): Promise<IndexListPage>;
401
+ /**
402
+ * Remote only: the complete run listing performed inside the fleet. Resolves
403
+ * to null when the worker predates the route; the caller then lists itself.
404
+ */
405
+ listResolvedRuns?(request: RunListRequest): Promise<RunListPage | null>;
363
406
  getHookByToken(token: string): Promise<string | null>;
364
407
  getHookById(hookId: string): Promise<string | null>;
365
408
  reserveHook(token: string, owner: HookTokenOwner): Promise<HookReservationResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/retention.ts","../src/apply-event.ts","../src/storage.ts","../src/stream-protocol.ts","../src/streamer.ts","../src/queue-protocol.ts","../src/queue.ts","../src/indexes.ts","../src/config.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/retention.ts","../src/apply-event.ts","../src/storage.ts","../src/stream-protocol.ts","../src/streamer.ts","../src/queue-protocol.ts","../src/queue.ts","../src/run-list.ts","../src/indexes.ts","../src/config.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as MAX_STREAM_BATCH_BYTES, B as decodeStreamWriteResult, C as boundedIntegerOption, F as MAX_STREAM_READ_CHUNKS, H as encodeStreamWriteBatch, I as MAX_STREAM_WRITE_CHUNKS, L as STREAM_BATCH_CONTENT_TYPE, M as MAX_STREAM_ERROR_BYTES, N as MAX_STREAM_LONG_POLL_MS, O as queueDelayDeadline, P as MAX_STREAM_READ_BYTES, R as decodeStreamReadResult, W as normalizeStreamError, b as MAX_QUEUE_DELIVERY_TIMEOUT_MS, d as rpcParse, f as rpcStringify, h as createWorkflowIndex, j as MAX_STREAM_CHUNK_BYTES, k as strictIntegerSetting, m as stringify, p as parse, r as nativeQueueDelaySeconds, u as compact, w as checkedQueueTimestampAdd, x as MAX_QUEUE_DERIVED_DEADLINE_HEADROOM_MS, y as MAX_FLEET_RPC_TIMEOUT_MS } from "./queue-protocol-CmJNMH_5.js";
2
- import { CreateEventSchema, EventSchema, HookSchema, MessageId, QueuePayloadSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, isTerminalWorkflowRunStatus, parseQueueName } from "@workflow/world";
1
+ import { B as STREAM_BATCH_CONTENT_TYPE, C as MAX_QUEUE_DELIVERY_TIMEOUT_MS, D as checkedQueueTimestampAdd, E as boundedIntegerOption, F as MAX_STREAM_ERROR_BYTES, H as decodeStreamReadResult, I as MAX_STREAM_LONG_POLL_MS, J as isChunkLimitRejection, K as encodeStreamWriteBatch, L as MAX_STREAM_READ_BYTES, M as strictIntegerSetting, N as MAX_STREAM_BATCH_BYTES, P as MAX_STREAM_CHUNK_BYTES, R as MAX_STREAM_READ_CHUNKS, S as MAX_FLEET_RPC_TIMEOUT_MS, V as STREAM_CHUNK_LIMIT_HEADER, W as decodeStreamWriteResult, Y as normalizeStreamError, _ as createWorkflowIndex, a as nativeQueueDelaySeconds, f as compact, g as stringify, h as parse, j as queueDelayDeadline, m as rpcStringify, n as listRunsPage, p as rpcParse, v as runIndexMetadata, w as MAX_QUEUE_DERIVED_DEADLINE_HEADROOM_MS, z as MAX_STREAM_WRITE_CHUNKS } from "./run-list-B__jKuE7.js";
2
+ import { CreateEventSchema, EventSchema, HookSchema, MessageId, QueuePayloadSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, parseQueueName } from "@workflow/world";
3
3
  import { EntityConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, TooEarlyError, WorkflowRunNotFoundError, WorkflowWorldError } from "@workflow/errors";
4
4
  import { monotonicFactory } from "ulid";
5
5
  //#region src/config.ts
@@ -132,6 +132,23 @@ const READ_ATTEMPTS = 3;
132
132
  const MAX_WRITE_RESPONSE_BYTES = 64;
133
133
  const MAX_READ_RESPONSE_BYTES = MAX_STREAM_READ_BYTES + 65536;
134
134
  const MAX_ERROR_RESPONSE_BYTES = 65536;
135
+ /**
136
+ * Per-request chunk limit the fleet has advertised, per transport. Absent
137
+ * until a worker advertises one, so a client never sends an older worker more
138
+ * than the baseline it accepts; a response without the header drops back.
139
+ */
140
+ const advertisedChunkLimits = /* @__PURE__ */ new WeakMap();
141
+ function streamChunkLimit(transport) {
142
+ return advertisedChunkLimits.get(transport) ?? 32;
143
+ }
144
+ function resetStreamChunkLimit(transport) {
145
+ advertisedChunkLimits.delete(transport);
146
+ }
147
+ function recordStreamChunkLimit(transport, response) {
148
+ const advertised = Number(response.headers.get(STREAM_CHUNK_LIMIT_HEADER));
149
+ if (Number.isSafeInteger(advertised) && advertised > 32) advertisedChunkLimits.set(transport, Math.min(advertised, 512));
150
+ else advertisedChunkLimits.delete(transport);
151
+ }
135
152
  function delayMs(ms, signal) {
136
153
  if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
137
154
  if (signal.aborted) return Promise.reject(aborted(signal));
@@ -202,7 +219,7 @@ function streamUrl(transport, name) {
202
219
  async function writeStreamChunks(transport, name, runId, chunks) {
203
220
  const timeoutMs = resolveFleetTimeoutMs(transport);
204
221
  const body = encodeStreamWriteBatch(chunks);
205
- if (body.byteLength > 8389632) throw new Error("world-celld: encoded stream batch exceeds configured limit");
222
+ if (body.byteLength > 8391680) throw new Error("world-celld: encoded stream batch exceeds configured limit");
206
223
  const url = new URL(streamUrl(transport, name));
207
224
  url.searchParams.set("runId", runId);
208
225
  const doFetch = transport.fetchImpl ?? fetch;
@@ -221,6 +238,7 @@ async function writeStreamChunks(transport, name, runId, chunks) {
221
238
  throw new FleetTransportError(`world-celld: fleet unreachable at ${url.href}`, error);
222
239
  }
223
240
  if (!response.ok) throw await errorFromResponse(response);
241
+ recordStreamChunkLimit(transport, response);
224
242
  try {
225
243
  return decodeStreamWriteResult(await readBoundedResponse(response, MAX_WRITE_RESPONSE_BYTES));
226
244
  } catch (error) {
@@ -257,13 +275,16 @@ async function readStreamChunks(transport, name, request, signal) {
257
275
  await response.body?.cancel(aborted(signal)).catch(() => void 0);
258
276
  throw aborted(signal);
259
277
  }
260
- if (response.ok) try {
261
- return decodeStreamReadResult(await readBoundedResponse(response, MAX_READ_RESPONSE_BYTES));
262
- } catch (error) {
263
- if (signal?.aborted) throw aborted(signal);
264
- lastError = error instanceof FleetTransportError ? error : new FleetTransportError(`world-celld: malformed stream response from ${url.href}`, error);
265
- if (attempt < READ_ATTEMPTS) continue;
266
- throw lastError;
278
+ if (response.ok) {
279
+ recordStreamChunkLimit(transport, response);
280
+ try {
281
+ return decodeStreamReadResult(await readBoundedResponse(response, MAX_READ_RESPONSE_BYTES));
282
+ } catch (error) {
283
+ if (signal?.aborted) throw aborted(signal);
284
+ lastError = error instanceof FleetTransportError ? error : new FleetTransportError(`world-celld: malformed stream response from ${url.href}`, error);
285
+ if (attempt < READ_ATTEMPTS) continue;
286
+ throw lastError;
287
+ }
267
288
  }
268
289
  let error;
269
290
  try {
@@ -276,6 +297,12 @@ async function readStreamChunks(transport, name, request, signal) {
276
297
  lastError = error;
277
298
  continue;
278
299
  }
300
+ if (request.maxChunks > 32 && isChunkLimitRejection(error) && attempt < READ_ATTEMPTS) {
301
+ resetStreamChunkLimit(transport);
302
+ url.searchParams.set("maxChunks", String(32));
303
+ lastError = error;
304
+ continue;
305
+ }
279
306
  throw error;
280
307
  }
281
308
  throw lastError instanceof Error ? lastError : new FleetTransportError("world-celld: stream read failed", lastError);
@@ -335,6 +362,8 @@ function makeStreamNamespace(transport) {
335
362
  const control = makeStub(transport, "streams", name, STREAMS);
336
363
  control.writeChunks = (runId, chunks) => writeStreamChunks(transport, name, runId, chunks);
337
364
  control.readChunks = (request, signal) => readStreamChunks(transport, name, request, signal);
365
+ control.streamChunkLimit = () => streamChunkLimit(transport);
366
+ control.resetStreamChunkLimit = () => resetStreamChunkLimit(transport);
338
367
  return control;
339
368
  }
340
369
  };
@@ -382,6 +411,20 @@ const HOOK_IDS = {
382
411
  ],
383
412
  mutating: /* @__PURE__ */ new Set()
384
413
  };
414
+ /** Transports whose worker answered the fleet-side run listing with 404. */
415
+ const legacyRunListing = /* @__PURE__ */ new WeakSet();
416
+ async function listResolvedRuns(transport, request) {
417
+ if (legacyRunListing.has(transport)) return null;
418
+ try {
419
+ return await callFleetRoute(transport, "/v1/index/runs/list-resolved", [request], { idempotent: true });
420
+ } catch (error) {
421
+ if (error.status === 404) {
422
+ legacyRunListing.add(transport);
423
+ return null;
424
+ }
425
+ throw error;
426
+ }
427
+ }
385
428
  function createRemoteEnv(transport) {
386
429
  const runCatalog = makeNamespace(transport, "run-catalog", RUN_CATALOG);
387
430
  const hookTokens = makeNamespace(transport, "hook-tokens", HOOK_TOKENS);
@@ -402,6 +445,7 @@ function createRemoteEnv(transport) {
402
445
  publicationExpiresAt
403
446
  ], { idempotent: true }),
404
447
  listRuns: (options) => callFleetRoute(transport, "/v1/index/runs/list", [options], { idempotent: true }),
448
+ listResolvedRuns: (request) => listResolvedRuns(transport, request),
405
449
  expireRun: (request) => callFleetRoute(transport, "/v1/index/runs/expire", [request], { idempotent: true }),
406
450
  reserveHook: (token, owner) => callFleetRoute(transport, "/v1/index/hooks/reserve", [token, owner], { idempotent: true }),
407
451
  finalizeHookIndexes: (token, hookId, serializedHook, owner, reservation) => callFleetRoute(transport, "/v1/index/hooks/finalize", [
@@ -458,7 +502,7 @@ const debug = createDebugLogger("celld-world");
458
502
  * Vendored from vinnymac/worlds packages/world-cloudflare/src/queue.ts
459
503
  * (Apache-2.0, see NOTICE), modified for celld:
460
504
  *
461
- * - Production publishes a small pointer through celld v0.5.0 native Queues.
505
+ * - Production publishes a small pointer through celld v0.6.0 native Queues.
462
506
  * The deployed Queue consumer forwards deliveries to the same HTTP handler
463
507
  * used by the in-process test pump.
464
508
  * - Run-bearing message bodies live in the fleet object store. This preserves
@@ -828,6 +872,7 @@ function parseApplyEventOutcome(value) {
828
872
  for (const releasedHook of value.releasedHooks) if (!isRecord(releasedHook) || typeof releasedHook.hookId !== "string" || releasedHook.hookId.length === 0 || typeof releasedHook.token !== "string" || releasedHook.token.length === 0) malformedApplyEventOutcome("releasedHooks entries must contain hookId and token strings");
829
873
  const event = parseSuccessEntity(value, "event", EventSchema);
830
874
  const run = parseSuccessEntity(value, "run", WorkflowRunSchema);
875
+ const replayRun = parseSuccessEntity(value, "replayRun", WorkflowRunSchema);
831
876
  const step = parseSuccessEntity(value, "step", StepSchema);
832
877
  const hook = parseSuccessEntity(value, "hook", HookSchema);
833
878
  const wait = parseSuccessEntity(value, "wait", WaitSchema);
@@ -851,6 +896,7 @@ function parseApplyEventOutcome(value) {
851
896
  if (value.cursor !== void 0 && value.cursor !== null && typeof value.cursor !== "string") malformedApplyEventOutcome("cursor must be a string or null");
852
897
  if (value.hasMore !== void 0 && typeof value.hasMore !== "boolean") malformedApplyEventOutcome("hasMore must be a boolean");
853
898
  if (value.maxEvents !== void 0 && (typeof value.maxEvents !== "number" || !Number.isSafeInteger(value.maxEvents) || value.maxEvents < 0)) malformedApplyEventOutcome("maxEvents must be a non-negative safe integer");
899
+ for (const flag of ["runIndexPublished", "hookIndexesReleased"]) if (value[flag] !== void 0 && value[flag] !== true) malformedApplyEventOutcome(`${flag} must be true when present`);
854
900
  if (run !== void 0 && value.indexPublicationExpiresAt === void 0) malformedApplyEventOutcome("indexPublicationExpiresAt is required when run is present");
855
901
  if (value.indexPublicationExpiresAt !== void 0 && (typeof value.indexPublicationExpiresAt !== "number" || !Number.isFinite(value.indexPublicationExpiresAt))) malformedApplyEventOutcome("indexPublicationExpiresAt must be a finite number");
856
902
  return {
@@ -859,6 +905,7 @@ function parseApplyEventOutcome(value) {
859
905
  ...value,
860
906
  event,
861
907
  run,
908
+ replayRun,
862
909
  step,
863
910
  hook,
864
911
  wait,
@@ -951,19 +998,6 @@ function unwrapRead(outcome) {
951
998
  if (!outcome.ok) throw new RunExpiredError(outcome.message);
952
999
  return outcome.value;
953
1000
  }
954
- /** Bound cross-run fanout so large list pages cannot create an RPC burst. */
955
- const RUN_LIST_CONCURRENCY = 8;
956
- /**
957
- * Run statuses only move pending -> running -> terminal, and terminal statuses
958
- * are immutable. An older index value may therefore safely exclude a filter
959
- * only when it is already terminal, or when the caller asks for pending and
960
- * the index has advanced beyond pending. Earlier non-terminal metadata cannot
961
- * exclude a later status because a post-commit index write may need replay.
962
- */
963
- function indexStatusExcludes(indexed, requested) {
964
- if (requested === void 0 || indexed === requested) return false;
965
- return isTerminalWorkflowRunStatus(indexed) || requested === "pending";
966
- }
967
1001
  function createStorage(config) {
968
1002
  const { env } = config;
969
1003
  const cleanup = { retentionMs: config.runRetentionMs ?? 0 };
@@ -981,61 +1015,31 @@ function createStorage(config) {
981
1015
  runs: {
982
1016
  get: runsGet,
983
1017
  async list(params) {
984
- const statuses = params?.status === void 0 ? void 0 : Array.isArray(params.status) ? params.status : [params.status];
985
- const limit = params?.pagination?.limit ?? 20;
986
- const prefix = params?.workflowName ? `run:${params.workflowName}:` : "runall:";
987
- const reverse = params?.pagination?.sortOrder === "desc";
988
- const matches = [];
989
- let scanCursor = params?.pagination?.cursor;
990
- let exhausted = false;
991
- while (matches.length <= limit && !exhausted) {
992
- const kvList = await env.WORKFLOW_INDEX.listRuns({
993
- prefix,
994
- limit: Math.min(1e3, Math.max(50, limit)),
995
- cursor: scanCursor,
996
- reverse
997
- });
998
- if (kvList.keys.length === 0) {
999
- exhausted = true;
1000
- break;
1001
- }
1002
- const candidates = [];
1003
- for (const key of kvList.keys) {
1004
- const metadata = JSON.parse(key.value);
1005
- if (statuses?.every((status) => indexStatusExcludes(metadata.status, status))) continue;
1006
- candidates.push({
1007
- key: key.name,
1008
- metadata
1009
- });
1010
- }
1011
- for (let offset = 0; offset < candidates.length && matches.length <= limit;) {
1012
- const needed = limit + 1 - matches.length;
1013
- const batch = candidates.slice(offset, offset + Math.min(RUN_LIST_CONCURRENCY, needed));
1014
- const resolved = await Promise.all(batch.map(async ({ key, metadata }) => {
1015
- try {
1016
- const run = await runsGet(metadata.runId, { resolveData: params?.resolveData });
1017
- return statuses === void 0 || statuses.includes(run.status) ? {
1018
- key,
1019
- run
1020
- } : null;
1021
- } catch (error) {
1022
- if (!WorkflowRunNotFoundError.is(error) && !RunExpiredError.is(error)) throw error;
1023
- return null;
1024
- }
1025
- }));
1026
- for (const match of resolved) if (match) matches.push(match);
1027
- offset += batch.length;
1028
- }
1029
- exhausted = kvList.list_complete;
1030
- scanCursor = kvList.cursor ?? kvList.keys.at(-1)?.name;
1031
- }
1032
- const hasMore = matches.length > limit;
1033
- const page = matches.slice(0, limit);
1034
- return {
1035
- data: page.map(({ run }) => run),
1036
- cursor: hasMore ? page.at(-1)?.key ?? null : null,
1037
- hasMore
1018
+ const request = {
1019
+ workflowName: params?.workflowName,
1020
+ statuses: params?.status === void 0 ? void 0 : Array.isArray(params.status) ? params.status : [params.status],
1021
+ limit: params?.pagination?.limit ?? 20,
1022
+ cursor: params?.pagination?.cursor,
1023
+ sortOrder: params?.pagination?.sortOrder,
1024
+ resolveData: params?.resolveData
1025
+ };
1026
+ const fleetPage = Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 1e3 ? await env.WORKFLOW_INDEX.listResolvedRuns?.(compact(request)) : void 0;
1027
+ if (fleetPage) return {
1028
+ data: fleetPage.data.map((run) => filterData(parseRun(run), params?.resolveData, ["input", "output"])),
1029
+ cursor: fleetPage.cursor,
1030
+ hasMore: fleetPage.hasMore
1038
1031
  };
1032
+ return await listRunsPage({
1033
+ listRuns: (options) => env.WORKFLOW_INDEX.listRuns(options),
1034
+ readRun: async (runId) => {
1035
+ try {
1036
+ return await runsGet(runId);
1037
+ } catch (error) {
1038
+ if (WorkflowRunNotFoundError.is(error) || RunExpiredError.is(error)) return null;
1039
+ throw error;
1040
+ }
1041
+ }
1042
+ }, request);
1039
1043
  }
1040
1044
  },
1041
1045
  events: {
@@ -1095,20 +1099,18 @@ function createStorage(config) {
1095
1099
  throwOutcomeError(parsedOutcome.outcome, effectiveRunId, data);
1096
1100
  }
1097
1101
  const outcome = parsedOutcome.outcome;
1098
- if (outcome.run) {
1099
- const meta = JSON.stringify({
1100
- runId: effectiveRunId,
1101
- createdAt: outcome.run.createdAt.toISOString(),
1102
- status: outcome.run.status
1103
- });
1102
+ if (outcome.run && !outcome.runIndexPublished) {
1104
1103
  if (outcome.indexPublicationExpiresAt === void 0) throw new Error("world-celld: authoritative run mutation omitted its index lease");
1105
- await env.WORKFLOW_INDEX.commitRun(outcome.run, meta, outcome.indexPublicationExpiresAt);
1104
+ await env.WORKFLOW_INDEX.commitRun(outcome.run, runIndexMetadata({
1105
+ ...outcome.run,
1106
+ runId: effectiveRunId
1107
+ }), outcome.indexPublicationExpiresAt);
1106
1108
  }
1107
1109
  if (outcome.hookToIndex) {
1108
1110
  const serialized = stringify(outcome.hookToIndex);
1109
1111
  await env.WORKFLOW_INDEX.finalizeHookIndexes(outcome.hookToIndex.token, outcome.hookToIndex.hookId, serialized, hookOwner(outcome.hookToIndex), hookAdmission?.reservation);
1110
1112
  } else if (hookAdmission) await env.WORKFLOW_INDEX.releaseHookReservation(hookAdmission.token, hookAdmission.owner, hookAdmission.reservation);
1111
- if (outcome.releasedHooks.length > 0) await env.WORKFLOW_INDEX.releaseHookIndexes({
1113
+ if (outcome.releasedHooks.length > 0 && !outcome.hookIndexesReleased) await env.WORKFLOW_INDEX.releaseHookIndexes({
1112
1114
  runId: effectiveRunId,
1113
1115
  hooks: outcome.releasedHooks
1114
1116
  });
@@ -1119,7 +1121,7 @@ function createStorage(config) {
1119
1121
  };
1120
1122
  return {
1121
1123
  event: outcome.event,
1122
- run: outcome.run,
1124
+ run: outcome.run ?? outcome.replayRun,
1123
1125
  step: outcome.step,
1124
1126
  hook: outcome.hook,
1125
1127
  wait: outcome.wait,
@@ -1276,18 +1278,28 @@ function createStreamer(config) {
1276
1278
  const chunks = input.map(toBytes);
1277
1279
  for (const chunk of chunks) if (chunk.byteLength > 1048576) throw new Error(`Stream chunk exceeds ${MAX_STREAM_CHUNK_BYTES} bytes`);
1278
1280
  await registerStreamForRun(name, resolvedRunId);
1279
- let batch = [];
1280
- let batchBytes = 0;
1281
- for (const chunk of chunks) {
1282
- if (batch.length > 0 && (batch.length === 32 || batchBytes + chunk.byteLength > 8388608)) {
1283
- await getStreamDO(name).writeChunks(resolvedRunId, batch);
1284
- batch = [];
1285
- batchBytes = 0;
1281
+ const stub = getStreamDO(name);
1282
+ let next = 0;
1283
+ while (next < chunks.length) {
1284
+ const chunkLimit = stub.streamChunkLimit?.() ?? 32;
1285
+ let end = next;
1286
+ let batchBytes = 0;
1287
+ while (end < chunks.length && end - next < chunkLimit && (end === next || batchBytes + chunks[end].byteLength <= 8388608)) {
1288
+ batchBytes += chunks[end].byteLength;
1289
+ end++;
1290
+ }
1291
+ const batch = chunks.slice(next, end);
1292
+ try {
1293
+ await stub.writeChunks(resolvedRunId, batch);
1294
+ } catch (error) {
1295
+ if (batch.length > 32 && isChunkLimitRejection(error)) {
1296
+ stub.resetStreamChunkLimit?.();
1297
+ continue;
1298
+ }
1299
+ throw error;
1286
1300
  }
1287
- batch.push(chunk);
1288
- batchBytes += chunk.byteLength;
1301
+ next = end;
1289
1302
  }
1290
- if (batch.length > 0) await getStreamDO(name).writeChunks(resolvedRunId, batch);
1291
1303
  };
1292
1304
  const writeToStream = (name, runId, chunk) => writeChunksToStream(name, runId, [chunk]);
1293
1305
  const closeStream = async (name, runId) => {
@@ -1325,7 +1337,7 @@ function createStreamer(config) {
1325
1337
  result = await stub.readChunks({
1326
1338
  runId,
1327
1339
  startIndex: nextIndex,
1328
- maxChunks: 32,
1340
+ maxChunks: stub.streamChunkLimit?.() ?? 32,
1329
1341
  maxBytes: MAX_STREAM_READ_BYTES,
1330
1342
  waitMs: streamLongPollMs
1331
1343
  }, abortController.signal);
@@ -1355,10 +1367,11 @@ function createStreamer(config) {
1355
1367
  const getStreamChunks = async (name, runId, options) => {
1356
1368
  const requestedLimit = options?.limit ?? DEFAULT_CHUNK_PAGE_SIZE;
1357
1369
  if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) throw new Error("Stream chunk limit must be a positive safe integer");
1358
- const limit = Math.min(requestedLimit, MAX_CHUNK_PAGE_SIZE);
1370
+ const stub = getStreamDO(name);
1371
+ const limit = Math.min(requestedLimit, stub.streamChunkLimit?.() ?? MAX_CHUNK_PAGE_SIZE);
1359
1372
  const startIndex = options?.cursor ? Number.parseInt(options.cursor, 10) : 0;
1360
1373
  if (Number.isNaN(startIndex) || startIndex < 0) throw new Error(`Invalid stream cursor: ${options?.cursor}`);
1361
- const result = await getStreamDO(name).readChunks({
1374
+ const result = await stub.readChunks({
1362
1375
  runId,
1363
1376
  startIndex,
1364
1377
  maxChunks: limit,