@procedures/client 0.24.1 → 0.26.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +15 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/server/handler.js +7 -2
  5. package/dist/adapters/http/server/handler.js.map +1 -1
  6. package/dist/adapters/http/server/ndjson.d.ts +10 -13
  7. package/dist/adapters/http/server/ndjson.d.ts.map +1 -1
  8. package/dist/adapters/http/server/ndjson.js +12 -13
  9. package/dist/adapters/http/server/ndjson.js.map +1 -1
  10. package/dist/adapters/websocket/shared/rpc.d.ts +5 -8
  11. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  12. package/dist/adapters/websocket/shared/rpc.js +11 -14
  13. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  14. package/dist/browser.d.ts +3 -3
  15. package/dist/browser.d.ts.map +1 -1
  16. package/dist/browser.js +4 -1
  17. package/dist/browser.js.map +1 -1
  18. package/dist/client/client.d.ts +7 -1
  19. package/dist/client/client.d.ts.map +1 -1
  20. package/dist/client/client.js +30 -19
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/client/middleware/retry.d.ts.map +1 -1
  23. package/dist/client/middleware/retry.js +3 -1
  24. package/dist/client/middleware/retry.js.map +1 -1
  25. package/dist/procedures/core/index.d.ts +19 -3
  26. package/dist/procedures/core/index.d.ts.map +1 -1
  27. package/dist/procedures/core/index.js +53 -178
  28. package/dist/procedures/core/index.js.map +1 -1
  29. package/dist/procedures/core/operands.d.ts +12 -13
  30. package/dist/procedures/core/operands.d.ts.map +1 -1
  31. package/dist/procedures/core/operands.js +22 -38
  32. package/dist/procedures/core/operands.js.map +1 -1
  33. package/dist/procedures/define-procedure.d.ts.map +1 -1
  34. package/dist/procedures/define-procedure.js +6 -6
  35. package/dist/procedures/define-procedure.js.map +1 -1
  36. package/dist/procedures/envelope.d.ts +24 -8
  37. package/dist/procedures/envelope.d.ts.map +1 -1
  38. package/dist/procedures/envelope.js +51 -20
  39. package/dist/procedures/envelope.js.map +1 -1
  40. package/dist/procedures/idempotency.d.ts +5 -0
  41. package/dist/procedures/idempotency.d.ts.map +1 -1
  42. package/dist/procedures/idempotency.js +8 -0
  43. package/dist/procedures/idempotency.js.map +1 -1
  44. package/dist/procedures/index.d.ts +3 -1
  45. package/dist/procedures/index.d.ts.map +1 -1
  46. package/dist/procedures/index.js +3 -1
  47. package/dist/procedures/index.js.map +1 -1
  48. package/dist/procedures/program.d.ts +251 -0
  49. package/dist/procedures/program.d.ts.map +1 -0
  50. package/dist/procedures/program.js +1120 -0
  51. package/dist/procedures/program.js.map +1 -0
  52. package/dist/procedures/ref.d.ts +8 -91
  53. package/dist/procedures/ref.d.ts.map +1 -1
  54. package/dist/procedures/ref.js +16 -279
  55. package/dist/procedures/ref.js.map +1 -1
  56. package/dist/protocol.d.ts +12 -0
  57. package/dist/protocol.d.ts.map +1 -1
  58. package/dist/protocol.js +20 -0
  59. package/dist/protocol.js.map +1 -1
  60. package/dist/server/procedure-server.d.ts +3 -6
  61. package/dist/server/procedure-server.d.ts.map +1 -1
  62. package/dist/server/procedure-server.js +33 -14
  63. package/dist/server/procedure-server.js.map +1 -1
  64. package/package.json +2 -1
  65. package/src/adapters/http/server/handler.ts +8 -2
  66. package/src/adapters/http/server/ndjson.ts +17 -18
  67. package/src/adapters/websocket/shared/rpc.ts +11 -14
  68. package/src/browser.ts +25 -1
  69. package/src/client/client.ts +35 -23
  70. package/src/client/middleware/retry.ts +3 -1
  71. package/src/procedures/core/index.ts +68 -212
  72. package/src/procedures/core/operands.ts +24 -44
  73. package/src/procedures/define-procedure.ts +5 -7
  74. package/src/procedures/envelope.ts +64 -21
  75. package/src/procedures/idempotency.ts +8 -0
  76. package/src/procedures/index.ts +34 -4
  77. package/src/procedures/program.ts +1371 -0
  78. package/src/procedures/ref.ts +20 -354
  79. package/src/protocol.ts +19 -0
  80. package/src/server/procedure-server.ts +32 -13
package/src/browser.ts CHANGED
@@ -220,6 +220,20 @@ export {
220
220
  extractTemplate,
221
221
  parseProcedureJson,
222
222
  stringifyProcedureJson,
223
+ // Programs on the coeval kernel (coeval D13, roadmap item M14)
224
+ runProgram,
225
+ runDocument,
226
+ programMachine,
227
+ PROGRAM_WORLD,
228
+ desugarProgram,
229
+ desugarInput,
230
+ desugarControlValue,
231
+ originOf,
232
+ namesOfScope,
233
+ scopeOfNames,
234
+ nameKey,
235
+ nameOfKey,
236
+ documentAt,
223
237
  // The envelope form of a program (coeval M7)
224
238
  isEnvelopeProgram,
225
239
  fromEnvelopeProgram,
@@ -344,6 +358,15 @@ export type {
344
358
  ContinueDecision,
345
359
  OutputRef,
346
360
  RefScope,
361
+ // Programs on the coeval kernel (coeval D13, roadmap item M14)
362
+ ProgramDocument,
363
+ DocumentOrigin,
364
+ DesugarOptions,
365
+ ProgramHost,
366
+ CallEffect,
367
+ CallPosition,
368
+ RunDocumentOptions,
369
+ RunProgramOptions,
347
370
  // The envelope form of a program (coeval M7)
348
371
  EnvelopeProgram,
349
372
  EnvelopeRef,
@@ -414,7 +437,8 @@ export type { PortLike, PortTransportOptions } from "./adapters/port/transport.j
414
437
  export { TRACEPARENT_METADATA_KEY, formatTraceparent, parseTraceparent, newTraceId, newSpanId, isW3cIds } from "./tracing/traceparent.js";
415
438
  export type { TraceContext } from "./tracing/traceparent.js";
416
439
  // Wire protocol v1: the version and the one list of error codes (roadmap R25)
417
- export { PROTOCOL_VERSION, PROTOCOL_HEADER, ERROR_CODES, isProtocolErrorCode, httpStatusOf, protocolVersionOf } from "./protocol.js";
440
+ // The retryable flag of an error that ends a call: the default of its code before the first item (CLI-68)
441
+ export { PROTOCOL_VERSION, PROTOCOL_HEADER, ERROR_CODES, isProtocolErrorCode, httpStatusOf, protocolVersionOf, retryableOf } from "./protocol.js";
418
442
  export type { ProtocolErrorCode, ErrorCodeInfo } from "./protocol.js";
419
443
  // The seq of the stream frames: the metadata key of an item after a gap, and the key of an item
420
444
  // that the HTTP client read from an NDJSON stream (WIRE-PROTOCOL.md)
@@ -88,12 +88,12 @@ import {
88
88
  isAnyProcedureRef,
89
89
  isPlainObject,
90
90
  normalizeRef,
91
- prepareRootCall,
92
91
  rawInputRule,
93
92
  type ProcedureRef,
94
93
  type AnyProcedureRef,
95
94
  type HydrationEvent,
96
95
  } from "../procedures/ref.js";
96
+ import { runProgram } from "../procedures/program.js";
97
97
  import { fromEnvelopeProgram, isEnvelopeProgram, type EnvelopeProgram } from "../procedures/envelope.js";
98
98
 
99
99
  /** Each element of each array item: the stream form of an `outputMode: "batch"` procedure. */
@@ -527,8 +527,10 @@ export class Client<TContext = {}> {
527
527
  options: ExecOptions = {}
528
528
  ): Promise<TOutput> {
529
529
  const caller: CallerContext = callerOf(options);
530
- const { path, input: hydratedInput } = await this.prepareRef(refOrPath, input, caller);
531
- return this.execInternal<TOutput>(path, hydratedInput, caller);
530
+ // The program is one run of the kernel: its root call is the root document (PROGRAM-FORMAT section 2)
531
+ return (await this.runRef(this.prepareRef(refOrPath, input), caller, (path, value) =>
532
+ this.execInternal(path, value, caller),
533
+ )) as TOutput;
532
534
  }
533
535
 
534
536
  /**
@@ -551,19 +553,42 @@ export class Client<TContext = {}> {
551
553
  const self = this;
552
554
  const caller: CallerContext = callerOf(options);
553
555
  return (async function* () {
554
- const { path, input: hydratedInput } = await self.prepareRef(refOrPath, input, caller);
555
- yield* self.execStreamInternal<TOutput>(path, hydratedInput, caller);
556
+ // The root call gives the items: the kernel gets the stream of the root call as its result
557
+ const items = await self.runRef(self.prepareRef(refOrPath, input), caller, (path, value) =>
558
+ self.execStreamInternal<TOutput>(path, value, caller),
559
+ );
560
+ yield* items as AsyncIterable<TOutput>;
556
561
  })();
557
562
  }
558
563
 
559
564
  /**
560
- * The path and the input of a reference, with the refs of the input run (hydrated).
565
+ * This function runs a program on the kernel. The nested calls go through `execInternal`, and
566
+ * `root` starts the root call.
561
567
  */
562
- private async prepareRef(
568
+ private runRef(ref: ProcedureRef, caller: CallerContext, root: (path: ProcedurePath, input: unknown) => unknown): Promise<unknown> {
569
+ // A procedure that takes its input raw (control flow, the `runs-refs` and `raw-input` tags,
570
+ // `procedure.define`) gets its operand refs RAW: it runs them itself, with correct
571
+ // ordering/laziness/error-scoping. Eagerly hydrating here would run the operands up-front
572
+ // and can hijack an all-refs array into an implicit chain that clobbers `steps`. See
573
+ // documentation/BUGS-2026-07.md (C2, H2, H3, M35). The procedure in the registry decides,
574
+ // also for the refs nested in the input (deep dive CORE-5: before, the last path segment did).
575
+ // The body of a defined procedure starts its root call the same way (runProgram, DOC-3)
576
+ return runProgram(ref, {
577
+ executor: <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => this.execInternal<TOut>(path, inp, caller),
578
+ rootExecutor: root,
579
+ rawInput: rawInputRule(this.procedureRegistry),
580
+ onRef: caller.onRef,
581
+ });
582
+ }
583
+
584
+ /**
585
+ * The root call of a program, as a ref: an envelope program changes into the `$proc` form, a
586
+ * path gets its input, and the fields of `input` go over the fields of the input of a ref.
587
+ */
588
+ private prepareRef(
563
589
  refOrPathOrEnvelope: AnyProcedureRef | ProcedurePath | EnvelopeProgram,
564
590
  input: unknown,
565
- caller: CallerContext = {}
566
- ): Promise<{ path: ProcedurePath; input: unknown }> {
591
+ ): ProcedureRef {
567
592
  // A program in the envelope form (coeval M7) changes into the $proc form first. The root
568
593
  // decides the form: an envelope in the input of a $proc program stays data.
569
594
  const refOrPath = isEnvelopeProgram(refOrPathOrEnvelope)
@@ -592,20 +617,7 @@ export class Client<TContext = {}> {
592
617
  } else {
593
618
  throw new Error("exec() requires a ProcedureRef, $proc JSON, or procedure path array");
594
619
  }
595
-
596
- // Create executor function for hydration
597
- const executor = async <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => {
598
- return this.execInternal<TOut>(path, inp, caller);
599
- };
600
-
601
- // A procedure that takes its input raw (control flow, the `runs-refs` and `raw-input` tags,
602
- // `procedure.define`) gets its operand refs RAW: it runs them itself, with correct
603
- // ordering/laziness/error-scoping. Eagerly hydrating here would run the operands up-front
604
- // and can hijack an all-refs array into an implicit chain that clobbers `steps`. See
605
- // documentation/BUGS-2026-07.md (C2, H2, H3, M35). The procedure in the registry decides,
606
- // also for the refs nested in the input (deep dive CORE-5: before, the last path segment did).
607
- // The body of a defined procedure starts its root call the same way (prepareRootCall, DOC-3)
608
- return prepareRootCall(ref, executor, { rawInput: rawInputRule(this.procedureRegistry), onRef: caller.onRef });
620
+ return ref;
609
621
  }
610
622
 
611
623
  /**
@@ -280,7 +280,9 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
280
280
  // The items pass through as they arrive (BUGS-2026-07 M2: before, the middleware
281
281
  // collected the whole stream first). A retry is possible only while no item has
282
282
  // reached the reader: the first item is a retryable error, or the attempt throws
283
- // before its first item. After that, a retry would repeat items.
283
+ // before its first item. After that, a retry would repeat items. The server gives an error
284
+ // without a flag before the first item the flag of its code, so a stream that fails with
285
+ // UNAVAILABLE before its first item goes again (CLI-68).
284
286
  let started = false;
285
287
  let lastItem: ResponseItem<TRes> | undefined;
286
288
  let retryItem: ResponseItem<TRes> | undefined;
@@ -130,17 +130,9 @@ import {
130
130
  getRefWhen,
131
131
  WHEN_NEVER,
132
132
  RUNS_REFS_TAG,
133
- type RefScope,
133
+ type AnyProcedureRef,
134
134
  } from "../ref.js";
135
- import {
136
- substituteRefs,
137
- hydrateOperand,
138
- callOperand,
139
- evaluateOperand,
140
- parentContext,
141
- scopeWith,
142
- controlFlowTags,
143
- } from "./operands.js";
135
+ import { substituteRefs, runControlFlow, controlFlowTags } from "./operands.js";
144
136
 
145
137
  // =============================================================================
146
138
  // Chain Procedure
@@ -182,57 +174,27 @@ const chainProcedure: ChainProcedure = defineProcedure({
182
174
  tags: controlFlowTags("control-flow"),
183
175
  },
184
176
  handler: async (input: ChainInput, ctx?: ProcedureContext): Promise<ChainOutput> => {
185
- const { steps, passThrough, initialInput, ...parentInput } = input;
177
+ const { steps } = input;
186
178
  if (!Array.isArray(steps)) {
187
179
  throw new Error(`client.chain: steps must be an array, got ${steps === null ? "null" : typeof steps}`);
188
180
  }
189
- const results: unknown[] = [];
190
- // The scope of an outer chain is the parent: its names stay visible here
181
+ if (ctx?.client) {
182
+ // The runner of the kind client.chain runs the steps on the kernel (program.ts). A step reads
183
+ // the names of the earlier steps, $last and the names of the caller (the scope of the input).
184
+ return (await runControlFlow("chain", input, ctx)) as ChainOutput;
185
+ }
186
+ // No client context: the result of a step is its input, with the known $refs resolved
191
187
  const scope = createRefScope(scopeOf(input));
192
-
193
- // Extract context to propagate to steps (e.g., cwd, node)
194
- const { cwd, node } = parentInput as { cwd?: string; node?: unknown };
195
-
196
- // With passThrough, the value that the next step without input gets (docs review DOC-4:
197
- // before, the procedure ignored passThrough and initialInput)
198
- let passed: unknown = passThrough && ctx?.client ? await hydrateOperand(initialInput, ctx, scope) : undefined;
199
-
188
+ const results: unknown[] = [];
200
189
  for (const step of steps) {
201
- let result: unknown;
202
-
203
- if (isAnyProcedureRef(step) && getRefWhen(step) === WHEN_NEVER) {
204
- // A $never step is data, like a $never ref anywhere (docs review DOC-1)
205
- result = step;
206
- } else if (!ctx?.client) {
207
- // No client context: the result of a step is its input, with the known $refs resolved
208
- result = substituteRefs(isAnyProcedureRef(step) ? normalizeRef(step).input : step, scope);
209
- } else if (isAnyProcedureRef(step)) {
210
- // Resolve the $refs of the step's input, run its nested refs, add the parent context (cwd, node).
211
- // With passThrough, a step with no input gets the passed value as a literal: it ran already.
212
- const operand =
213
- passThrough && normalizeRef(step).input === undefined ? { ...step, input: { $literal: passed } } : step;
214
- result = await callOperand(operand, ctx, { scope, extra: parentContext({ cwd, node }) });
215
- } else {
216
- // A $ref, a $literal, or a value that holds refs
217
- result = await hydrateOperand(step, ctx, scope);
218
- }
219
-
220
- // Store named output
221
- const stepName = isAnyProcedureRef(step) ? normalizeRef(step).$name : undefined;
222
- if (stepName) {
223
- scope.outputs.set(stepName, result);
224
- }
225
-
226
- // Update $last
190
+ const call = isAnyProcedureRef(step) ? normalizeRef(step) : undefined;
191
+ // A $never step is data, like a $never ref anywhere (docs review DOC-1)
192
+ const result = call && getRefWhen(step as AnyProcedureRef) === WHEN_NEVER ? step : substituteRefs(call ? call.input : step, scope);
193
+ if (call?.$name) scope.outputs.set(call.$name, result);
227
194
  scope.last = result;
228
- passed = result;
229
195
  results.push(result);
230
196
  }
231
-
232
- return {
233
- results,
234
- final: results[results.length - 1],
235
- };
197
+ return { results, final: results[results.length - 1] };
236
198
  },
237
199
  });
238
200
 
@@ -245,7 +207,13 @@ interface ParallelInput {
245
207
  tasks: unknown[];
246
208
  /** Maximum concurrency (default: unlimited) */
247
209
  concurrency?: number;
248
- /** Whether to fail fast on first error (default: false) */
210
+ /**
211
+ * Whether to fail fast on first error (default: false).
212
+ *
213
+ * After the first error, the other tasks stop at their next wait, and their later calls do not
214
+ * start (D26). Before 0.26.0, they went on in the background to their end. When those calls
215
+ * must start, wait for the tasks yourself, or do not use `failFast`.
216
+ */
249
217
  failFast?: boolean;
250
218
  }
251
219
 
@@ -254,7 +222,12 @@ interface ParallelOutput {
254
222
  results: unknown[];
255
223
  /** Whether all tasks succeeded */
256
224
  allSucceeded: boolean;
257
- /** Errors from failed tasks */
225
+ /**
226
+ * Errors from failed tasks.
227
+ *
228
+ * The error text of a task that throws `undefined` is the reason of the kernel (D26). Before
229
+ * 0.26.0, it was `"undefined"`.
230
+ */
258
231
  errors: Array<{ index: number; error: string }>;
259
232
  }
260
233
 
@@ -282,64 +255,12 @@ const parallelProcedure: ParallelProcedure = defineProcedure({
282
255
  tags: controlFlowTags("control-flow"),
283
256
  },
284
257
  handler: async (input: ParallelInput, ctx?: ProcedureContext): Promise<ParallelOutput> => {
285
- const { tasks, concurrency, failFast } = input as ParallelInput & {
286
- cwd?: string;
287
- node?: unknown;
288
- };
289
- const { cwd, node } = input as { cwd?: string; node?: unknown };
290
- const scope = scopeOf(input);
291
-
292
- // Operands arrive raw (exec() does not pre-hydrate control-flow procedures). Execute
293
- // task refs concurrently, honoring `concurrency` and `failFast`, and report per-task
294
- // errors. Other tasks are values: their refs run. See BUGS-2026-07 M35.
295
- const runTask = async (task: unknown): Promise<unknown> => {
296
- if (!ctx?.client) {
297
- return task;
298
- }
299
- return evaluateOperand(task, ctx, { scope, extra: parentContext({ cwd, node }) });
300
- };
301
-
302
- const results: unknown[] = new Array(tasks.length);
303
- const errors: Array<{ index: number; error: string }> = [];
304
-
305
- const limit =
306
- typeof concurrency === "number" && concurrency > 0
307
- ? Math.min(concurrency, tasks.length)
308
- : tasks.length;
309
-
310
- let nextIndex = 0;
311
- let aborted = false;
312
-
313
- const worker = async (): Promise<void> => {
314
- while (!aborted) {
315
- const index = nextIndex++;
316
- if (index >= tasks.length) {
317
- return;
318
- }
319
- try {
320
- results[index] = await runTask(tasks[index]);
321
- } catch (error) {
322
- const message = error instanceof Error ? error.message : String(error);
323
- if (failFast) {
324
- aborted = true;
325
- throw error;
326
- }
327
- errors.push({ index, error: message });
328
- results[index] = undefined;
329
- }
330
- }
331
- };
332
-
333
- const workerCount = Math.max(1, limit);
334
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
335
-
336
- errors.sort((a, b) => a.index - b.index);
337
-
338
- return {
339
- results,
340
- allSucceeded: errors.length === 0,
341
- errors,
342
- };
258
+ // Operands arrive raw (exec() does not pre-hydrate control-flow procedures). The runner of
259
+ // the kind client.parallel runs the tasks at the same time on the kernel, with `concurrency`
260
+ // and `failFast`, and reports the error of each task. See BUGS-2026-07 M35.
261
+ if (ctx?.client) return (await runControlFlow("parallel", input, ctx)) as ParallelOutput;
262
+ // Without a client, nothing can run: each task is its value
263
+ return { results: Array.from(input.tasks), allSucceeded: true, errors: [] };
343
264
  },
344
265
  });
345
266
 
@@ -367,38 +288,15 @@ const conditionalProcedure: ConditionalProcedure = defineProcedure({
367
288
  tags: controlFlowTags("control-flow"),
368
289
  },
369
290
  handler: async (input: ConditionalInput, ctx?: ProcedureContext): Promise<unknown> => {
370
- const { condition: rawCondition, then: thenValue, else: elseValue, ...parentInput } = input as ConditionalInput & {
371
- cwd?: string;
372
- node?: unknown;
373
- };
374
-
375
- // Extract context to propagate to branches (e.g., cwd, node)
376
- const { cwd, node } = parentInput;
377
- const scope = scopeOf(input);
378
-
379
291
  // Operands arrive raw (exec() does not pre-hydrate control-flow procedures), so the
380
- // condition runs here, before its truthiness is known. See BUGS-2026-07 H2. A condition
381
- // that is a $ref reads the scope of the caller.
382
- const condition: unknown = ctx?.client
383
- ? await evaluateOperand(rawCondition, ctx, { scope, extra: parentContext({ cwd, node }) })
384
- : rawCondition;
385
-
386
- // Determine truthiness - check for .value property (from predicates like git.hasChanges)
387
- let isTruthy: boolean;
388
- if (condition && typeof condition === "object" && "value" in condition) {
389
- isTruthy = Boolean((condition as { value: unknown }).value);
390
- } else {
391
- isTruthy = Boolean(condition);
392
- }
393
-
394
- // Select the branch to execute/return
395
- const selectedBranch = isTruthy ? thenValue : elseValue;
396
-
397
- // Only the selected branch runs. Without a client, the branch is the result as it is.
398
- if (selectedBranch === undefined || !ctx?.client) {
399
- return selectedBranch;
400
- }
401
- return evaluateOperand(selectedBranch, ctx, { scope, extra: parentContext({ cwd, node }) });
292
+ // condition runs in the runner of the kind client.conditional, before its truthiness is
293
+ // known, and only the selected branch runs. See BUGS-2026-07 H2.
294
+ if (ctx?.client) return runControlFlow("conditional", input, ctx);
295
+ // Without a client, the branch is the result as it is. The `value` field of an object decides
296
+ // (a predicate such as git.hasChanges gives { value }).
297
+ const { condition } = input;
298
+ const truthy = condition && typeof condition === "object" && "value" in condition ? Boolean((condition as { value: unknown }).value) : Boolean(condition);
299
+ return truthy ? input.then : input.else;
402
300
  },
403
301
  });
404
302
 
@@ -426,16 +324,17 @@ import {
426
324
  * is the last value. (Deep dive CORE-1: before, the handler only tested the raw operands, and a
427
325
  * ref is always truthy, so no operand ran.)
428
326
  */
429
- function shortCircuit(stopOn: (value: unknown) => boolean) {
327
+ function shortCircuit(name: "and" | "or", stopOn: (value: unknown) => boolean) {
430
328
  return async (input: { values: unknown[]; cwd?: string; node?: unknown }, ctx?: ProcedureContext): Promise<unknown> => {
431
329
  const { values } = input;
432
330
  if (!Array.isArray(values)) {
433
331
  throw new Error(`values must be an array, got ${values === null ? "null" : typeof values}`);
434
332
  }
435
- const scope = scopeOf(input);
333
+ // The runner of the kind client.and or client.or runs the operands in order on the kernel
334
+ if (ctx?.client) return runControlFlow(name, input, ctx);
436
335
  let last: unknown = undefined;
437
336
  for (const operand of values) {
438
- last = ctx?.client ? await evaluateOperand(operand, ctx, { scope, extra: parentContext(input) }) : operand;
337
+ last = operand;
439
338
  if (stopOn(last)) {
440
339
  return last;
441
340
  }
@@ -456,7 +355,7 @@ const andProcedure: AndProcedure = defineProcedure({
456
355
  input: LogicInputSchema as any,
457
356
  output: anyOutput("The first falsy operand value, or the last value"),
458
357
  metadata: { ...andMetadata, tags: [...andMetadata.tags, RUNS_REFS_TAG] },
459
- handler: shortCircuit((value) => !value),
358
+ handler: shortCircuit("and", (value) => !value),
460
359
  });
461
360
 
462
361
  interface OrInput {
@@ -471,7 +370,7 @@ const orProcedure: OrProcedure = defineProcedure({
471
370
  input: LogicInputSchema as any,
472
371
  output: anyOutput("The first truthy operand value, or the last value"),
473
372
  metadata: { ...orMetadata, tags: [...orMetadata.tags, RUNS_REFS_TAG] },
474
- handler: shortCircuit((value) => Boolean(value)),
373
+ handler: shortCircuit("or", (value) => Boolean(value)),
475
374
  });
476
375
 
477
376
  interface NotInput {
@@ -556,27 +455,6 @@ const MapOutputSchema = z.looseObject({
556
455
 
557
456
  type MapProcedure = Procedure<MapInput, MapOutput, { description: string; tags: string[] }>;
558
457
 
559
- /**
560
- * This function runs the refs of an `items` operand. It runs each element separately and in
561
- * order, so an array whose elements are all refs stays an array (hydration makes such an array
562
- * a chain).
563
- */
564
- async function hydrateItems(items: unknown, ctx: ProcedureContext | undefined, scope: RefScope | undefined): Promise<unknown[]> {
565
- if (!ctx?.client) return Array.isArray(items) ? items : [];
566
- let value: unknown;
567
- if (Array.isArray(items)) {
568
- const results: unknown[] = [];
569
- for (const item of items) results.push(await hydrateOperand(item, ctx, scope));
570
- value = results;
571
- } else {
572
- value = await hydrateOperand(items, ctx, scope);
573
- }
574
- if (!Array.isArray(value)) {
575
- throw new Error(`items must be an array, got ${value === null ? "null" : typeof value}`);
576
- }
577
- return value;
578
- }
579
-
580
458
  const mapProcedure: MapProcedure = defineProcedure({
581
459
  path: ["map"],
582
460
  input: MapInputSchema as any,
@@ -586,23 +464,15 @@ const mapProcedure: MapProcedure = defineProcedure({
586
464
  tags: controlFlowTags("collection"),
587
465
  },
588
466
  handler: async (input: MapInput, ctx?: ProcedureContext): Promise<MapOutput> => {
589
- // map is a control-flow procedure: its operands arrive raw, and it runs them here.
590
- // Before, the handler only returned the raw items, so their refs never ran and no
591
- // function was applied.
467
+ // map is a control-flow procedure: its operands arrive raw, and the runner of the kind
468
+ // client.map runs them on the kernel. Before, the handler only returned the raw items, so
469
+ // their refs never ran and no function was applied.
592
470
  if (input.fn !== undefined && !isAnyProcedureRef(input.fn)) {
593
471
  // Before, a fn that was not a call was ignored without a word (roadmap R4)
594
472
  throw new Error("client.map: fn must be one call ({ $proc, input }), or absent");
595
473
  }
596
- const outer = scopeOf(input);
597
- const items = await hydrateItems(input.items, ctx, outer);
598
- if (!isAnyProcedureRef(input.fn) || !ctx?.client) {
599
- return { results: items };
600
- }
601
- const results: unknown[] = [];
602
- for (const [index, item] of items.entries()) {
603
- results.push(await callOperand(input.fn, ctx, { scope: scopeWith({ [input.as ?? "item"]: item, index }, outer) }));
604
- }
605
- return { results };
474
+ if (ctx?.client) return (await runControlFlow("map", input, ctx)) as MapOutput;
475
+ return { results: Array.isArray(input.items) ? input.items : [] };
606
476
  },
607
477
  });
608
478
 
@@ -643,18 +513,10 @@ const reduceProcedure: ReduceProcedure = defineProcedure({
643
513
  if (!ctx?.client) {
644
514
  return input.accumulated ?? input.initial;
645
515
  }
646
- if (!isAnyProcedureRef(input.fn)) {
647
- // Without fn, the result is the accumulated value, hydrated like any operand. (Docs review
648
- // DOC-4: before, a call in initial did not run, while map without fn hydrated its items.)
649
- return hydrateOperand(input.accumulated ?? input.initial, ctx, scopeOf(input));
650
- }
651
- const outer = scopeOf(input);
652
- const items = await hydrateItems(input.items, ctx, outer);
653
- let acc = await hydrateOperand(input.initial, ctx, outer);
654
- for (const [index, item] of items.entries()) {
655
- acc = await callOperand(input.fn, ctx, { scope: scopeWith({ acc, item, index }, outer) });
656
- }
657
- return acc;
516
+ // The runner of the kind client.reduce runs the operands on the kernel. Without fn, the
517
+ // result is the accumulated value, hydrated like any operand. (Docs review DOC-4: before, a
518
+ // call in initial did not run, while map without fn hydrated its items.)
519
+ return runControlFlow("reduce", input, ctx);
658
520
  },
659
521
  });
660
522
 
@@ -752,7 +614,12 @@ interface TryCatchOutput {
752
614
  success: boolean;
753
615
  /** Result value */
754
616
  value: unknown;
755
- /** Error if failed */
617
+ /**
618
+ * Error if failed.
619
+ *
620
+ * The error text of a `try` that throws `undefined` is the reason of the kernel (D26). Before
621
+ * 0.26.0, it was `"undefined"`.
622
+ */
756
623
  error?: string;
757
624
  }
758
625
 
@@ -773,25 +640,14 @@ const tryCatchProcedure: TryCatchProcedure = defineProcedure({
773
640
  tags: controlFlowTags("control-flow"),
774
641
  },
775
642
  handler: async (input: TryCatchInput, ctx?: ProcedureContext): Promise<TryCatchOutput> => {
776
- const { try: tryValue, catch: catchValue } = input;
777
-
778
643
  // Without a client, nothing can run: `try` is the value
779
644
  if (!ctx?.client) {
780
- return { success: true, value: tryValue };
781
- }
782
-
783
- // Operands arrive raw (exec() does not pre-hydrate control-flow procedures), so the
784
- // `try` operand runs HERE inside a real JS try/catch. If it throws, the `catch` operand
785
- // runs (or its value is the result). See BUGS-2026-07 H3.
786
- const scope = scopeOf(input);
787
- try {
788
- const value = await evaluateOperand(tryValue, ctx, { scope });
789
- return { success: true, value };
790
- } catch (error) {
791
- const message = error instanceof Error ? error.message : String(error);
792
- const value = await evaluateOperand(catchValue, ctx, { scope: scopeWith({ error: message }, scope) });
793
- return { success: false, value, error: message };
645
+ return { success: true, value: input.try };
794
646
  }
647
+ // Operands arrive raw (exec() does not pre-hydrate control-flow procedures), so the runner of
648
+ // the kind client.tryCatch runs the `try` operand. If it fails, the `catch` operand runs with
649
+ // the name `error`, and its value is the result. See BUGS-2026-07 H3.
650
+ return (await runControlFlow("tryCatch", input, ctx)) as TryCatchOutput;
795
651
  },
796
652
  });
797
653